mysql秒杀热点行更新瓶颈无法通过调参或sql优化根治,根本原因是innodb对同一行加x锁必须串行等待;有效方案只有三:redis预扣减过滤无效请求、分段更新打散锁压力、mysql最终一致性校验兜底。

MySQL 无法根治秒杀热点行更新瓶颈——这不是调参或换 SQL 写法能解决的问题,本质是 InnoDB 对同一行加 X 锁时必须串行等待。真正有效的路径只有三条:Redis 预扣减过滤无效请求、分段更新(shard by slot)打散锁压力、MySQL 最终一致性校验兜底。
为什么 UPDATE ... WHERE id = ? 必然排队,调索引没用
只要所有请求都命中同一行(比如 product_id = 123),哪怕 EXPLAIN 显示 type=const、rows=1,也改变不了物理页和锁对象唯一的事实。聚簇索引决定了这行数据在磁盘和内存中位置固定,锁竞争无法靠优化器绕开。
常见误判现象:
-
SHOW ENGINE INNODB STATUS里反复出现waiting for trx id -
information_schema.INNODB_TRX中大量事务trx_state = 'LOCK WAIT'却卡在 RUNNING 状态 - CPU 拉满、磁盘 IO 很低——锁在内存里空转,根本没走到磁盘
分段更新 UPDATE ... WHERE product_id = ? AND shard_id = ? 的硬前提
把单行库存拆成 10 行(shard_id 0~9),实测可降锁冲突 80%+,但漏掉任一条件,效果归零甚至更糟:
-
WHERE必须同时包含product_id和shard_id,且联合主键顺序为(product_id, shard_id);EXPLAIN中key字段必须显示PRIMARY,rows ≤ 1 - 事务只包这一条
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 最终校验仍不可省,UPDATE ... WHERE stock >= ? 是防超卖最后一道防线
Redis 和 MySQL 之间存在网络分区风险,仅靠 Redis 扣减无法保证强一致。每次真实扣减必须走 MySQL 最终校验:
- SQL 示例:
UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1 - 必须检查
ROW_COUNT()是否为 1,为 0 则说明 DB 层库存已不足,需回滚 Redis 扣减(用 Lua 脚本保证原子性) - 不要依赖
SELECT ... FOR UPDATE+UPDATE两阶段,它比单条带条件的UPDATE多一次锁获取,反而加重竞争
最容易被忽略的是:分段更新和 Redis 预扣减都只是缓解手段,最终校验那条带 stock >= ? 条件的 UPDATE 语句,才是防止超卖的不可替代环节——它不在应用层做判断,而是在数据库引擎层用原子写保证。











