📚 实战篇 22. 秒杀优化 - 异步秒杀思路学习文档
一、 核心痛点:同步下单的性能瓶颈
回顾我们之前的秒杀下单流程,它是一个典型的**“同步(Synchronous)”**执行过程。
当用户点击“抢购”时,Tomcat 的主线程会一直被占用,按顺序去 MySQL 中执行以下操作:
- 查询优惠券
- 判断秒杀库存是否充足
- 查询订单表,校验“一人一单”
- 扣减库存
- 创建订单
- 致命缺陷: 所有的业务逻辑都在同一个线程中执行,且包含了大量的数据库 I/O 读写操作。数据库的并发能力是非常有限的。高并发下,大量请求会阻塞在数据库这里,导致接口响应时间(RT)急剧飙升,整个系统的吞吐量极低。
二、 破局核心思路:“分流”与“异步”
如何让接口瞬间变快?核心思想就是:把耗时的操作扔给后台慢慢做,前台只做最轻量级的校验。 我们可以将整个秒杀流程“一刀切断”,拆分成两个截然不同的阶段:
阶段 1:前台极速拦截与校验(使用 Redis)
Tomcat 线程收到请求后,绝对不去碰 MySQL。
而是直接去内存级别的 Redis 中,校验当前用户有没有资格买、当前还有没有库存。
- 如果没库存或者已经买过了:直接光速返回错误提示。
- 如果有库存且没买过:在 Redis 中扣减库存、记录用户 ID,然后光速给用户返回“抢单成功”和“订单号”。此时,用户的页面已经可以提示成功了!
阶段 2:后台异步落库(使用消息队列/线程池)
在前台返回成功的同时,把这个订单的具体信息(用户 ID、优惠券 ID、订单 ID)封装成一个消息,扔到一个**队列(Queue)**里。
后台开启一个独立的线程,像个没有感情的搬砖机器一样,不断从队列里拿出订单消息,再去慢慢地执行数据库操作(扣减 MySQL 库存、写入订单表)。
三、 核心技术数据结构设计
为了实现上述的异步改造,我们需要将 MySQL 中的部分数据“前置”到 Redis 中进行预热:
-
缓存库存: 使用 Redis 的
String类型。- Key:
seckill:stock:voucherId - Value:
100(剩余库存数)
- Key:
-
记录一人一单: 使用 Redis 的
Set集合(天然去重)。- Key:
seckill:users:voucherId - Value: 成功抢到券的用户 ID 集合。
- Key:
-
保证原子性: 因为我们要同时判断库存、判断 Set、扣库存、写 Set,这是多步操作,必须使用 Lua 脚本在 Redis 中一口气执行完毕,防止超卖。
四、 优化后的业务全流程推演
-
系统初始化(缓存预热): 活动开始前,提前把优惠券的库存数量存入 Redis。
-
用户发起秒杀: 携带
voucherId请求到达后端。 -
执行 Lua 脚本(极速判断): * 查库存:如果
stock <= 0,返回异常代码 1(库存不足)。- 查 Set:如果
sismember发现用户已经在集合里了,返回异常代码 2(不能重复下单)。 - 扣库存并记录:如果都通过,执行
decr stock,并sadd users userId,返回 0(有资格抢单)。
- 查 Set:如果
-
生成订单号并放入队列: Tomcat 发现 Lua 返回了 0,立刻调用我们之前写的全局唯一 ID 生成器生成一个订单号,并将订单对象丢入**阻塞队列(BlockingQueue)**中。
-
返回用户结果: 将生成的订单号直接返回给前端,主线程结束。整个过程不到 10 毫秒!
-
异步搬砖: 后台专门配置的单个或多个线程,不断从阻塞队列中
take()订单,再去老老实实地调用 MyBatis 写入数据库。
学习总结
“异步秒杀”是利用 Redis 的极高读写性能代替 MySQL 抗下瞬间的高并发洪峰。用户感觉自己瞬间抢到了,实际上订单还在后排队慢慢写入数据库。这种**“削峰填谷”**的思想,是所有大型互联网高并发架构的基石。