黑马点评秒杀优化一:为什么秒杀下单不能一直同步查库?
本文整理黑马点评 Redis 实战篇第 6 章「秒杀优化」。
这一篇只聚焦第 6 章的起点:为什么已经有了乐观锁、一人一单、分布式锁之后,还要继续做"Redis 预检 + 异步下单"?
注意:本文按讲义中的 BlockingQueue 异步秒杀版本来讲,不展开后续 Redis Stream 消息队列版本。
1. 这篇文章解决什么问题
学秒杀优化时,最容易出现一个疑惑:
text
前面不是已经解决超卖和一人一单了吗?
为什么第 6 章还要把秒杀逻辑搬到 Redis 里?
为什么还要搞异步下单?
这个疑惑很正常。
因为前几章解决的是:
text
高并发下,业务结果不能错。
第 6 章开始解决的是:
text
高并发下,请求不能都卡在数据库上慢慢排队。
也就是说,前面更偏"正确性",第 6 章更偏"性能和吞吐量"。
先给结论:
秒杀优化的核心不是让数据库扣库存变得神奇地更快,而是把请求线程里最耗时、最容易排队的数据库操作后移;请求线程只在 Redis 中快速判断用户有没有抢购资格,通过后立即返回订单 id,真正落库交给后台线程慢慢处理。
2. 原始同步秒杀流程有什么问题
在没有第 6 章优化之前,一个用户发起秒杀请求,大致要经历这些步骤:
text
1. 查询秒杀券
2. 判断秒杀是否开始、是否结束
3. 判断库存是否充足
4. 查询订单,判断一人一单
5. 扣减数据库库存
6. 创建数据库订单
7. 返回订单 id
这套流程最大的问题不是"逻辑复杂",而是:
text
请求线程要一直等数据库操作全部完成。
比如有 1 万个用户同时抢券,请求会大量涌入 Tomcat。
每个请求都要去数据库:
text
查库存
查订单
扣库存
写订单
数据库连接数是有限的,SQL 执行也是需要时间的。
于是大量请求会堵在数据库这一层。
同步流程示意图
#mermaid-svg-nxw2ItcU0HJqFFNy{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-nxw2ItcU0HJqFFNy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nxw2ItcU0HJqFFNy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nxw2ItcU0HJqFFNy .error-icon{fill:#552222;}#mermaid-svg-nxw2ItcU0HJqFFNy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nxw2ItcU0HJqFFNy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nxw2ItcU0HJqFFNy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nxw2ItcU0HJqFFNy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nxw2ItcU0HJqFFNy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nxw2ItcU0HJqFFNy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nxw2ItcU0HJqFFNy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nxw2ItcU0HJqFFNy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nxw2ItcU0HJqFFNy .marker.cross{stroke:#333333;}#mermaid-svg-nxw2ItcU0HJqFFNy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nxw2ItcU0HJqFFNy p{margin:0;}#mermaid-svg-nxw2ItcU0HJqFFNy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nxw2ItcU0HJqFFNy .cluster-label text{fill:#333;}#mermaid-svg-nxw2ItcU0HJqFFNy .cluster-label span{color:#333;}#mermaid-svg-nxw2ItcU0HJqFFNy .cluster-label span p{background-color:transparent;}#mermaid-svg-nxw2ItcU0HJqFFNy .label text,#mermaid-svg-nxw2ItcU0HJqFFNy span{fill:#333;color:#333;}#mermaid-svg-nxw2ItcU0HJqFFNy .node rect,#mermaid-svg-nxw2ItcU0HJqFFNy .node circle,#mermaid-svg-nxw2ItcU0HJqFFNy .node ellipse,#mermaid-svg-nxw2ItcU0HJqFFNy .node polygon,#mermaid-svg-nxw2ItcU0HJqFFNy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nxw2ItcU0HJqFFNy .rough-node .label text,#mermaid-svg-nxw2ItcU0HJqFFNy .node .label text,#mermaid-svg-nxw2ItcU0HJqFFNy .image-shape .label,#mermaid-svg-nxw2ItcU0HJqFFNy .icon-shape .label{text-anchor:middle;}#mermaid-svg-nxw2ItcU0HJqFFNy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nxw2ItcU0HJqFFNy .rough-node .label,#mermaid-svg-nxw2ItcU0HJqFFNy .node .label,#mermaid-svg-nxw2ItcU0HJqFFNy .image-shape .label,#mermaid-svg-nxw2ItcU0HJqFFNy .icon-shape .label{text-align:center;}#mermaid-svg-nxw2ItcU0HJqFFNy .node.clickable{cursor:pointer;}#mermaid-svg-nxw2ItcU0HJqFFNy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nxw2ItcU0HJqFFNy .arrowheadPath{fill:#333333;}#mermaid-svg-nxw2ItcU0HJqFFNy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nxw2ItcU0HJqFFNy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nxw2ItcU0HJqFFNy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nxw2ItcU0HJqFFNy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nxw2ItcU0HJqFFNy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nxw2ItcU0HJqFFNy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nxw2ItcU0HJqFFNy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nxw2ItcU0HJqFFNy .cluster text{fill:#333;}#mermaid-svg-nxw2ItcU0HJqFFNy .cluster span{color:#333;}#mermaid-svg-nxw2ItcU0HJqFFNy 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-nxw2ItcU0HJqFFNy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nxw2ItcU0HJqFFNy rect.text{fill:none;stroke-width:0;}#mermaid-svg-nxw2ItcU0HJqFFNy .icon-shape,#mermaid-svg-nxw2ItcU0HJqFFNy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nxw2ItcU0HJqFFNy .icon-shape p,#mermaid-svg-nxw2ItcU0HJqFFNy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nxw2ItcU0HJqFFNy .icon-shape .label rect,#mermaid-svg-nxw2ItcU0HJqFFNy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nxw2ItcU0HJqFFNy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nxw2ItcU0HJqFFNy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nxw2ItcU0HJqFFNy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户请求秒杀接口
Tomcat 请求线程
查询数据库秒杀券
判断库存和时间
查询订单表判断一人一单
更新数据库库存
插入订单表
返回订单 id
这张图的关键点是:
text
返回结果之前,请求线程一直没有释放。
如果数据库慢,请求线程也慢。
如果请求线程被占满,后面的请求就排队。
3. 为什么不能简单地多开几个线程并发做这些事
刚学到"异步"时,很容易想到一个方案:
text
那我能不能在一个请求里开多个线程?
一个线程查库存,一个线程查订单,一个线程扣库存,一个线程写订单。
这个想法看起来是在提速,但它不适合秒杀主链路。
原因有三个。
原因 1:这些步骤本来就有顺序依赖
比如:
text
库存不足,就不应该创建订单。
用户重复下单,也不应该扣库存。
所以这些步骤不是完全独立的。
不能随便拆成多个线程并行跑。
原因 2:请求量大时,线程本身也会成为资源压力
秒杀场景的特点是:
text
瞬间请求很多。
如果每个请求再额外创建多个任务,线程池很容易被打满。
本来数据库已经很忙了,现在应用线程也更忙。
这不叫优化,这叫给系统再添一把火。
原因 3:用户不一定需要马上看到"数据库订单已创建"
秒杀业务里,用户点击抢券后,最关心的是:
text
我有没有抢到资格?
不一定要求当前 HTTP 请求返回时,MySQL 订单已经插入完成。
所以我们可以换个思路:
text
先快速判断用户是否具备抢购资格。
如果具备,先返回订单 id。
真正创建订单可以稍后由后台线程完成。
这就是第 6 章异步秒杀的入口。
4. 第 6 章到底优化了哪里
讲义给出的优化思路是:
text
把耗时短、判断性强、适合高并发读写的逻辑放到 Redis。
把耗时长、最终落库的逻辑放到后台线程。
请求线程只做两件核心事情:
text
1. 在 Redis 中判断库存是否足够、一人一单是否通过。
2. 判断通过后,把订单任务放入阻塞队列,然后立即返回订单 id。
后台线程再做:
text
1. 从阻塞队列取出订单任务。
2. 执行真正的数据库扣库存和保存订单。
优化后流程示意图
#mermaid-svg-yFyEXw0qiACSlHTQ{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-yFyEXw0qiACSlHTQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yFyEXw0qiACSlHTQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yFyEXw0qiACSlHTQ .error-icon{fill:#552222;}#mermaid-svg-yFyEXw0qiACSlHTQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yFyEXw0qiACSlHTQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yFyEXw0qiACSlHTQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yFyEXw0qiACSlHTQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yFyEXw0qiACSlHTQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yFyEXw0qiACSlHTQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yFyEXw0qiACSlHTQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yFyEXw0qiACSlHTQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yFyEXw0qiACSlHTQ .marker.cross{stroke:#333333;}#mermaid-svg-yFyEXw0qiACSlHTQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yFyEXw0qiACSlHTQ p{margin:0;}#mermaid-svg-yFyEXw0qiACSlHTQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yFyEXw0qiACSlHTQ .cluster-label text{fill:#333;}#mermaid-svg-yFyEXw0qiACSlHTQ .cluster-label span{color:#333;}#mermaid-svg-yFyEXw0qiACSlHTQ .cluster-label span p{background-color:transparent;}#mermaid-svg-yFyEXw0qiACSlHTQ .label text,#mermaid-svg-yFyEXw0qiACSlHTQ span{fill:#333;color:#333;}#mermaid-svg-yFyEXw0qiACSlHTQ .node rect,#mermaid-svg-yFyEXw0qiACSlHTQ .node circle,#mermaid-svg-yFyEXw0qiACSlHTQ .node ellipse,#mermaid-svg-yFyEXw0qiACSlHTQ .node polygon,#mermaid-svg-yFyEXw0qiACSlHTQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yFyEXw0qiACSlHTQ .rough-node .label text,#mermaid-svg-yFyEXw0qiACSlHTQ .node .label text,#mermaid-svg-yFyEXw0qiACSlHTQ .image-shape .label,#mermaid-svg-yFyEXw0qiACSlHTQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-yFyEXw0qiACSlHTQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yFyEXw0qiACSlHTQ .rough-node .label,#mermaid-svg-yFyEXw0qiACSlHTQ .node .label,#mermaid-svg-yFyEXw0qiACSlHTQ .image-shape .label,#mermaid-svg-yFyEXw0qiACSlHTQ .icon-shape .label{text-align:center;}#mermaid-svg-yFyEXw0qiACSlHTQ .node.clickable{cursor:pointer;}#mermaid-svg-yFyEXw0qiACSlHTQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yFyEXw0qiACSlHTQ .arrowheadPath{fill:#333333;}#mermaid-svg-yFyEXw0qiACSlHTQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yFyEXw0qiACSlHTQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yFyEXw0qiACSlHTQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yFyEXw0qiACSlHTQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yFyEXw0qiACSlHTQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yFyEXw0qiACSlHTQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yFyEXw0qiACSlHTQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yFyEXw0qiACSlHTQ .cluster text{fill:#333;}#mermaid-svg-yFyEXw0qiACSlHTQ .cluster span{color:#333;}#mermaid-svg-yFyEXw0qiACSlHTQ 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-yFyEXw0qiACSlHTQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yFyEXw0qiACSlHTQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-yFyEXw0qiACSlHTQ .icon-shape,#mermaid-svg-yFyEXw0qiACSlHTQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yFyEXw0qiACSlHTQ .icon-shape p,#mermaid-svg-yFyEXw0qiACSlHTQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yFyEXw0qiACSlHTQ .icon-shape .label rect,#mermaid-svg-yFyEXw0qiACSlHTQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yFyEXw0qiACSlHTQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yFyEXw0qiACSlHTQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yFyEXw0qiACSlHTQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不通过
通过
用户请求秒杀接口
Tomcat 请求线程
执行 Lua 脚本
Redis 判断资格是否通过?
直接返回库存不足或重复下单
生成订单任务
订单任务放入阻塞队列
立即返回订单 id
后台线程
从阻塞队列取订单任务
数据库扣库存
数据库保存订单
这张图里最关键的变化是:
text
请求线程不再等数据库落库完成。
请求线程只要确认 Redis 资格判断成功,就能返回。
5. 为什么库存和一人一单适合放 Redis 判断
秒杀资格判断主要看两个条件:
text
1. 库存是否大于 0。
2. 当前用户是否已经抢过这张券。
这两个条件都很适合放 Redis。
库存可以用 String 保存:
text
seckill:stock:{voucherId} -> 剩余库存
某张券已经下单的用户可以用 Set 保存:
text
seckill:order:{voucherId} -> 已经抢过该券的 userId 集合
比如秒杀券 id 是 10,Redis 中可以这样存:
text
seckill:stock:10 = 100
seckill:order:10 = {101, 205, 309}
用户 888 来抢券时,只需要判断:
text
seckill:stock:10 是否大于 0
seckill:order:10 中是否已经包含 888
如果库存大于 0,并且 Set 中没有该用户,就说明初步有资格。
6. 为什么一定要保证 Redis 里的判断是原子的
虽然 Redis 很快,但仍然要注意一个并发问题:
text
判断库存、判断一人一单、扣 Redis 库存、记录用户已下单
这几步必须作为一个整体执行。
假设不保证原子性,可能出现这种情况:
text
线程 A 判断库存 > 0
线程 B 判断库存 > 0
线程 A 扣库存
线程 B 也扣库存
或者:
text
同一个用户两个请求都判断"Set 中还没有我"
然后两个请求都通过资格判断
所以第 6 章使用 Lua 脚本。
Lua 脚本在 Redis 中执行时,可以把多条 Redis 命令当成一个整体。
也就是:
text
判断库存
判断是否重复下单
扣 Redis 库存
记录用户已下单
这些步骤中间不会被其他请求插队。
7. "抢购成功"不等于"订单已经写入数据库"
这是第 6 章最容易混淆的地方。
Lua 返回 0 时,表示:
text
Redis 资格判断通过。
Redis 库存已经扣减。
Redis 已经记录该用户抢过这张券。
订单任务已经准备交给后台线程处理。
但它不表示:
text
MySQL 订单已经创建成功。
因为数据库落库是在后台线程中异步执行的。
所以第 6 章这个方案本质上是:
text
前台先确认资格,后台最终落库。
用户拿到订单 id 后,后续可以通过订单 id 查询订单状态。
这也是讲义里说"前端可以通过返回的订单 id 判断是否下单成功"的原因。
8. 方案演进总览
可以把第 6 章看成一次架构演进。
原来是同步下单:
text
请求线程 -> 数据库完整下单 -> 返回结果
优化后是异步下单:
text
请求线程 -> Redis 快速资格判断 -> 队列 -> 后台线程落库
演进图
#mermaid-svg-cES9DtIqcqnt8g29{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-cES9DtIqcqnt8g29 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cES9DtIqcqnt8g29 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cES9DtIqcqnt8g29 .error-icon{fill:#552222;}#mermaid-svg-cES9DtIqcqnt8g29 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cES9DtIqcqnt8g29 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cES9DtIqcqnt8g29 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cES9DtIqcqnt8g29 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cES9DtIqcqnt8g29 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cES9DtIqcqnt8g29 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cES9DtIqcqnt8g29 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cES9DtIqcqnt8g29 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cES9DtIqcqnt8g29 .marker.cross{stroke:#333333;}#mermaid-svg-cES9DtIqcqnt8g29 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cES9DtIqcqnt8g29 p{margin:0;}#mermaid-svg-cES9DtIqcqnt8g29 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cES9DtIqcqnt8g29 .cluster-label text{fill:#333;}#mermaid-svg-cES9DtIqcqnt8g29 .cluster-label span{color:#333;}#mermaid-svg-cES9DtIqcqnt8g29 .cluster-label span p{background-color:transparent;}#mermaid-svg-cES9DtIqcqnt8g29 .label text,#mermaid-svg-cES9DtIqcqnt8g29 span{fill:#333;color:#333;}#mermaid-svg-cES9DtIqcqnt8g29 .node rect,#mermaid-svg-cES9DtIqcqnt8g29 .node circle,#mermaid-svg-cES9DtIqcqnt8g29 .node ellipse,#mermaid-svg-cES9DtIqcqnt8g29 .node polygon,#mermaid-svg-cES9DtIqcqnt8g29 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cES9DtIqcqnt8g29 .rough-node .label text,#mermaid-svg-cES9DtIqcqnt8g29 .node .label text,#mermaid-svg-cES9DtIqcqnt8g29 .image-shape .label,#mermaid-svg-cES9DtIqcqnt8g29 .icon-shape .label{text-anchor:middle;}#mermaid-svg-cES9DtIqcqnt8g29 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cES9DtIqcqnt8g29 .rough-node .label,#mermaid-svg-cES9DtIqcqnt8g29 .node .label,#mermaid-svg-cES9DtIqcqnt8g29 .image-shape .label,#mermaid-svg-cES9DtIqcqnt8g29 .icon-shape .label{text-align:center;}#mermaid-svg-cES9DtIqcqnt8g29 .node.clickable{cursor:pointer;}#mermaid-svg-cES9DtIqcqnt8g29 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cES9DtIqcqnt8g29 .arrowheadPath{fill:#333333;}#mermaid-svg-cES9DtIqcqnt8g29 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cES9DtIqcqnt8g29 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cES9DtIqcqnt8g29 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cES9DtIqcqnt8g29 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cES9DtIqcqnt8g29 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cES9DtIqcqnt8g29 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cES9DtIqcqnt8g29 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cES9DtIqcqnt8g29 .cluster text{fill:#333;}#mermaid-svg-cES9DtIqcqnt8g29 .cluster span{color:#333;}#mermaid-svg-cES9DtIqcqnt8g29 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-cES9DtIqcqnt8g29 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cES9DtIqcqnt8g29 rect.text{fill:none;stroke-width:0;}#mermaid-svg-cES9DtIqcqnt8g29 .icon-shape,#mermaid-svg-cES9DtIqcqnt8g29 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cES9DtIqcqnt8g29 .icon-shape p,#mermaid-svg-cES9DtIqcqnt8g29 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cES9DtIqcqnt8g29 .icon-shape .label rect,#mermaid-svg-cES9DtIqcqnt8g29 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cES9DtIqcqnt8g29 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cES9DtIqcqnt8g29 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cES9DtIqcqnt8g29 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 同步秒杀
所有判断和落库都在请求线程完成
数据库压力大,请求线程等待久
Redis 预检
请求线程只判断资格
阻塞队列异步下单
后台线程慢慢落库
这里的"慢慢"不是说可以无限慢。
而是说:
text
落库动作不再阻塞用户当前请求。
9. 本篇最容易混淆的几个点
1. 异步秒杀是不是就是多开线程并发查库
不是。
第 6 章不是让一个请求里的多个步骤并行执行。
它是把请求线程和落库线程拆开:
text
请求线程负责快速判断资格。
后台线程负责最终创建订单。
2. Redis 预检通过是不是订单已经创建了
不是。
Redis 预检通过只表示用户拿到了抢购资格。
MySQL 订单是否创建,要看后台线程是否成功处理队列里的订单任务。
3. 第 6 章是不是推翻了前面的锁和乐观锁
不是。
前面的锁、乐观锁解决的是同步数据库阶段的并发正确性。
第 6 章是在此基础上,把高并发资格判断前移到 Redis,提高吞吐量。
4. 为什么还需要后台线程
因为 Redis 只适合做快速判断和状态记录,最终订单仍然要进入 MySQL。
后台线程就是负责把"抢购成功的任务"真正落到数据库。
10. 面试怎么回答
如果面试官问:秒杀优化的整体思路是什么?
可以这样回答:
原始秒杀流程中,请求线程需要串行完成查库存、查订单、扣库存、创建订单等数据库操作,高并发下数据库压力大,请求耗时也长。优化后把库存判断和一人一单判断前移到 Redis 中,用 Lua 脚本保证判断和扣 Redis 库存的原子性。Redis 判断通过后,请求线程生成订单任务放入阻塞队列,并立即返回订单 id;后台线程再异步消费队列,完成数据库扣库存和保存订单。这样可以减少请求线程等待数据库的时间,提高秒杀接口吞吐量。
如果面试官问:异步秒杀为什么不直接让请求线程落库?
可以这样回答:
秒杀请求的并发量非常高,如果每个请求都直接访问数据库完成完整下单流程,数据库和 Tomcat 请求线程都会承受很大压力。异步秒杀先用 Redis 快速判断资格,把真正的数据库落库交给后台线程处理,请求线程不再等待数据库写入完成,从而提升接口响应速度。
11. 总结
第 6 章秒杀优化的主线可以用一句话概括:
把"能不能抢"的判断放到 Redis 快速完成,把"真正创建订单"的动作交给后台线程异步完成。
这一章不是为了炫技,也不是为了把所有数据库操作都消灭。
它真正解决的是:
text
高并发秒杀时,请求线程不能都堵在数据库完整下单流程上。
下一篇继续讲第一个关键点:
text
Lua 脚本到底如何在 Redis 中完成库存判断和一人一单判断?