🔥个人主页:北极的代码(欢迎来访)
🎬作者简介: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校验 + 放入队列 + 返回订单ID | Tomcat线程 |
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 插入的订单。
线程 处理顺序 查询订单数 结果 单线程消费者 先处理请求1 0 创建订单 单线程消费者 后处理请求2 1(请求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); } }结语:如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!