id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week
| id |
title |
type |
domain |
stack |
tags |
level |
prerequisites |
est_hours |
related |
week |
| backend.redis.scenario.stock-reservation |
抢购库存预扣:Lua 原子扣减、防重复下单、异步落库 |
scenario |
backend |
|
|
advanced |
| backend.redis.lock-ratelimit |
|
3 |
| backend.redis.lock-ratelimit |
| backend.mq.reliability |
| backend.redis.interview |
|
|
抢购库存预扣:Lua 原子扣减、防重复下单、异步落库
业务背景与问题
电商的限时抢购活动:100 件商品,开抢瞬间几万个请求。之前直接在数据库里 UPDATE stock SET num = num - 1 WHERE num > 0,结果数据库行锁排队、连接池被占满,连带其他业务一起变慢;还出现过同一个用户买了多件。
约束
- 绝对不能超卖;每人限购 1 件。
- 抢购接口要快速返回结果(成功进入下单 / 已抢光)。
- 数据库是最终的数据来源,Redis 可能丢数据。
- 不能影响其他业务。
方案设计
- 层层削峰:前端按钮防重复点击、网关按用户 / IP 限流、活动未开始直接拒绝。
- Redis 预扣:活动开始前把库存加载到 Redis;Lua 脚本原子执行“是否已购买 → 库存是否大于 0 → 扣减 → 记录用户”。
- 异步下单:预扣成功后发消息到 MQ,接口立即返回“抢购成功,订单生成中”;消费者按数据库能承受的速度建订单,用数据库唯一约束
(活动ID, 用户ID) 兜底防重复。
- 对账和回补:下单失败或超时未支付时回补 Redis 库存;定时对账 Redis 和数据库的库存。
关键代码
完整代码和并发测试见 examples/stock-lock-ratelimit(stock.go):
测试:100 个用户各点两次、共 200 个并发请求抢 10 件,结果固定是成功 10 次、剩余库存 0、购买人数 10。
取舍
| 方案 |
优点 |
缺点 |
| 数据库条件更新 |
简单、强一致 |
高并发下行锁排队,拖垮数据库 |
| Redis Lua 预扣 + MQ 异步下单 |
扛高并发、保护数据库 |
链路长,要处理 Redis 与数据库不一致、消息丢失 |
Redis DECR 后判断负数再 INCR 回去 |
不用 Lua |
不好同时做限购;有短暂负数 |
| 分布式锁包住“查 + 扣” |
好理解 |
所有请求串行抢锁,吞吐很低 |
易错点
- 先
GET 判断再 DECR(不是原子的)→ 超卖。
- 预扣成功但消息发送失败 → 库存少了但没有订单。处理:发送失败时回补库存,或者使用可靠消息(见消息可靠性)。
- 消费者重复消费 → 重复建单。用唯一约束和幂等处理。
- 用户抢到后不支付 → 超时取消订单并回补库存(延迟消息,见 MQ 专题)。
- Redis Cluster 下 Lua 脚本涉及的多个 key 必须在同一个槽:key 用相同的哈希标签,如
stock:{sku1} 和 stock:{sku1}:buyers。
面试怎么讲
“抢购的核心是不超卖和保护数据库。我在 Redis 用 Lua 脚本把‘限购检查、库存检查、扣减、记录用户’做成一个原子操作,预扣成功再发 MQ 异步建订单,数据库用唯一约束兜底防重复;下单失败或超时未支付时回补库存,定时对账。我写了并发测试:200 个请求抢 10 件,成功数固定是 10。”
追问准备:Redis 宕机怎么办(主从 + 对账,极端情况下暂停活动)?为什么不用分布式锁?消息丢了怎么办?