黑马点评秒杀优化三:阻塞队列如何把下单任务交给后台线程?
本文继续整理黑马点评 Redis 实战篇第 6 章「秒杀优化」。
上一篇讲了 Lua 如何在 Redis 中完成库存和一人一单判断。
这一篇讲第 6 章的第二个关键点:Lua 判断通过后,订单到底是谁创建的?请求线程、阻塞队列、后台线程之间到底怎么配合?
1. 这篇文章解决什么问题
学到异步秒杀时,最容易卡住的问题是:
text
Lua 通过了,只能说明 Redis 中资格判断成功。
那 MySQL 订单是谁保存的?
请求线程不是已经返回了吗?
这就是阻塞队列和后台线程要解决的问题。
先给结论:
请求线程在 Lua 判断通过后,不直接访问数据库创建订单,而是把订单信息封装成 VoucherOrder 放入 BlockingQueue;项目启动时会创建一个后台线程,这个线程一直从队列中取订单任务,取到后再执行数据库扣库存和保存订单。
2. 整体链路先看一遍
第 6 章 BlockingQueue 版本的链路可以拆成两条线。
第一条线是请求线程:
text
用户请求秒杀接口
↓
生成订单 id
↓
执行 Lua 脚本
↓
Lua 返回 0,说明资格通过
↓
封装 VoucherOrder
↓
放入 BlockingQueue
↓
返回订单 id
第二条线是后台线程:
text
项目启动后,后台线程开始运行
↓
不断从 BlockingQueue 取订单任务
↓
取到 VoucherOrder
↓
加用户锁,防止同一用户重复处理
↓
调用事务方法 createVoucherOrder
↓
数据库扣库存
↓
数据库保存订单
请求线程和后台线程关系图
#mermaid-svg-yRWjiWC33ngDBX30{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yRWjiWC33ngDBX30 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yRWjiWC33ngDBX30 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yRWjiWC33ngDBX30 .error-icon{fill:#552222;}#mermaid-svg-yRWjiWC33ngDBX30 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yRWjiWC33ngDBX30 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yRWjiWC33ngDBX30 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yRWjiWC33ngDBX30 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yRWjiWC33ngDBX30 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yRWjiWC33ngDBX30 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yRWjiWC33ngDBX30 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yRWjiWC33ngDBX30 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yRWjiWC33ngDBX30 .marker.cross{stroke:#333333;}#mermaid-svg-yRWjiWC33ngDBX30 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yRWjiWC33ngDBX30 p{margin:0;}#mermaid-svg-yRWjiWC33ngDBX30 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yRWjiWC33ngDBX30 .cluster-label text{fill:#333;}#mermaid-svg-yRWjiWC33ngDBX30 .cluster-label span{color:#333;}#mermaid-svg-yRWjiWC33ngDBX30 .cluster-label span p{background-color:transparent;}#mermaid-svg-yRWjiWC33ngDBX30 .label text,#mermaid-svg-yRWjiWC33ngDBX30 span{fill:#333;color:#333;}#mermaid-svg-yRWjiWC33ngDBX30 .node rect,#mermaid-svg-yRWjiWC33ngDBX30 .node circle,#mermaid-svg-yRWjiWC33ngDBX30 .node ellipse,#mermaid-svg-yRWjiWC33ngDBX30 .node polygon,#mermaid-svg-yRWjiWC33ngDBX30 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yRWjiWC33ngDBX30 .rough-node .label text,#mermaid-svg-yRWjiWC33ngDBX30 .node .label text,#mermaid-svg-yRWjiWC33ngDBX30 .image-shape .label,#mermaid-svg-yRWjiWC33ngDBX30 .icon-shape .label{text-anchor:middle;}#mermaid-svg-yRWjiWC33ngDBX30 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yRWjiWC33ngDBX30 .rough-node .label,#mermaid-svg-yRWjiWC33ngDBX30 .node .label,#mermaid-svg-yRWjiWC33ngDBX30 .image-shape .label,#mermaid-svg-yRWjiWC33ngDBX30 .icon-shape .label{text-align:center;}#mermaid-svg-yRWjiWC33ngDBX30 .node.clickable{cursor:pointer;}#mermaid-svg-yRWjiWC33ngDBX30 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yRWjiWC33ngDBX30 .arrowheadPath{fill:#333333;}#mermaid-svg-yRWjiWC33ngDBX30 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yRWjiWC33ngDBX30 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yRWjiWC33ngDBX30 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yRWjiWC33ngDBX30 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yRWjiWC33ngDBX30 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yRWjiWC33ngDBX30 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yRWjiWC33ngDBX30 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yRWjiWC33ngDBX30 .cluster text{fill:#333;}#mermaid-svg-yRWjiWC33ngDBX30 .cluster span{color:#333;}#mermaid-svg-yRWjiWC33ngDBX30 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yRWjiWC33ngDBX30 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yRWjiWC33ngDBX30 rect.text{fill:none;stroke-width:0;}#mermaid-svg-yRWjiWC33ngDBX30 .icon-shape,#mermaid-svg-yRWjiWC33ngDBX30 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yRWjiWC33ngDBX30 .icon-shape p,#mermaid-svg-yRWjiWC33ngDBX30 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yRWjiWC33ngDBX30 .icon-shape .label rect,#mermaid-svg-yRWjiWC33ngDBX30 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yRWjiWC33ngDBX30 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yRWjiWC33ngDBX30 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yRWjiWC33ngDBX30 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
用户请求秒杀接口
请求线程执行 Lua
Lua 返回是否为 0?
返回库存不足或重复下单
封装 VoucherOrder
放入 BlockingQueue
返回订单 id
后台线程启动后一直运行
orderTasks.take()
取到 VoucherOrder
Redisson 按 userId 加锁
调用事务方法落库
扣数据库库存并保存订单
这张图最重要的是:
text
请求线程和后台线程通过 BlockingQueue 交接订单任务。
3. 后台线程是怎么启动的
讲义中的代码:
java
// 异步处理线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
// 在类初始化之后执行,因为当这个类初始化好了之后,随时都是有可能要执行的
@PostConstruct
private void init() {
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
这里有三个新东西:
text
ExecutorService
Executors.newSingleThreadExecutor()
@PostConstruct
ExecutorService 是什么
ExecutorService 是 Java 里的线程池接口。
可以简单理解为:
text
我不直接手动 new Thread,而是把任务交给线程池执行。
这里的任务就是:
java
new VoucherOrderHandler()
它是一个实现了 Runnable 的后台任务。
newSingleThreadExecutor 是什么
java
Executors.newSingleThreadExecutor()
意思是创建一个单线程线程池。
它内部只有一个工作线程。
为什么这里只用一个线程?
因为讲义中的 BlockingQueue 版本主要是教学用的简化实现。
单线程更容易理解:
text
一个后台线程按顺序处理队列中的订单任务。
这不是最高性能方案,但非常适合先把异步下单思想讲清楚。
@PostConstruct 是什么
@PostConstruct 表示:
text
Spring 创建好这个 Bean,并完成依赖注入之后,自动执行这个方法。
也就是说,项目启动时,VoucherOrderServiceImpl 初始化完成后,会自动调用:
java
init()
然后执行:
java
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
后台线程就开始跑了。
4. VoucherOrderHandler 在干什么
讲义代码:
java
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);
}
}
}
}
这个类的作用非常朴素:
text
一直盯着队列。
队列里有订单任务,就拿出来处理。
队列里没有订单任务,就等待。
最关键的是:
java
while (true)
它表示后台任务一直运行。
它不会处理完一个订单就结束。
否则用户后续再秒杀,队列里新增任务,就没人消费了。
5. BlockingQueue 是什么
讲义中定义了一个阻塞队列:
java
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
可以把它理解成:
text
一个线程安全的订单任务容器。
请求线程负责往里面放:
java
orderTasks.add(voucherOrder);
后台线程负责从里面取:
java
VoucherOrder voucherOrder = orderTasks.take();
它叫"阻塞队列",重点在 take()。
如果队列里有数据,take() 会立即取出。
如果队列里没数据,take() 会让后台线程阻塞等待。
这里的"阻塞等待"不是后台线程疯狂空转。
它不是这样:
text
while(true) 一直问队列:有数据吗?有数据吗?有数据吗?
而是:
text
队列为空 -> 后台线程挂起等待 -> 不占用 CPU 忙等
队列有数据 -> 后台线程被唤醒 -> 继续处理
队列阻塞等待示意图
后台线程 BlockingQueue 请求线程 后台线程 BlockingQueue 请求线程 #mermaid-svg-4Yj6MSOreSlMASdS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4Yj6MSOreSlMASdS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4Yj6MSOreSlMASdS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4Yj6MSOreSlMASdS .error-icon{fill:#552222;}#mermaid-svg-4Yj6MSOreSlMASdS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4Yj6MSOreSlMASdS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4Yj6MSOreSlMASdS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4Yj6MSOreSlMASdS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4Yj6MSOreSlMASdS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4Yj6MSOreSlMASdS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4Yj6MSOreSlMASdS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4Yj6MSOreSlMASdS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4Yj6MSOreSlMASdS .marker.cross{stroke:#333333;}#mermaid-svg-4Yj6MSOreSlMASdS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4Yj6MSOreSlMASdS p{margin:0;}#mermaid-svg-4Yj6MSOreSlMASdS .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4Yj6MSOreSlMASdS text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-4Yj6MSOreSlMASdS .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4Yj6MSOreSlMASdS .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-4Yj6MSOreSlMASdS .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-4Yj6MSOreSlMASdS .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-4Yj6MSOreSlMASdS #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-4Yj6MSOreSlMASdS .sequenceNumber{fill:white;}#mermaid-svg-4Yj6MSOreSlMASdS #sequencenumber{fill:#333;}#mermaid-svg-4Yj6MSOreSlMASdS #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-4Yj6MSOreSlMASdS .messageText{fill:#333;stroke:none;}#mermaid-svg-4Yj6MSOreSlMASdS .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4Yj6MSOreSlMASdS .labelText,#mermaid-svg-4Yj6MSOreSlMASdS .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-4Yj6MSOreSlMASdS .loopText,#mermaid-svg-4Yj6MSOreSlMASdS .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-4Yj6MSOreSlMASdS .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4Yj6MSOreSlMASdS .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-4Yj6MSOreSlMASdS .noteText,#mermaid-svg-4Yj6MSOreSlMASdS .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-4Yj6MSOreSlMASdS .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4Yj6MSOreSlMASdS .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4Yj6MSOreSlMASdS .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4Yj6MSOreSlMASdS .actorPopupMenu{position:absolute;}#mermaid-svg-4Yj6MSOreSlMASdS .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-4Yj6MSOreSlMASdS .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4Yj6MSOreSlMASdS .actor-man circle,#mermaid-svg-4Yj6MSOreSlMASdS line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-4Yj6MSOreSlMASdS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} take()队列为空,线程阻塞等待add(voucherOrder)唤醒后台线程handleVoucherOrder(voucherOrder)
这个机制非常重要。
它让后台线程可以一直存在,但不会在没任务时白白烧 CPU。
6. 请求线程如何把订单任务放进队列
讲义中 seckillVoucher 方法大致是:
java
@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
long orderId = redisIdWorker.nextId("order");
// 1.执行lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString(), String.valueOf(orderId)
);
int r = result.intValue();
// 2.判断结果是否为0
if (r != 0) {
return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
}
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
// 2.6.放入阻塞队列
orderTasks.add(voucherOrder);
// 3.获取代理对象
proxy = (IVoucherOrderService) AopContext.currentProxy();
// 4.返回订单id
return Result.ok(orderId);
}
这里要抓住一句:
java
orderTasks.add(voucherOrder);
这句就是请求线程和后台线程的交接点。
请求线程不再自己调用数据库创建订单。
它只把订单任务放进队列,然后返回:
java
return Result.ok(orderId);
7. 为什么 VoucherOrder 里要提前设置 id
请求线程会创建:
java
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
这里最容易忽略的是:
java
voucherOrder.setId(orderId);
这个订单 id 应该和返回给前端的订单 id 一致。
因为请求线程返回:
java
return Result.ok(orderId);
如果后台线程落库时又重新生成一个订单 id,就会出现:
text
前端拿到订单 id = A
数据库实际保存订单 id = B
这会导致前端后续用 A 查订单时查不到。
所以正确理解是:
text
请求线程提前生成订单 id。
这个 id 既返回给前端,也随 VoucherOrder 进入队列。
后台线程最终保存的也是这个 id。
8. 后台线程处理订单时为什么还要加锁
讲义中的处理逻辑可以按下面这个版本理解。
这里补一句容易踩坑的 API 细节:Redisson 的 lock() 方法是直接阻塞式加锁,返回值是 void;如果代码需要得到 boolean isLock,应该使用 tryLock()。所以如果你看到讲义片段里写成 boolean isLock = redisLock.lock(),按"尝试获取锁并返回是否成功"的 tryLock() 思路理解即可。
java
private void handleVoucherOrder(VoucherOrder voucherOrder) {
// 1.获取用户
Long userId = voucherOrder.getUserId();
// 2.创建锁对象
RLock redisLock = redissonClient.getLock("lock:order:" + userId);
// 3.尝试获取锁
boolean isLock = redisLock.tryLock();
// 4.判断是否获得锁成功
if (!isLock) {
log.error("不允许重复下单!");
return;
}
try {
proxy.createVoucherOrder(voucherOrder);
} finally {
// 释放锁
redisLock.unlock();
}
}
这里的锁是按用户维度加的:
text
lock:order:{userId}
理论上,Lua 已经通过 Redis Set 判断了一人一单。
那为什么后台还要加锁?
可以把它理解成数据库落库阶段的兜底保护。
因为异步系统里可能出现一些复杂情况:
text
队列任务重复
后台线程重复处理
Redis 状态和数据库状态短暂不一致
代码后续演进导致重复消息
所以后台真正落库时,仍然用用户锁保护:
text
同一个用户同一时间只能进入自己的下单落库流程。
它不是第 6 章的主防线。
第 6 章的主防线是 Lua。
但后台锁可以作为兜底。
9. createVoucherOrder 真正做什么
讲义中的事务方法:
java
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
// 5.1.查询订单
int count = query()
.eq("user_id", userId)
.eq("voucher_id", voucherOrder.getVoucherId())
.count();
// 5.2.判断是否存在
if (count > 0) {
log.error("用户已经购买过了");
return;
}
// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherOrder.getVoucherId())
.gt("stock", 0)
.update();
if (!success) {
log.error("库存不足");
return;
}
save(voucherOrder);
}
这个方法才是真正落库的地方。
它做三件事:
text
1. 查数据库订单表,兜底判断一人一单。
2. 扣数据库库存,并带上 stock > 0 条件。
3. 保存订单。
注意它虽然在第 6 章是异步执行的,但它仍然是核心业务写库逻辑。
Redis 的 Lua 是前置资格判断。
MySQL 的 createVoucherOrder 是最终事实落地。
10. 为什么要通过 proxy 调用事务方法
讲义中有一句:
java
proxy.createVoucherOrder(voucherOrder);
而不是:
java
this.createVoucherOrder(voucherOrder);
原因是 Spring 的 @Transactional 是基于 AOP 代理实现的。
简单理解:
text
事务不是方法自己天然带的能力。
而是 Spring 在代理对象外面包了一层增强逻辑。
如果同一个类内部直接 this.createVoucherOrder(...),可能不会经过 Spring 代理,事务增强就可能不生效。
通过:
java
proxy = (IVoucherOrderService) AopContext.currentProxy();
拿到当前 Bean 的代理对象后,再调用:
java
proxy.createVoucherOrder(voucherOrder);
就能让调用经过 Spring 代理,从而触发 @Transactional。
这里不用把 AOP 细节背得很深。
先记住一个项目级结论:
在同一个类内部调用带
@Transactional的方法,直接this.xxx()容易导致事务不生效;需要通过 Spring 代理对象调用。
11. 线程池、队列、事务的配合关系
可以把第 6 章的 BlockingQueue 版本理解成三层配合:
text
请求线程:负责接收用户请求,执行 Lua,放入队列,快速返回。
阻塞队列:负责临时保存抢购成功但还没落库的订单任务。
后台线程:负责从队列取任务,调用事务方法完成数据库落库。
三层配合图
#mermaid-svg-LY4Lgd2SccXqGNh8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LY4Lgd2SccXqGNh8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LY4Lgd2SccXqGNh8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LY4Lgd2SccXqGNh8 .error-icon{fill:#552222;}#mermaid-svg-LY4Lgd2SccXqGNh8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LY4Lgd2SccXqGNh8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LY4Lgd2SccXqGNh8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LY4Lgd2SccXqGNh8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LY4Lgd2SccXqGNh8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LY4Lgd2SccXqGNh8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LY4Lgd2SccXqGNh8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LY4Lgd2SccXqGNh8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LY4Lgd2SccXqGNh8 .marker.cross{stroke:#333333;}#mermaid-svg-LY4Lgd2SccXqGNh8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LY4Lgd2SccXqGNh8 p{margin:0;}#mermaid-svg-LY4Lgd2SccXqGNh8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LY4Lgd2SccXqGNh8 .cluster-label text{fill:#333;}#mermaid-svg-LY4Lgd2SccXqGNh8 .cluster-label span{color:#333;}#mermaid-svg-LY4Lgd2SccXqGNh8 .cluster-label span p{background-color:transparent;}#mermaid-svg-LY4Lgd2SccXqGNh8 .label text,#mermaid-svg-LY4Lgd2SccXqGNh8 span{fill:#333;color:#333;}#mermaid-svg-LY4Lgd2SccXqGNh8 .node rect,#mermaid-svg-LY4Lgd2SccXqGNh8 .node circle,#mermaid-svg-LY4Lgd2SccXqGNh8 .node ellipse,#mermaid-svg-LY4Lgd2SccXqGNh8 .node polygon,#mermaid-svg-LY4Lgd2SccXqGNh8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LY4Lgd2SccXqGNh8 .rough-node .label text,#mermaid-svg-LY4Lgd2SccXqGNh8 .node .label text,#mermaid-svg-LY4Lgd2SccXqGNh8 .image-shape .label,#mermaid-svg-LY4Lgd2SccXqGNh8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-LY4Lgd2SccXqGNh8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LY4Lgd2SccXqGNh8 .rough-node .label,#mermaid-svg-LY4Lgd2SccXqGNh8 .node .label,#mermaid-svg-LY4Lgd2SccXqGNh8 .image-shape .label,#mermaid-svg-LY4Lgd2SccXqGNh8 .icon-shape .label{text-align:center;}#mermaid-svg-LY4Lgd2SccXqGNh8 .node.clickable{cursor:pointer;}#mermaid-svg-LY4Lgd2SccXqGNh8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LY4Lgd2SccXqGNh8 .arrowheadPath{fill:#333333;}#mermaid-svg-LY4Lgd2SccXqGNh8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LY4Lgd2SccXqGNh8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LY4Lgd2SccXqGNh8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LY4Lgd2SccXqGNh8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LY4Lgd2SccXqGNh8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LY4Lgd2SccXqGNh8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LY4Lgd2SccXqGNh8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LY4Lgd2SccXqGNh8 .cluster text{fill:#333;}#mermaid-svg-LY4Lgd2SccXqGNh8 .cluster span{color:#333;}#mermaid-svg-LY4Lgd2SccXqGNh8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LY4Lgd2SccXqGNh8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LY4Lgd2SccXqGNh8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-LY4Lgd2SccXqGNh8 .icon-shape,#mermaid-svg-LY4Lgd2SccXqGNh8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LY4Lgd2SccXqGNh8 .icon-shape p,#mermaid-svg-LY4Lgd2SccXqGNh8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LY4Lgd2SccXqGNh8 .icon-shape .label rect,#mermaid-svg-LY4Lgd2SccXqGNh8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LY4Lgd2SccXqGNh8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LY4Lgd2SccXqGNh8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LY4Lgd2SccXqGNh8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求线程
执行 Lua 资格判断
封装 VoucherOrder
BlockingQueue 暂存订单任务
后台线程 take() 取任务
Redisson 用户锁兜底
@Transactional createVoucherOrder
MySQL 扣库存 + 保存订单
这张图就是第 6 章异步下单的主链路。
12. 阻塞队列版本有什么问题
讲义最后的小总结提到:
text
基于阻塞队列的异步秒杀存在内存限制问题。
这句话非常重要。
因为 BlockingQueue 是 JVM 内存里的队列。
它有几个天然问题:
问题 1:内存有限
讲义中队列容量是:
java
new ArrayBlockingQueue<>(1024 * 1024)
虽然看起来很大,但仍然是有限的。
请求太多时,队列可能被塞满。
问题 2:服务宕机,队列数据会丢失
BlockingQueue 存在当前 JVM 内存中。
如果服务突然宕机,队列里还没处理的订单任务就没了。
这对订单系统是很严重的问题。
问题 3:多实例部署时,每个 JVM 都有自己的队列
如果部署多台服务,每台服务都有自己的本地队列。
队列之间不共享。
这会让任务管理、失败恢复、重复消费处理变得更复杂。
所以第 6 章 BlockingQueue 更像是:
text
用最容易理解的方式先讲通异步下单思想。
后续才会继续演进到 Redis Stream 这种更像消息队列的方案。
13. 本篇最容易混淆的几个点
1. 后台线程是不是请求来了才创建
不是。
后台线程在 Bean 初始化后通过 @PostConstruct 启动。
请求来了以后,只是往队列里放任务。
2. 队列为空时后台线程会不会一直占 CPU
不会。
orderTasks.take() 在队列为空时会阻塞等待。
线程会挂起,不会疯狂空转。
3. Lua 通过后为什么还要 createVoucherOrder
因为 Lua 只操作 Redis。
最终数据库订单还没有创建。
createVoucherOrder 才是真正写 MySQL 的方法。
4. 为什么要用 proxy 调事务方法
因为 @Transactional 是 Spring AOP 代理增强。
同类内部直接调用可能绕过代理,导致事务不生效。
5. BlockingQueue 是不是最终方案
不是。
它适合教学和理解异步思想,但存在内存限制、宕机丢数据、多实例队列不共享等问题。
14. 面试怎么回答
如果面试官问:阻塞队列版本的异步秒杀流程是什么?
可以这样回答:
请求进入秒杀接口后,先生成订单 id,然后执行 Lua 脚本,在 Redis 中原子判断库存和一人一单。如果 Lua 返回失败,直接返回库存不足或重复下单;如果返回成功,就封装 VoucherOrder,把订单 id、用户 id、优惠券 id 放进去,并加入 BlockingQueue,然后立即把订单 id 返回给前端。项目启动时通过单线程线程池启动一个后台任务,后台线程一直从 BlockingQueue 中 take 订单任务,取到后加用户锁并调用事务方法完成数据库扣库存和保存订单。
如果面试官问:BlockingQueue 版本有什么缺陷?
可以这样回答:
BlockingQueue 是 JVM 本地内存队列,容量有限,服务宕机时队列中未处理的订单任务会丢失,而且多实例部署时每个服务实例都有自己的本地队列,队列之间不共享。因此它适合用来理解异步秒杀思想,但生产中通常会演进到可靠消息队列或 Redis Stream 等方案。
15. 总结
第 6 章 BlockingQueue 异步下单的主线是:
text
请求线程执行 Lua
↓
Lua 判断通过
↓
请求线程封装 VoucherOrder
↓
订单任务放入 BlockingQueue
↓
请求线程返回订单 id
↓
后台线程 take 订单任务
↓
事务方法完成数据库落库
这一篇最应该记住的不是某个 API,而是这句话:
BlockingQueue 是请求线程和后台线程之间的交接点,它让"抢购资格判断"和"数据库创建订单"从同一个同步流程拆成了前台快速返回、后台异步落库两个阶段。
下一篇继续讲一个很容易产生误解的问题:
text
既然 Lua 已经判断过库存和一人一单了,为什么数据库落库阶段还要保留一人一单检查、stock > 0 条件更新、用户锁和事务?