📚 实战篇 24. 秒杀优化 - 基于 BlockingQueue 的异步落库学习文档
一、 核心思想:生产者与消费者模型
在秒杀场景下,我们要让 Tomcat 的主线程(处理用户 HTTP 请求的线程)尽早“下班”返回结果。所以,主线程只负责判断资格(生产订单),而脏活累活(写 MySQL)则交给后台专门的线程去慢慢干(消费订单)。
这就好比餐厅点餐:
- 前台服务员(Tomcat 主线程): 只负责确认你有没有资格点这道特价菜(查 Redis),确认有资格后,马上给你一个取餐号(返回订单号),然后立刻去接待下一位顾客。
- 后厨订单筐(
BlockingQueue): 服务员把你的点菜单扔进这个筐里。 - 后厨大厨(后台单线程
ExecutorService): 大厨不关心外面有多少客人,他只死死盯着订单筐,里面有一张单子,他就拿出来炒一道菜(写 MySQL 数据库)。
二、 核心代码深度拆解
结合你上传的代码截图,我们来逐块分析这些底层组件的作用:
1. 存储容器:阻塞队列 ArrayBlockingQueue
Java
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
- 为什么用
BlockingQueue? 因为它是线程安全的!当队列空了的时候,如果后台线程去拿任务,会被阻塞(休眠) ,不会空耗 CPU;当队列满了的时候,如果前台线程想塞任务进去,也会被阻塞。 - 为什么容量是
1024 * 1024(100万)? 这是一个有界队列,防止极端并发下无限制地创建订单对象,最终导致 JVM 内存溢出(OOM)。
2. 异步工人:单线程线程池 ExecutorService
Java
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
- 为什么用单线程池? 秒杀写库的核心瓶颈在 MySQL 的 I/O 速度,而不是后台处理得不够快。为了保证订单写入数据库时的绝对串行和安全(避免复杂的并发冲突),开一个线程在后台按顺序慢慢写是最稳妥的。
3. 引擎点火:@PostConstruct 启动器
Java
@PostConstruct
private void init() {
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
- 作用:
@PostConstruct是 Spring 的生命周期注解。它表示在当前这个VoucherOrderServiceImpl类被 Spring 实例化并完成依赖注入后,立刻且只执行一次这个init方法。 - 效果: 项目刚一启动,后台的“大厨”线程就被唤醒了,随时准备接单。
4. 消费者逻辑:无情的搬砖机器 VoucherOrderHandler
Java
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) { // 死循环,永远不下班
try {
// 1. 获取队列中的订单信息 (take() 方法是阻塞的,没有订单就会在这里死等,不耗费 CPU)
VoucherOrder voucherOrder = orderTasks.take();
// 2. 创建订单 (真正的写库逻辑)
handleVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}
take()方法的魅力: 它是整个循环的灵魂。平时没有秒杀活动时,这个while(true)循环不会疯狂空转,而是安静地停在take()这一行睡觉,极大地节约了服务器资源。
三、 架构反思(面试高频考点)
虽然基于 JDK 内存的 BlockingQueue 完美实现了异步解耦,让秒杀接口的响应时间从几百毫秒骤降到了几毫秒,但在真实的工业级生产环境中,这种方案存在两大致命隐患:
- 内存限制风险(OOM): 如果秒杀太火爆,100 万的队列被塞满了怎么办?再有订单进来要么报错,要么前台线程被堵死。
- 数据丢失风险(最致命): 存放在
ArrayBlockingQueue里的订单都在 JVM 内存里。如果这 100 万个订单还没来得及写进 MySQL,此时服务器突然停电或 Tomcat 重启了,这 100 万个订单就会瞬间人间蒸发!用户付了钱却没订单,这是非常严重的生产事故。
为了解决内存限制和数据丢失的问题,业界标准的做法是抛弃 JVM 本地队列,引入专业的独立消息队列组件(MQ) 。