MySQL秒杀行锁竞争是结构性瓶颈,无法通过配置或SQL优化绕开;必须采用Redis预扣减+分段更新(shard)+最终一致性校验三重方案。

MySQL 秒杀场景的行锁竞争不是配置或 SQL 写法能绕开的问题——UPDATE goods SET stock = stock - 1 WHERE id = 123 这类语句,只要所有请求都命中同一行,InnoDB 就必须串行加 X 锁。物理页位置固定、聚簇索引不可变,锁竞争是结构性瓶颈。
为什么 EXPLAIN 显示 type=const 也拦不住排队?
因为锁竞争发生在存储引擎层,跟优化器是否走索引无关。哪怕 EXPLAIN 显示 rows=1、type=const,只要所有事务更新同一 id,就共享同一个锁对象(lock struct),后到的事务只能等前一个事务释放锁。
-
SHOW ENGINE INNODB STATUS里反复出现waiting for trx id,但information_schema.INNODB_TRX中大量事务trx_state = 'LOCK WAIT'卡在RUNNING - CPU 拉满、磁盘 IO 极低 → 锁在内存中空转,根本没走到磁盘 I/O 阶段
- 调大
innodb_buffer_pool_size或改innodb_log_file_size完全无效
分段更新(shard by slot)必须满足的硬条件
把单行库存拆成 10 行(shard_id 0~9),实测可降锁冲突 80%+,但漏掉任一条件,效果归零甚至更糟:
-
WHERE必须同时含product_id和shard_id,且联合主键顺序为(product_id, shard_id) -
EXPLAIN中key字段必须显示PRIMARY,rows ≤ 1;若出现range或index,说明没走对索引 - 事务只包这一条
UPDATE+ROW_COUNT()判断;禁止日志、HTTP 调用、SLEEP(),否则锁滞留时间指数增长 - 隔离级别必须设为
READ COMMITTED:它禁用间隙锁,避免因shard_id查询范围扩大而锁住相邻槽位
Redis 预扣减不是可选项,是流量闸门
真实压测中,80% 请求是库存已空还狂点——这些请求根本不该碰 MySQL。必须用 Redis 原子操作提前拦截:
- Key 按
goods_id % 10分片,如goods:123:stock:0~goods:123:stock:9,避免单 key 热点 - 扣减用
DECRBY goods:123:stock:0 1,返回值 ≥ 0 才放行;否则直接拒绝,不走 DB - 异步落库节奏要可控:用
Redis Streams或限速消费者(如每秒最多 500 条),防止 DB 写入突增
MySQL 最终一致性校验不能省,stock >= ? 是防超卖最后一道防线
Redis 和 MySQL 之间存在网络分区风险,仅靠 Redis 扣减无法保证强一致。每次真实扣减必须走 MySQL 最终校验:
- SQL 示例:
UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1 - 必须检查
ROW_COUNT()是否为 1;若为 0,说明库存已不足,需回滚 Redis 扣减(用INCRBY补回) - 这个判断不能放在应用层“先查再更”,必须合并在一条原子 UPDATE 里,否则竞态窗口依然存在
最容易被忽略的是:分段更新后,每个 shard_id 的库存总和 ≠ 实际可用库存。业务侧必须接受这种最终一致性,并在展示层做聚合(如缓存汇总值并定时刷新),而不是依赖实时 SUM() 查询——那又会引入新的锁竞争点。



















