秒杀前无需预热MySQL热点行,因X锁是事务执行时动态加的,预热缓存页对写锁无用;真正需预热的是Redis分片key,并确保MySQL有唯一索引、整型直传WHERE条件、隔离级别设为READ COMMITTED。

秒杀前要不要预热热点行?别白忙活
MySQL行锁不靠“预热”生效,InnoDB的X锁是事务执行时动态加的,缓存页(buffer pool)里有没有这行数据,不影响锁竞争本质。所谓“预热”,顶多让SELECT更快返回,但对UPDATE ... FOR UPDATE这种写锁场景毫无帮助——真正卡住请求的从来不是磁盘IO,而是内存中那个锁对象的串行等待队列。
常见错误现象包括:提前跑几十遍SELECT * FROM stock WHERE sku_id = 123 FOR UPDATE,以为能“占住锁”,结果秒杀一开照样排队;或者用SELECT ... LOCK IN SHARE MODE反复查,反而把间隙锁(Gap Lock)长期持有着,阻塞后续插入。
- 真正该预热的是Redis分片key,比如
goods:123:stock:0到goods:123:stock:9,确保它们都已加载进内存并有初始值 - MySQL侧只需确认
sku_id字段上有唯一索引(或主键),且EXPLAIN显示type=const、key=PRIMARY - 避免在预热脚本里混入事务外操作(如日志打点、HTTP上报),否则可能意外延长事务生命周期
WHERE条件带函数就等于放弃索引锁
哪怕你建了UNIQUE INDEX idx_sku ON stock(sku_id),只要SQL里出现隐式转换或函数包裹,InnoDB就无法精准定位单行,锁会立刻扩散——轻则锁住整个索引范围,重则全表扫描加锁,秒杀时直接雪崩。
典型失效写法:
-
WHERE sku_id = '123'→sku_id是INT类型,字符串比较触发隐式转换,索引失效 -
WHERE CAST(sku_id AS CHAR) = '123'→ 显式函数,同样跳过索引 -
WHERE sku_id IN (123, 456, 789)→ MySQL 8.0.22之前,FOR UPDATE SKIP LOCKED不保证逐个跳过被锁项,行为不可控
正确姿势只有一条:WHERE sku_id = 123,整型直传,无空格、无引号、无函数。
READ COMMITTED是热点更新的底线隔离级别
默认的REPEATABLE READ在秒杀场景下是隐形杀手:它会让WHERE sku_id = 123这种等值查询也带上间隙锁(Gap Lock),一旦库存扣减后新记录插入(比如补货单),就会被意外阻塞;更糟的是,分段更新(shard by slot)若用了(product_id, shard_id)联合索引,但查询只写了WHERE product_id = 123,间隙锁可能锁住整个shard_id区间,10个槽位全被封死。
必须显式设置:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
这个级别下:
-
UPDATE只锁命中的物理行,不加间隙锁 -
SELECT ... FOR UPDATE SKIP LOCKED能真正跳过被锁行,而不是退化成等待 - 事务提交后锁立即释放,不会因MVCC快照拖长锁持有时间
SKIP LOCKED不是万能跳过,用错就变全表锁
SKIP LOCKED只在SELECT ... FOR UPDATE或FOR SHARE中生效,且严重依赖执行计划。它不是“智能跳过”,而是优化器在遍历索引时,遇到已被加X锁的记录就跳过,继续找下一个——前提是它得能快速定位“下一个”。
所以必须满足:
- WHERE条件命中唯一索引或主键,
EXPLAIN中rows显示≤1 - 显式加上
LIMIT 1,防止优化器误判为范围扫描 - 不能嵌套子查询,例如
SELECT * FROM (SELECT ... FOR UPDATE SKIP LOCKED) t,子查询里的SKIP LOCKED会被忽略
反例:SELECT * FROM stock WHERE count > 0 FOR UPDATE SKIP LOCKED——没主键条件,全表扫描,每行都尝试加锁再跳过,实际效果≈全表锁定+高CPU空转。
真正难的不是写对那条SQL,而是把Redis预扣减、分段更新、隔离级别切换、索引验证这四件事拧成一股绳——漏掉任一环,前面所有优化都会在流量洪峰里瞬间归零。


















