news 2026/5/1 20:14:23

【黑马点评日记】异步秒杀:异步线程和阻塞队列以及Lua脚本的相关流程分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【黑马点评日记】异步秒杀:异步线程和阻塞队列以及Lua脚本的相关流程分析

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:苍穹外卖日记,SSM框架深入,JavaWeb
命运的结局尽可永在,不屈的挑战却不可须臾或缺!

前言:

前面我们分析了异步秒杀的整体流程以及实现,但是其中关于异步线程的实现,以及执行流程感觉很混乱和复杂,以及异步线程和主线程各自的职责也分不清。这里就针对这一流程,进行分析,看看异步线程到底是怎么实现的,完全搞明白。

摘要:

本文深入分析了异步秒杀系统的实现流程,重点解析了异步线程与主线程的职责划分。系统采用三层防护机制:Lua脚本快速拦截重复请求(RedisSet)、分布式锁处理并发请求、数据库唯一索引兜底。通过阻塞队列实现生产消费模式,主线程负责校验和入队(2-3ms快速响应),独立线程池异步处理订单。关键设计包括:1)RedisSet与数据库双重校验;2)分布式锁保证并发安全;3)事务注解确保数据一致性。这种架构既保障了高并发性能(每秒数万请求),又通过多级防护机制确保了数据准确性,体现了"快速失败"和"最终一致"的设计思想。

整体架构图

text

┌─────────────────────────────────────────────────────────────────────────────┐ │ 项目启动时 │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ @PostConstruct init() 执行 │ │ │ │ → 提交 VoucherOrderHandler 任务到线程池 │ │ │ │ → 消费者线程启动,执行 while(true) { orderTasks.take() } │ │ │ │ → 此时队列为空,线程阻塞等待 │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ ↓ 用户请求到达 ┌─────────────────────────────────────────────────────────────────────────────┐ │ 【生产者】Tomcat 线程执行 seckillVoucher() │ ├─────────────────────────────────────────────────────────────────────────────┤ │ 1. 获取用户ID(从 ThreadLocal) │ │ 2. 执行 Lua 脚本(Redis 扣库存 + 去重) │ │ ├─ 返回 1 → 库存不足,直接返回失败 │ │ ├─ 返回 2 → 重复下单,直接返回失败 │ │ └─ 返回 0 → 秒杀资格校验通过 │ │ 3. 生成订单ID(雪花算法) │ │ 4. 创建 VoucherOrder 对象(id、userId、voucherId) │ │ 5. 放入阻塞队列 orderTasks.add(voucherOrder) ← 核心:只放队列,不写数据库 │ │ 6. 获取代理对象 proxy = AopContext.currentProxy() │ │ 7. 返回订单ID给用户("排队中")← Tomcat 线程结束,总耗时约 2-3ms │ └─────────────────────────────────────────────────────────────────────────────┘ ↓ 队列中有数据了 ┌─────────────────────────────────────────────────────────────────────────────┐ │ 【消费者】独立线程执行 VoucherOrderHandler │ ├─────────────────────────────────────────────────────────────────────────────┤ │ while(true) { │ │ voucherOrder = orderTasks.take(); ← 被唤醒,拿到订单 │ │ handleVoucherOrder(voucherOrder); ← 处理订单 │ │ } │ └─────────────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────────────┐ │ handleVoucherOrder() 订单处理 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ 1. 获取用户ID │ │ 2. 获取分布式锁:RLock lock = redissonClient.getLock("lock:order:"+userId) │ │ 3. tryLock() 尝试加锁(非阻塞) │ │ ├─ 获取失败 → 直接返回(不处理,可能有丢失风险) │ │ └─ 获取成功 → 执行 proxy.createVoucherOrder(voucherOrder) │ │ 4. 释放锁 │ └─────────────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────────────┐ │ createVoucherOrder() 实际写数据库(@Transactional) │ ├─────────────────────────────────────────────────────────────────────────────┤ │ 1. 查询订单是否存在(一人一单校验) │ │ └─ 已存在 → 记录日志,返回 │ │ 2. 扣减数据库库存(乐观锁:stock>0) │ │ └─ 失败 → 记录日志,返回 │ │ 3. 保存订单 save(voucherOrder) │ └─────────────────────────────────────────────────────────────────────────────┘

各组件职责

组件类型职责执行线程
seckillVoucher()生产者Lua校验 + 放入队列 + 返回订单IDTomcat线程
orderTasks阻塞队列缓冲订单,解耦生产者和消费者
VoucherOrderHandler消费者从队列取订单,调用处理逻辑独立线程池
handleVoucherOrder()处理器加分布式锁 + 调用下单消费者线程
createVoucherOrder()数据库操作一人一单校验 + 扣库存 + 保存订单消费者线程

关于异步线程的流程:

【消费者线程】(独立线程池中的一个线程) │ ▼ VoucherOrderHandler.run() │ ├── orderTasks.take() ← 从队列取订单 │ ▼ handleVoucherOrder(voucherOrder) │ ├── 加分布式锁 │ ▼ proxy.createVoucherOrder(voucherOrder) │ ├── 一人一单查询(数据库) ├── 扣库存(数据库) ├── 保存订单(数据库) │ ▼ (方法结束,释放锁,回到 while 循环,继续 take 下一个订单)

职责分离,不是线程分离

方法职责为什么单独拆出来
VoucherOrderHandler从队列取订单,循环调度独立线程的入口,负责"取任务"
handleVoucherOrder()加锁、解锁把"锁管理"和"业务逻辑"分离
createVoucherOrder()一人一单 + 扣库存 + 保存纯业务逻辑,方便加事务注解

好处:

  • 每个方法只做一件事,便于理解和维护

  • createVoucherOrder()可以单独加@Transactional

为什么要加分布式锁呢:

首先要明确:

单线程下可以不需要

text

队列中的订单顺序: ┌────────────┬────────────┬────────────┐ │ 用户A-请求1 │ 用户A-请求2 │ 用户B-请求1 │ ... └────────────┴────────────┴────────────┘ ↓ 单线程消费者(一个一个处理) ↓ 处理请求1:查询数据库(无订单)→ 创建订单 → 提交 ↓ 处理请求2:查询数据库(已有订单)→ 拒绝

关键:请求1 处理完并提交事务后,请求2 才开始处理。所以请求2 查询时一定能看到请求1 插入的订单。

线程处理顺序查询订单数结果
单线程消费者先处理请求10创建订单
单线程消费者后处理请求21(请求1已提交)拒绝

结论:单线程下,不需要分布式锁就能保证一人一单,因为不存在并发执行。

多线程情况下

时间线(极短的时间内):

请求1(线程A) 请求2(线程B)
│ │
▼ ▼
Lua脚本执行 Lua脚本执行
├─ 检查Redis Set:无记录 ✅ ├─ 检查Redis Set:无记录 ✅
├─ 扣Redis库存 ├─ 扣Redis库存
└─ 写Redis Set(还未生效) └─ 写Redis Set(还未生效)
│ │
▼ ▼
放入阻塞队列 放入阻塞队列
│ │
▼ ▼
消费者线程A处理 消费者线程B处理
├─ 查数据库:无订单 ├─ 查数据库:无订单
├─ 扣数据库库存 ├─ 扣数据库库存
└─ 保存订单(未提交) └─ 保存订单(未提交)
│ │
▼ ▼
提交事务 ✅ 提交事务 ✅

结果:用户A创建了2个订单 ❌

分布式锁必须用(并发请求)

这种情况是:用户A的第一次请求还在处理中,还没出结果,紧接着第二个请求就来了。

假设用户A在两台手机上同时点击。

  • 请求1(消费者线程A处理):执行 Lua 脚本,发现 Redis Set 里没有userId,扣库存成功。

  • 紧接着,极短的时间内(请求1还没提交数据库)

  • 请求2(另一个消费者线程B处理):执行 Lua 脚本时,由于请求1还没来得及把userId写入 Redis Set,所以 Redis Set 里依然没有userId!脚本也会判定成功

Redis Set 失效了。因为 Redis Set 的写入和数据库订单的写入,不是同一个原子操作。

结果就是:两个线程都认为可以下单,然后都去执行后面的createVoucherOrder方法,最终导致同一个用户插入了两条订单记录,一人两单

分布式锁的作用:给并发请求排队

handleVoucherOrder里的分布式锁,就是为了解决上面的场景。

它的逻辑变成了:

  • 请求1:获取lock:order:用户A锁。成功!去执行createVoucherOrder(查库、写库)。

  • 请求2:尝试获取同一把锁。发现锁被占用了,拿不到。直接失败返回。

这样一来,即使 Redis Set 没能拦住请求2,分布式锁也能在数据库的门口,把请求2给拦住。

总结

问题答案
Lua 脚本和一人一单有关系吗?✅ 有,它是第一道快速防线
Lua 脚本能完全替代数据库一人一单吗?❌ 不能,Redis 可能丢数据,需要数据库兜底
为什么不只用数据库一人一单?因为每次请求都查数据库,性能太差
为什么不只用 Redis 一人一单?Redis 不是持久化存储,数据可能丢失
操作命令作用
判断用户是否存在sismember orderKey userId防重:用户是否已秒杀过
记录用户sadd orderKey userId标记用户已秒杀

为什么需要两重防重

防重层位置作用能拦截什么
Lua脚本 + Redis Set秒杀入口快速拦截用户重复点击、并发请求
数据库查询消费者线程兜底Redis Set 丢失、过期或遗漏

第一层(Redis Set)拦截 99.9% 的重复请求,毫秒级返回。
第二层(数据库查询)保证最终一致性,防止 Redis 状态丢失。

为什么有 Redis Set 还需要数据库查询

因为 Redis 可能丢失数据:

场景Redis Set数据库查询结果
正常情况有记录 → 拦截防重生效 ✅
Redis 重启丢失无记录 → 放行查询到订单 → 拦截防重依然生效 ✅
Redis key 过期无记录 → 放行查询到订单 → 拦截防重依然生效 ✅

数据库查询是最后的防线,保证即使 Redis 出问题,也不会一人多单。

Redis Set 保证了:同一个用户只能秒杀成功一次。

因为 Lua 脚本在扣库存之前,先判断用户是否在 Set 中,如果在就直接返回失败。

一句话:Lua 脚本中的 Redis Set 是"快校验",数据库查询是"准校验"。快校验拦截大部分重复请求,准校验兜底保证最终数据正确。两者互补,不是重复。

简单来说:Lua脚本的Redis Set是用来拦住“第二次请求”的,而分布式锁是用来防止“并发请求”的。

理清它们面对的不同并发场景


总结一下三者的关系

组件防御位置针对的并发目的
Lua脚本 (Redis Set)入口处串行的重复请求快速拦截老用户,减轻后端压力。
分布式锁数据库门前并发的重复请求强行将并发操作串行化,防止数据库出现脏数据。
数据库唯一索引数据库内部任何突破前两道防线的漏网之鱼最后的、绝对的兜底保障。

你可以这样理解:

  • Redis Set是一个高效的“门卫”,能拦住大部分明显可疑的人(已经买过的)。

  • 分布式锁是一个“队列管理员”,强制要求几个同时到达的人排好队,一个一个进去。

  • 数据库唯一索引是“钢化玻璃门”,万一前面的人都失守了,这是最后一道不会出错的防线。

所以,Lua脚本的“一人一单”和分布式锁的“一人一单”是互补的,缺一不可。(多线程)

锁方案单实例单线程单实例多线程多实例集群
synchronized✅ 能用✅ 能用❌ 失效
Redisson 分布式锁✅ 能用(杀鸡用牛刀)✅ 能用✅ 能用

因此:代码中用分布式锁不是为了解决当前的单线程问题,而是为了兼容未来可能的多线程、集群部署场景。这是一个前瞻性的设计。

完整代码:

package com.hmdp.service.impl; import com.hmdp.dto.Result; import com.hmdp.entity.VoucherOrder; import com.hmdp.mapper.VoucherOrderMapper; import com.hmdp.service.ISeckillVoucherService; import com.hmdp.service.IVoucherOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.utils.RedisIdWork; import com.hmdp.utils.UserHolder; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.aop.framework.AopContext; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.Collections; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * <p> * 服务实现类 * </p> * * @author 虎哥 * @since 2021-12-22 */ @Slf4j @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private ISeckillVoucherService iSeckillVoucherService; @Resource private RedisIdWork redisIdWork; @Resource private StringRedisTemplate stringRedisTemplate; @Resource private RedissonClient redissonClient; private static final DefaultRedisScript<Long> SECKILL_SCRIPT ; //静态代码块进行初始化 static { SECKILL_SCRIPT = new DefaultRedisScript<>(); SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua")); SECKILL_SCRIPT.setResultType(Long.class); } //阻塞队列 private BlockingQueue<VoucherOrder> orderTasks=new ArrayBlockingQueue<>(1024*1024); //创建一个线程池实现异步下单 private static final ExecutorService SECKILL_ORDER_EXECUTOR= Executors.newSingleThreadExecutor(); //执行时机,当前类初始化完成后执行 @PostConstruct private void init(){ //提交任务 SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()); } //创建一个线程任务,执行下面的run方法 private class VoucherOrderHandler implements Runnable{ //秒杀抢购之前开启任务, @Override public void run() { //要不断地从队列中取 while (true){ try { //1.获取队列的信息 VoucherOrder voucherOrder = orderTasks.take(); //2.创建订单 handleVoucherOrder(voucherOrder); } catch (Exception e) { log.info("处理订单异常",e); } } } } //创建订单 private void handleVoucherOrder(VoucherOrder voucherOrder) { //尝试创建锁对象 //因为是多线程,是从线程池获得的全新的线程,不是ThreadLocal Long userId = voucherOrder.getUserId(); RLock lock = redissonClient.getLock("lock:order:" + userId); //获取锁 boolean isLock = lock.tryLock(); //判断锁获取是否成功 if (!isLock){ //获取失败 } try { proxy. createVoucherOrder(voucherOrder); } finally { //释放锁 lock.unlock(); } } /** * 下单秒杀优惠卷 * @param voucherId 优惠券id * @return */ private IVoucherOrderService proxy; public Result seckillVoucher(Long voucherId){ //获取用户id Long userId = UserHolder.getUser().getId(); //1.执行lua脚本 Long result=stringRedisTemplate.execute( SECKILL_SCRIPT , Collections.emptyList() , voucherId.toString(),userId.toString() ); //判断结果是否为0 int r=result.intValue(); //不为0,则代表没有购买资格 if (r!=0){ return Result.fail(r==1?"库存不足":"不能重复下单"); } //若为0,将下单信息保存到阻塞队列中 //先把下单信息封装 //创建订单 VoucherOrder voucherOrder = new VoucherOrder(); // 5.1 生成全局唯一订单ID long orderId = redisIdWork.nextId("order"); voucherOrder.setId(orderId); // 5.2 设置用户ID(从ThreadLocal获取当前登录用户) voucherOrder.setUserId(userId); // 5.3 设置优惠券ID voucherOrder.setVoucherId(voucherId); //放入阻塞队列 orderTasks.add(voucherOrder); //获取代理对象,同理也拿不到线程,因此放在这里,初始化 proxy = (IVoucherOrderService) AopContext.currentProxy(); //返回订单id return Result.ok(orderId); } @Transactional //异步线程,不需要返回值 public void createVoucherOrder(VoucherOrder voucherOrder) { //6.一人一单 Long userId=voucherOrder.getUserId(); //将synchronized 加在用户上 //6.1根据查询订单,是否购买过 int count= query().eq("user_Id",userId).eq("voucher_Id", voucherOrder.getVoucherId()).count(); //6.2判断是否存在 if (count>0){ log.error("用户已经购买过一次"); return; } //扣减库存 Boolean success=iSeckillVoucherService. update() .setSql("stock=stock-1") .eq("voucher_id", voucherOrder.getVoucherId()) .gt("stock",0) .update(); if (!success){ log.error("库存不足"); return; } // 保存订单 save(voucherOrder); } }

结语:如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 20:10:24

三步搞定抖音内容保存:你的专属无水印下载神器

三步搞定抖音内容保存&#xff1a;你的专属无水印下载神器 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback support. 抖音…

作者头像 李华
网站建设 2026/5/1 20:07:41

Ubuntu 20.04上CUDA 11.8全家桶(含cuDNN/TensorRT)保姆级deb安装与卸载全攻略

Ubuntu 20.04完整AI开发环境搭建与清理指南&#xff1a;CUDA 11.8全家桶深度实践 在AI开发领域&#xff0c;环境配置往往是项目开始前的第一道门槛。不同于简单的软件安装&#xff0c;CUDA、cuDNN和TensorRT这一套NVIDIA生态工具的部署涉及系统级配置、版本兼容性检查和复杂的依…

作者头像 李华
网站建设 2026/5/1 20:04:23

终极二维码修复指南:QRazyBox让你的失效二维码重获新生

终极二维码修复指南&#xff1a;QRazyBox让你的失效二维码重获新生 【免费下载链接】qrazybox QR Code Analysis and Recovery Toolkit 项目地址: https://gitcode.com/gh_mirrors/qr/qrazybox 你是否遇到过重要二维码因打印模糊、物理损坏或图像失真而无法扫描的困境&a…

作者头像 李华
网站建设 2026/5/1 20:00:58

借助模型广场与官方折扣为新项目选择高性价比模型

借助模型广场与官方折扣为新项目选择高性价比模型 1. 理解模型广场的核心功能 Taotoken 模型广场是开发者接入大模型服务的起点。该页面聚合了多家厂商的主流模型&#xff0c;以标准化格式展示各模型的基础能力、适用场景和技术参数。对于新项目团队而言&#xff0c;模型广场…

作者头像 李华
网站建设 2026/5/1 19:58:40

bilibili视频解析API:如何轻松获取B站视频原始链接?

bilibili视频解析API&#xff1a;如何轻松获取B站视频原始链接&#xff1f; 【免费下载链接】bilibili-parse bilibili Video API 项目地址: https://gitcode.com/gh_mirrors/bi/bilibili-parse 还在为无法离线观看B站优质内容而烦恼吗&#xff1f;bilibili-parse是一个…

作者头像 李华