redis_点评(13.优惠劵秒杀-基于阻塞队列实现秒杀异步下单(Redis 快速校验资格 + 异步队列慢慢写数据库)
这次改造的核心目标是:把秒杀的数据库操作异步化,用阻塞队列 + 线程池实现「请求快速响应、后台异步处理订单」,彻底解决秒杀场景下的数据库压力瓶颈。
一、新增核心组件:阻塞队列与线程池
// 阻塞队列:存放秒杀订单任务
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
// 异步处理线程池
private static ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
关键说明:
-
ArrayBlockingQueue-
是有界队列,固定容量 1024*1024,可承载百万级秒杀订单任务,防止无限堆积导致内存溢出;
-
具备阻塞特性:队列满时生产者入队会阻塞,队列空时消费者取任务会阻塞;
-
天然适配生产者 - 消费者模型:秒杀接口作为生产者往队列放订单,后台线程作为消费者从队列取订单处理;
-
线程安全,无需手动加锁即可在多线程环境下安全入队、出队。
-
-
newSingleThreadExecutor-
内部只有一个工作线程,串行依次处理每一个秒杀订单;
-
保证订单按到达顺序依次处理,避免多线程并发操作同一张订单表、同一条商品库存记录,引发数据错乱、超卖、重复下单等问题;
-
线程池全局静态唯一,项目生命周期内只创建一次,复用线程,减少线程创建销毁开销。
-
二、类初始化时启动消费者线程
@PostConstruct
private void init() {
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
详解
-
@PostConstruct:Spring 生命周期注解,当前 Bean 被 Spring 容器初始化完成后立刻自动执行,不用手动调用; -
项目一启动就自动开启消费者监听线程,提前待命,不用等用户发起秒杀才创建线程;
-
将自定义任务
VoucherOrderHandler提交给单线程池,开始无限循环监听阻塞队列,等待秒杀订单任务进入。
三、消费者线程实现:VoucherOrderHandler
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
// 1. 从阻塞队列获取订单信息(队列空时自动阻塞)
VoucherOrder voucherOrder = orderTasks.take();
// 2. 处理订单
handleVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}
解析
-
while(true):开启死循环常驻监听,只要项目不停止,就一直监听队列任务; -
orderTasks.take():阻塞式获取元素,队列里没有订单任务时,线程会进入阻塞休眠状态,不占用 CPU 资源;一旦有订单入队立刻被唤醒处理; -
try-catch 捕获异常:单个订单处理报错不影响整体循环,避免一个订单异常导致整个秒杀消费线程挂掉;
-
所有数据库下单、扣库存逻辑都放到异步线程中执行,和前端请求主线程完全解耦。
四、订单处理方法:handleVoucherOrder
private void handleVoucherOrder(VoucherOrder voucherOrder) {
// 1. 获取用户ID(从订单对象中取,避免ThreadLocal失效)
Long userId = voucherOrder.getUserId();
// 2. 创建Redisson锁
RLock lock = redissonClient.getLock("lock:order:" + userId);
// 3. 尝试获取锁
boolean isLock = lock.tryLock();
if (!isLock) {
log.error("不允许重复下单");
return;
}
try {
// 4. 获取代理对象(解决事务失效问题)
proxy.createVoucherOrder(voucherOrder);
} finally {
// 5. 释放锁
lock.unlock();
}
}
关键变更:
1、用户 ID 不从 UserHolder 获取
异步线程不属于前端请求的主线程,ThreadLocal 中的用户信息会失效、拿不到数据,所以只能从已经封装好的 voucherOrder 订单对象中获取用户 ID。
2、引入 Redisson 分布式锁
即使 Redis Lua 脚本已经做了一人一单前置校验,这里仍加分布式锁做业务兜底,防止网络重试、重复请求导致重复下单;保证同一个用户同一时间只能处理一个秒杀订单。
3、必须使用代理对象调用事务方法
异步线程中如果直接 this.createVoucherOrder(),会绕过 Spring AOP 代理,导致 @Transactional 事务注解失效;通过 AopContext.currentProxy() 获取当前类代理对象调用方法,才能让事务生效,保证扣库存、创订单的原子性。
4、finally 释放锁
保证无论业务正常执行还是抛出异常,分布式锁都一定会释放,避免死锁。
五、秒杀入口方法改造:快速响应 + 入队
@Override
public Result seckillVoucher(Long voucherId) {
// 获取用户ID
Long userId = UserHolder.getUser().getId();
// 1. 执行Lua脚本(库存、一人一单校验+扣减)
Long result = stringRedisTemplate.execute(...);
int r = result.intValue();
if (r != 0) {
return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
}
// 2. 秒杀成功,构建订单对象
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
// 3. 放入阻塞队列(生产者)
orderTasks.add(voucherOrder);
// 4. 返回订单ID,直接响应前端
return Result.ok(orderId);
}
关键变更:
-
所有复杂校验下沉到 Redis + Lua
原子完成:库存判断、一人一单判断、Redis 库存扣减,杜绝超卖和重复下单,性能远高于 Java 层多次判断 DB。
-
请求主线程不再操作数据库
只做 Lua 脚本调用、封装订单、入队操作,全程都是内存和 Redis 操作,接口响应速度极快。
-
异步解耦
订单真正入库、扣数据库库存全部交给后台消费线程慢慢处理,前端不用等待数据库 IO,体验无感。
-
全局唯一订单 ID
通过 Redis 全局 ID 生成器生成 orderId,提前返回给前端,前端可凭订单 ID 查询后续订单状态。
六、创建订单方法改造:异步处理 + 参数变更
@Override
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
// 1. 从订单对象获取用户ID和优惠券ID
Long userId = voucherOrder.getUserId();
Long voucherId = voucherOrder.getVoucherId();
// 2. 一人一单校验(兜底)
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
log.error("用户已经购买过一次!");
return;
}
// 3. 扣减库存(乐观锁)
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).gt("stock", 0)
.update();
if (!success) {
log.error("库存不足");
return;
}
// 4. 保存订单
save(voucherOrder);
}
改造说明
-
方法参数由原来的优惠券 ID,改为完整订单对象,适配异步线程无 ThreadLocal 的场景;
-
保留数据库层一人一单兜底校验,形成 Redis 前置拦截 + DB 最终兜底的双层防护;
-
采用乐观锁扣库存,
gt("stock",0)保证库存大于 0 才扣减,防止超卖; -
添加
@Transactional事务注解,保证扣库存、新增订单要么同时成功,要么同时回滚,数据一致不错乱。
总结
这次改造的核心目的:通过阻塞队列 + 线程池实现秒杀请求的异步处理,把数据库操作从请求主线程剥离,实现「前端快速响应、后台异步处理订单」,大幅提升系统并发能力,同时解决秒杀场景下的数据库压力瓶颈。
💡 补充:这就是典型的生产者 - 消费者模式,Lua 脚本是「快速校验 + 生产者」,阻塞队列和线程池是「消费者」,实现了高并发场景下的流量削峰。
更多推荐

所有评论(0)