黑马点评-秒杀优化-01_async_seckill_idea

黑马点评秒杀优化一:为什么秒杀下单不能一直同步查库?

本文整理黑马点评 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 中完成库存判断和一人一单判断?
相关推荐
摇滚侠1 小时前
IDEA 创建 Java 项目 lib 和 resources
java·ide·intellij-idea
必胜刻1 小时前
一个异步生成游戏功能的落地复盘:Redis Stream + WebSocket + 状态补偿
redis·websocket·golang·gin·状态补偿
宸津-代码粉碎机1 小时前
Spring AI企业级Agent实战|多工具自动规划+并行调度落地,彻底解决复杂业务AI任务编排问题
java·大数据·人工智能·spring boot·python·spring
lixia0417mul21 小时前
flink接入spring体系
java·spring·flink
biubiubiu07061 小时前
自定义starter 可以导入SpringBoot直接使用
java·spring boot·spring
TDengine (老段)2 小时前
TDengine 数据修复与迁移 — VGroup 调度、S3 外挂与运维操作
大数据·运维·数据库·物联网·时序数据库·iot·tdengine
TFHoney2 小时前
当 AI 真正走进你的终端:Claude Code 使用指南
java·人工智能·ai编程
努力努力再努力wz2 小时前
【Qt入门系列】一文掌握 Qt 常用显示类控件:QLCDNumber、QProgressBar 与 QCalendarWidget
c语言·开发语言·数据结构·数据库·c++·git·qt
TeamDev2 小时前
JxBrowser 9.1.1 版本发布啦!
java·前端·chromium·混合应用·jxbrowser·嵌入式浏览器·浏览器控件