黑马点评-秒杀优化-03_blocking_queue_async_order

黑马点评秒杀优化三:阻塞队列如何把下单任务交给后台线程?

本文继续整理黑马点评 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 条件更新、用户锁和事务?
相关推荐
Python私教1 小时前
免费用上 GPT-4 级模型:国产大模型 API 接入教程(2026 最新版)
数据库
真实的菜1 小时前
Redis 从入门到精通(六):集群模式(Cluster)—— 分布式架构、哈希槽与 Gossip 协议全解
redis·分布式·架构
星空椰1 小时前
Tauri 开发模式下 SQLite 数据库文件变更导致应用自动重启问题
数据库·sqlite·tauri
我是一颗柠檬1 小时前
【Redis】Redis分布式锁Day13(2026年)
java·redis·分布式·缓存
kingwebo'sZone3 小时前
WPF 在(WrapPanel父级使用可以自动换行)每个 TextBlock 显示一行数据(竖排,垂直)
wpf
不会就选b10 小时前
MySQL之视图
数据库·mysql
>no problem<10 小时前
基于cola5.0的基础设施层的多数据库切换方案思路
数据库·spring boot·mybatisplus·cola5.0·数据库迁移适配
OceanBase数据库官方博客10 小时前
OceanBase 赋能央国企:从发电到用电的全链路业务承载
数据库·oceanbase
瀚高PG实验室11 小时前
pgsql-ogr-fdw
数据库·postgresql·瀚高数据库·highgo