主键索引锁在秒杀减库存时成为热点锁,因所有请求均更新同一主键行(如id=1),即使命中主键索引加行锁,仍导致成千上万并发请求串行排队争抢该X锁;拆分主键或加唯一索引无效,除非UPDATE语句实际改用新索引字段;乐观锁仅缩短持锁时间,不减少争锁频次;实用解法包括库存分段、预占+异步扣减、Redis初筛。

主键索引锁为什么在秒杀减库存时变成热点锁
因为所有减库存语句都落在同一行(比如 id = 1 的商品记录),MySQL 的 UPDATE ... WHERE id = ? 会命中主键索引,加的是行级 X 锁——但这一行就一个,成千上万并发请求全卡在这把锁上排队。
即使用了 SELECT ... FOR UPDATE 显式加锁,只要 WHERE 条件恒定指向同一主键值,锁竞争本质没变;InnoDB 不会因为你“先查后更”就绕过锁排队。
常见错误现象:SHOW ENGINE INNODB STATUS 里看到大量 lock wait timeout exceeded 或线程长期处于 Updating 状态;information_schema.INNODB_TRX 中多个事务的 TRX_WAITING 指向同一个 LOCK_TRX_ID。
为什么不能靠拆分主键或加唯一索引缓解
主键本身是唯一的,拆不了;加额外唯一索引(比如 sku_code)只是多一条索引路径,但减库存语句若仍走 WHERE id = ?,锁还是落在主键上。只有当查询条件真正改用新唯一索引字段、且该字段能定位到单行,锁才会落到对应二级索引的叶子页——但这要求业务逻辑重构,且二级索引加 X 锁后还需回表,反而可能引入更多锁等待。
关键点:
- 锁对象由执行计划决定,不是由表结构决定
-
EXPLAIN必须确认实际使用的key是哪个索引 - 如果
WHERE sku_code = ?走了唯一索引,那锁的是该索引记录,不是主键记录——但前提是你的UPDATE语句真这么写了
乐观锁方案里 version 字段为什么挡不住主键锁竞争
很多人以为加 version 就能跳过锁,其实不然:乐观锁本质仍是 UPDATE ... SET stock = ?, version = ? WHERE id = ? AND version = ?,InnoDB 执行前仍需对 id = ? 这行加 X 锁来校验并修改,否则无法保证 WHERE 条件的原子性。锁依然存在,只是失败后由应用重试而非数据库阻塞。
所以乐观锁降低的是「持有锁的时间」,不是「争抢锁的频次」——高并发下照样排队获取锁,只是更快释放而已。
性能影响明显体现在:
- CPU 在锁队列调度上开销上升(
innodb_row_lock_waits指标飙升) - 事务平均响应时间毛刺变多,尤其在锁超时重试叠加时
- 重试逻辑若没做指数退避,可能引发雪崩式重试放大流量
真正绕开主键行锁的实用手段
核心思路是让并发请求不落在同一行上。可行做法包括:
- 库存分段:把 10000 库存拆成 100 行,每行 100 份,用
item_id和segment_id联合主键,减库存随机选一段更新(需配合分布式 ID 或一致性哈希) - 异步扣减 + 预占:前端先调用
INSERT INTO stock_prelock (item_id, user_id) VALUES (?, ?)(唯一索引防重复),成功后再异步走消息队列扣真实库存——预占表写压力分散,且冲突时直接失败,不进锁队列 - Redis 原子计数器兜底:用
DECR做初筛,仅当DECR返回 ≥ 0 时才走 MySQL 扣减,大幅降低 MySQL 请求量
注意:分段和预占都要求业务接受“最终一致性”,比如超卖防护粒度从“绝对不超”放宽到“极大概率不超”。这往往是秒杀场景下不得不做的权衡。


















