秒杀库存扣减不能仅靠mysql行锁,因锁粒度粗、磁盘io慢导致tps难超500;应采用redis分片+lua原子脚本预扣减,避免单key瓶颈与非原子操作。

库存扣减为什么不能只用数据库行锁
直接在 MySQL 里对 stock 字段加 SELECT ... FOR UPDATE,在秒杀场景下会迅速成为瓶颈:大量请求排队等锁,事务堆积导致超时、死锁、连接耗尽。哪怕用了连接池,TPS 也很难突破 500。根本问题不在 SQL 写得对不对,而在于「锁粒度太粗 + 存储层响应慢」。
- 每笔扣减都触发一次磁盘 IO 和事务日志刷盘,吞吐上不去
- 热点商品(比如 ID=1001)的所有请求都争抢同一行锁,形成“锁队列”
- 网络延迟放大锁等待时间,一个请求卡住 200ms,后面几十个就全跟着 hang 住
用 Redis 原子操作做预扣减,但要注意 key 设计
把库存先加载到 Redis,用 DECR 或 DECRBY 做原子扣减,是主流做法。但很多人直接用商品 ID 当 key,比如 stock:1001,这会导致单个 key 成为性能瓶颈——Redis 是单线程处理命令,所有对这个 key 的 DECR 都串行执行。
- 正确做法是分片:按商品 ID 取模,例如
stock:1001:0、stock:1001:1… 用INCR初始化后,扣减时随机选一个分片DECR - 分片数建议设为 8 或 16,太少起不到分流作用,太多增加客户端计算和 key 管理成本
- 必须配合 Lua 脚本保证「检查 + 扣减 + 设置过期」三步原子性,不能拆成多个 Redis 命令
示例 Lua 脚本:
if redis.call("GET", KEYS[1]) == false then
return -1
end
local stock = redis.call("DECR", KEYS[1])
if stock
<h3>扣减成功后,异步写库与超卖兜底怎么配</h3>
<p>Redis 预扣减只是第一道闸门,最终库存必须落库。但不能同步写 MySQL——那又回到锁瓶颈。所以采用「异步写 + 补单校验」组合:</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/ai/3995" title="Scikit-learn"><img
src="https://img.php.cn/upload/ai_manual/001/246/273/178599594177707.png" alt="Scikit-learn" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/ai/3995" title="Scikit-learn" class="overflowclass">Scikit-learn</a>
<p class="overflowclass">Scikit-learn是一款AI学习资源工具,Python机器学习库。</p>
</div>
<a rel="nofollow" href="/ai/3995" title="Scikit-learn" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
- Redis 扣减成功后,立即发消息到 Kafka/RocketMQ,由消费服务批量更新 MySQL
stock字段 - 消费端按商品 ID 分组,合并相同商品的多次扣减(如 10 次 -1 → 一次 -10),减少 DB 更新次数
- 关键兜底:定时任务每分钟扫描 Redis 中剩余库存为 0 的商品,比对 DB 实际库存;若 DB 还有余量,说明 Redis 扣减漏了或消息丢失,触发补偿写入
注意:补偿逻辑里不能只靠 Redis key 是否存在判断,要读 stock 字段值,因为 key 可能被误删或过期。
Go 里怎么控制请求不打穿 Redis
即使用了 Redis,瞬时流量仍可能压垮它。需要在 Go 层做限流 + 请求合并,而不是依赖下游扛压。
- 用
golang.org/x/time/rate对商品维度限流,比如rate.NewLimiter(1000, 2000)表示每秒最多放行 1000 请求,允许最多 2000 请求积压 - 对同一商品 ID 的请求,在 5–10ms 窗口内合并:用
sync.Map缓存 pending 请求,用time.AfterFunc触发批量处理 - 拒绝策略要明确:当 Redis 返回 -2(超卖)或连接超时时,直接返回
"sold out",不重试、不降级查 DB——查 DB 此时只会雪上加霜
容易忽略的是 panic 恢复:Redis 客户端超时或连接断开时,redis.Client.Do() 可能 panic,必须用 defer/recover 包裹,否则 goroutine 泄露。










