黑马点评秒杀优化四:Lua 已经判断过了,为什么数据库还要兜底?
本文继续整理黑马点评 Redis 实战篇第 6 章「秒杀优化」。
前三篇已经讲完异步秒杀的两段主链路:Redis + Lua 前置资格判断,以及 BlockingQueue + 后台线程异步落库。
这一篇专门解决一个非常容易误解的问题:Lua 都已经解决库存和一人一单了,为什么数据库落库阶段还要继续判断、加锁、开事务?
1. 这篇文章解决什么问题
学完第 6 章后,一个很自然的疑惑是:
text
Lua 不是已经判断库存了吗?
Lua 不是已经判断一人一单了吗?
Lua 不是已经扣 Redis 库存并记录用户了吗?
那数据库里为什么还要 count 查订单?
为什么还要 stock > 0 条件更新?
为什么后台还可能加用户锁?
为什么还要 @Transactional?
甚至会进一步觉得:
text
那前面学乐观锁、分布式锁是不是白学了?
答案是:不是白学。
真正的理解应该是:
Lua 是秒杀资格判断的前置主防线,负责在 Redis 中快速过滤大部分请求;数据库阶段是最终事实落地的后置兜底防线,负责保证 MySQL 中的库存和订单结果最终正确。
2. 先区分两个阶段
第 6 章异步秒杀一定要分成两个阶段看。
第一阶段:Redis 资格判断阶段
这一阶段发生在请求线程中。
主要做:
text
1. 判断 Redis 库存是否大于 0。
2. 判断 Redis Set 中是否已经有当前用户。
3. 如果通过,扣 Redis 库存。
4. 如果通过,把当前 userId 写入 Redis Set。
5. 返回 0,表示资格通过。
它解决的是:
text
让请求线程快速判断"你有没有资格抢"。
第二阶段:数据库落库阶段
这一阶段发生在后台线程中。
主要做:
text
1. 查询数据库订单表,确认是否已经存在订单。
2. 扣减数据库库存。
3. 保存订单到数据库。
4. 事务提交。
它解决的是:
text
让 MySQL 中最终真的出现正确的订单和库存结果。
两阶段关系图
#mermaid-svg-jsCdhiakfAfGiNBR{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-jsCdhiakfAfGiNBR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jsCdhiakfAfGiNBR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jsCdhiakfAfGiNBR .error-icon{fill:#552222;}#mermaid-svg-jsCdhiakfAfGiNBR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jsCdhiakfAfGiNBR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jsCdhiakfAfGiNBR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jsCdhiakfAfGiNBR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jsCdhiakfAfGiNBR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jsCdhiakfAfGiNBR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jsCdhiakfAfGiNBR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jsCdhiakfAfGiNBR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jsCdhiakfAfGiNBR .marker.cross{stroke:#333333;}#mermaid-svg-jsCdhiakfAfGiNBR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jsCdhiakfAfGiNBR p{margin:0;}#mermaid-svg-jsCdhiakfAfGiNBR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jsCdhiakfAfGiNBR .cluster-label text{fill:#333;}#mermaid-svg-jsCdhiakfAfGiNBR .cluster-label span{color:#333;}#mermaid-svg-jsCdhiakfAfGiNBR .cluster-label span p{background-color:transparent;}#mermaid-svg-jsCdhiakfAfGiNBR .label text,#mermaid-svg-jsCdhiakfAfGiNBR span{fill:#333;color:#333;}#mermaid-svg-jsCdhiakfAfGiNBR .node rect,#mermaid-svg-jsCdhiakfAfGiNBR .node circle,#mermaid-svg-jsCdhiakfAfGiNBR .node ellipse,#mermaid-svg-jsCdhiakfAfGiNBR .node polygon,#mermaid-svg-jsCdhiakfAfGiNBR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jsCdhiakfAfGiNBR .rough-node .label text,#mermaid-svg-jsCdhiakfAfGiNBR .node .label text,#mermaid-svg-jsCdhiakfAfGiNBR .image-shape .label,#mermaid-svg-jsCdhiakfAfGiNBR .icon-shape .label{text-anchor:middle;}#mermaid-svg-jsCdhiakfAfGiNBR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jsCdhiakfAfGiNBR .rough-node .label,#mermaid-svg-jsCdhiakfAfGiNBR .node .label,#mermaid-svg-jsCdhiakfAfGiNBR .image-shape .label,#mermaid-svg-jsCdhiakfAfGiNBR .icon-shape .label{text-align:center;}#mermaid-svg-jsCdhiakfAfGiNBR .node.clickable{cursor:pointer;}#mermaid-svg-jsCdhiakfAfGiNBR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jsCdhiakfAfGiNBR .arrowheadPath{fill:#333333;}#mermaid-svg-jsCdhiakfAfGiNBR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jsCdhiakfAfGiNBR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jsCdhiakfAfGiNBR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jsCdhiakfAfGiNBR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jsCdhiakfAfGiNBR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jsCdhiakfAfGiNBR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jsCdhiakfAfGiNBR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jsCdhiakfAfGiNBR .cluster text{fill:#333;}#mermaid-svg-jsCdhiakfAfGiNBR .cluster span{color:#333;}#mermaid-svg-jsCdhiakfAfGiNBR 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-jsCdhiakfAfGiNBR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jsCdhiakfAfGiNBR rect.text{fill:none;stroke-width:0;}#mermaid-svg-jsCdhiakfAfGiNBR .icon-shape,#mermaid-svg-jsCdhiakfAfGiNBR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jsCdhiakfAfGiNBR .icon-shape p,#mermaid-svg-jsCdhiakfAfGiNBR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jsCdhiakfAfGiNBR .icon-shape .label rect,#mermaid-svg-jsCdhiakfAfGiNBR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jsCdhiakfAfGiNBR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jsCdhiakfAfGiNBR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jsCdhiakfAfGiNBR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
用户请求秒杀
第一阶段:Redis + Lua
判断库存和一人一单
Lua 是否返回 0?
请求直接失败
订单任务进入阻塞队列
第二阶段:后台线程 + MySQL
再次兜底判断一人一单
stock > 0 条件扣库存
保存订单
这两个阶段不是互相替代关系。
它们是:
text
前置过滤 + 后置落库
3. Lua 解决了什么
Lua 主要解决两个问题。
问题 1:请求线程不用再大量访问数据库
原始同步秒杀中,请求线程要查数据库库存、查数据库订单、扣数据库库存、写数据库订单。
第 6 章优化后,请求线程先访问 Redis。
Redis 很快,而且 Lua 脚本一次执行完判断逻辑。
这样大部分无资格请求可以直接失败:
text
库存不足 -> Redis 直接返回失败
重复下单 -> Redis 直接返回失败
不需要再进入数据库。
问题 2:Redis 中的资格判断是原子的
Lua 把这些操作合在一起:
text
判断库存
判断用户是否抢过
扣 Redis 库存
记录用户已抢过
避免多个请求在 Redis 判断过程中插队。
这解决的是 Redis 预检阶段的并发安全。
4. Lua 没有解决什么
Lua 很关键,但它不是万能的。
它没有直接创建 MySQL 订单
Lua 操作的是 Redis。
它不能直接帮你执行:
sql
insert into tb_voucher_order ...
所以 MySQL 订单仍然需要 Java 后台线程保存。
它没有替代数据库事务
数据库里扣库存和保存订单仍然是两个写操作。
如果扣库存成功后,保存订单失败,就会出现:
text
数据库库存少了
但订单没生成
这就需要事务来保证:
text
扣库存和保存订单要么都成功,要么都回滚。
Lua 保证的是 Redis 脚本内部原子性。
@Transactional 保证的是数据库操作事务性。
这两个不是一回事。
它没有处理队列任务重复消费
BlockingQueue 版本中,正常情况下一个任务只会被处理一次。
但从架构理解上说,异步系统天然要考虑:
text
任务是否可能重复
任务是否可能失败
任务是否可能被重新投递
所以数据库阶段保留幂等兜底非常重要。
它没有保证 Redis 与 MySQL 永远瞬间一致
Lua 成功后,Redis 已经扣了库存。
但 MySQL 还没落库。
这时系统处于短暂的中间状态:
text
Redis 显示用户已抢到资格
MySQL 订单暂时还没创建
这种状态是异步方案的正常现象。
后台线程最终要把 MySQL 补上。
5. 为什么 createVoucherOrder 还要查订单 count
讲义中的事务方法里仍然有:
java
int count = query()
.eq("user_id", userId)
.eq("voucher_id", voucherOrder.getVoucherId())
.count();
if (count > 0) {
log.error("用户已经购买过了");
return;
}
这一步是在数据库层面兜底一人一单。
有人会问:
text
Redis Set 不是已经判断过了吗?
是的,Redis Set 是前置判断。
但数据库订单表才是最终事实记录。
如果某些情况下后台处理了重复订单任务,数据库这里再查一次,就能挡住重复插入。
可以这样理解:
text
Redis Set:在请求入口处拦住重复请求。
数据库 count:在最终落库前再确认一次事实。
前者提升性能。
后者保证落库安全。
6. 为什么扣数据库库存还要 stock > 0
讲义中扣库存:
java
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherOrder.getVoucherId())
.gt("stock", 0)
.update();
if (!success) {
log.error("库存不足");
return;
}
关键是:
java
.gt("stock", 0)
它会让 SQL 类似:
sql
update tb_seckill_voucher
set stock = stock - 1
where voucher_id = ?
and stock > 0
这就是数据库层面的防超卖兜底。
有人会问:
text
Redis 库存不是已经扣了吗?
为什么 MySQL 还要判断 stock > 0?
原因还是一样:
text
Redis 是前置资格判断,MySQL 是最终事实落地。
在数据库真正扣库存时,仍然应该让数据库自己保证不会把库存扣成负数。
这叫防线不要只放一层。
7. 为什么后台还会加用户锁
讲义中的后台处理逻辑:
java
RLock redisLock = redissonClient.getLock("lock:order:" + userId);
boolean isLock = redisLock.tryLock();
if (!isLock) {
log.error("不允许重复下单!");
return;
}
try {
proxy.createVoucherOrder(voucherOrder);
} finally {
redisLock.unlock();
}
这里的锁保护的是:
text
同一个用户的数据库落库临界区。
虽然 Lua 已经通过 Set 限制了一人一单,但后台落库阶段加用户锁,可以防止同一个用户多个订单任务同时进入数据库落库逻辑。
它的意义类似:
text
入口处已经检查门票。
真正进场时再检查一次通道秩序。
第 6 章里,Lua 是主防线。
用户锁是后台落库阶段的兜底手段。
8. 为什么 @Transactional 仍然重要
讲义中:
java
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
// 查询订单
// 扣减库存
// 保存订单
}
这里事务非常重要。
锁解决的是:
text
同一时间谁能进来执行这段业务。
事务解决的是:
text
进来之后,这几条数据库操作要不要作为一个整体提交或回滚。
这两个不是同一个问题。
举个例子:
text
后台线程拿到了锁。
扣数据库库存成功。
保存订单时数据库异常。
如果没有事务,就可能变成:
text
库存少了
订单没了
有事务时,保存订单失败会导致整个事务回滚:
text
库存扣减也回滚
订单也不保存
所以不能说:
text
反正都上锁了,事务就没用了。
锁和事务解决的问题不同。
9. 前面学的乐观锁、分布式锁是不是白学
不是。
它们是理解第 6 章的台阶。
乐观锁解决什么
乐观锁主要解决:
text
多个线程同时扣数据库库存时,不能把库存扣成负数。
典型写法:
java
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update()
第 6 章数据库落库阶段仍然保留了这个思想。
分布式锁解决什么
分布式锁主要解决:
text
集群环境下,同一个用户的多个请求不能同时进入一人一单临界区。
第 6 章后台落库阶段仍然可能用 Redisson 用户锁兜底。
Lua 解决什么
Lua 解决的是:
text
把库存判断和一人一单判断前移到 Redis,并保证 Redis 内部判断原子。
它重点提升入口吞吐量。
所以三者不是谁替代谁,而是在不同层面保护系统:
text
Lua:Redis 入口快速预检
分布式锁:后台落库用户维度互斥兜底
乐观锁:数据库库存扣减兜底
事务:数据库多步写操作一致性
10. 多层防线图
#mermaid-svg-TY3KKTS6PDbMWo4w{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-TY3KKTS6PDbMWo4w .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TY3KKTS6PDbMWo4w .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TY3KKTS6PDbMWo4w .error-icon{fill:#552222;}#mermaid-svg-TY3KKTS6PDbMWo4w .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TY3KKTS6PDbMWo4w .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TY3KKTS6PDbMWo4w .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TY3KKTS6PDbMWo4w .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TY3KKTS6PDbMWo4w .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TY3KKTS6PDbMWo4w .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TY3KKTS6PDbMWo4w .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TY3KKTS6PDbMWo4w .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TY3KKTS6PDbMWo4w .marker.cross{stroke:#333333;}#mermaid-svg-TY3KKTS6PDbMWo4w svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TY3KKTS6PDbMWo4w p{margin:0;}#mermaid-svg-TY3KKTS6PDbMWo4w .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TY3KKTS6PDbMWo4w .cluster-label text{fill:#333;}#mermaid-svg-TY3KKTS6PDbMWo4w .cluster-label span{color:#333;}#mermaid-svg-TY3KKTS6PDbMWo4w .cluster-label span p{background-color:transparent;}#mermaid-svg-TY3KKTS6PDbMWo4w .label text,#mermaid-svg-TY3KKTS6PDbMWo4w span{fill:#333;color:#333;}#mermaid-svg-TY3KKTS6PDbMWo4w .node rect,#mermaid-svg-TY3KKTS6PDbMWo4w .node circle,#mermaid-svg-TY3KKTS6PDbMWo4w .node ellipse,#mermaid-svg-TY3KKTS6PDbMWo4w .node polygon,#mermaid-svg-TY3KKTS6PDbMWo4w .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TY3KKTS6PDbMWo4w .rough-node .label text,#mermaid-svg-TY3KKTS6PDbMWo4w .node .label text,#mermaid-svg-TY3KKTS6PDbMWo4w .image-shape .label,#mermaid-svg-TY3KKTS6PDbMWo4w .icon-shape .label{text-anchor:middle;}#mermaid-svg-TY3KKTS6PDbMWo4w .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TY3KKTS6PDbMWo4w .rough-node .label,#mermaid-svg-TY3KKTS6PDbMWo4w .node .label,#mermaid-svg-TY3KKTS6PDbMWo4w .image-shape .label,#mermaid-svg-TY3KKTS6PDbMWo4w .icon-shape .label{text-align:center;}#mermaid-svg-TY3KKTS6PDbMWo4w .node.clickable{cursor:pointer;}#mermaid-svg-TY3KKTS6PDbMWo4w .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TY3KKTS6PDbMWo4w .arrowheadPath{fill:#333333;}#mermaid-svg-TY3KKTS6PDbMWo4w .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TY3KKTS6PDbMWo4w .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TY3KKTS6PDbMWo4w .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TY3KKTS6PDbMWo4w .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TY3KKTS6PDbMWo4w .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TY3KKTS6PDbMWo4w .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TY3KKTS6PDbMWo4w .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TY3KKTS6PDbMWo4w .cluster text{fill:#333;}#mermaid-svg-TY3KKTS6PDbMWo4w .cluster span{color:#333;}#mermaid-svg-TY3KKTS6PDbMWo4w 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-TY3KKTS6PDbMWo4w .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TY3KKTS6PDbMWo4w rect.text{fill:none;stroke-width:0;}#mermaid-svg-TY3KKTS6PDbMWo4w .icon-shape,#mermaid-svg-TY3KKTS6PDbMWo4w .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TY3KKTS6PDbMWo4w .icon-shape p,#mermaid-svg-TY3KKTS6PDbMWo4w .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TY3KKTS6PDbMWo4w .icon-shape .label rect,#mermaid-svg-TY3KKTS6PDbMWo4w .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TY3KKTS6PDbMWo4w .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TY3KKTS6PDbMWo4w .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TY3KKTS6PDbMWo4w :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
秒杀请求
Lua 防线
Redis 判断库存和一人一单
资格通过?
直接失败,不进数据库
订单任务进入队列
后台线程处理
用户锁兜底
数据库 count 兜底一人一单
stock > 0 兜底防超卖
@Transactional 保证扣库存和保存订单一致
这张图很适合面试时讲。
它能说明你不是只会背:
text
用 Redis + Lua + 队列优化秒杀。
而是真的理解:
text
不同机制分别保护哪一层。
11. 本篇最容易混淆的几个点
1. Lua 成功是不是数据库订单一定成功
不是。
Lua 成功表示 Redis 资格判断通过。
数据库订单还要由后台线程异步创建。
2. Lua 原子性是不是等于数据库事务
不是。
Lua 原子性保证 Redis 脚本内部不会被插队。
数据库事务保证数据库多条写操作要么一起成功,要么一起回滚。
3. 有锁是不是就不需要事务
不是。
锁控制并发进入。
事务控制数据库操作一致性。
4. 有 Lua 是不是就不需要数据库层兜底
不建议这样理解。
Redis 是前置判断,数据库是最终事实。
最终落库时仍然保留一人一单、库存大于 0、事务等兜底更稳。
5. 前面学的乐观锁和分布式锁是不是被废弃了
不是。
第 6 章是在它们基础上的性能优化。
乐观锁和分布式锁仍然是理解数据库兜底和用户维度互斥的重要基础。
12. 面试怎么回答
如果面试官问:Lua 已经判断库存和一人一单了,为什么数据库还要判断?
可以这样回答:
Lua 解决的是 Redis 入口处的秒杀资格预检,它可以快速过滤无效请求,并保证 Redis 中库存判断、一人一单判断、扣库存、记录用户这些操作的原子性。但 MySQL 才是最终订单和库存的事实数据源,后台异步落库时仍然需要数据库层面的兜底,比如再次判断用户是否已有订单、扣库存时带上 stock > 0 条件,避免异常、重复任务或状态不一致导致脏数据。
如果面试官问:有分布式锁是不是就不需要事务?
可以这样回答:
不是。分布式锁解决的是并发互斥问题,也就是同一时刻谁能进入临界区;事务解决的是数据库多步操作的一致性问题,比如扣库存和保存订单要么都成功,要么都回滚。即使拿到了锁,如果扣库存成功后保存订单失败,没有事务仍然会造成库存和订单不一致。
如果面试官问:第 6 章秒杀优化中的多层防线是什么?
可以这样回答:
第一层是 Redis + Lua,在请求入口处原子判断库存和一人一单,快速过滤请求;第二层是阻塞队列和后台线程,把数据库落库异步化;第三层是后台落库时的用户锁、一人一单查询、stock > 0 条件更新和事务,保证数据库中的最终结果正确。
13. 总结
第 6 章最容易误解的地方是:
text
看到 Lua 很强,就以为数据库层防线都可以不要。
更准确的理解是:
text
Lua 是前置主防线。
数据库落库逻辑是最终兜底防线。
完整链路应该这样记:
text
请求线程执行 Lua
↓
Redis 原子判断资格
↓
资格通过后订单任务进入队列
↓
后台线程异步落库
↓
数据库层继续兜底一人一单、防超卖和事务一致性
所以第 6 章并没有推翻前面学过的乐观锁、分布式锁、事务。
它是在这些正确性基础上,把高并发入口迁移到 Redis,让请求线程更快返回,让数据库只处理真正有资格的订单任务。