全表扫描时InnoDB为每条聚集索引记录加X,REC_NOT_GAP记录锁,非Next-Key Lock;因锁住全部现有行且覆盖全主键区间,效果近似锁表,但本质是逐行Record Lock。

全表扫描不会“触发”Next-Key Lock,而是直接跳过行级锁机制,退化为隐式表级阻塞——这不是Next-Key Lock在起作用,是InnoDB被迫锁住所有聚集索引记录的结果。
WHERE条件没走索引时,InnoDB到底加了什么锁?
当DELETE或UPDATE语句的WHERE条件未命中任何索引(比如字段无索引、用了函数包装、类型隐式转换),优化器会走type: ALL执行计划。此时InnoDB无法定位具体行,只能逐行扫描聚集索引(即主键B+树)并为每一条扫描到的记录加X(排他)记录锁。这不是Next-Key Lock,但效果接近锁表:
- 所有已存在记录都被加上
Record Lock,后续INSERT、UPDATE、DELETE只要涉及任意一行,都会被阻塞 - 由于扫描覆盖全部主键值区间,间隙部分也被“事实覆盖”,新插入几乎必然等待
-
SHOW ENGINE INNODB STATUS里看到的lock_mode X locks rec but not gap说明是纯记录锁,不是Next-Key - 若表为空或只有几行,阻塞不明显;但千万级表上,这个过程本身就会卡住其他事务的DML
为什么有人误以为这是Next-Key Lock导致的“锁表”?
因为现象高度相似:QPS骤降、连接堆积、SELECT ... FOR UPDATE和INSERT全部卡住。但根因不同:
- Next-Key Lock是“有索引但范围大”——比如
WHERE status = 0走非唯一索引idx_status,会锁(prev_value, 0]整个间隙,可能横跨数万行 - 全表扫描是“没索引就硬扫”——锁的是每一条聚集索引记录,数量=表行数,且没有间隙锁逻辑
- 两者在
performance_schema.data_locks中表现不同:LOCK_MODE字段一个是X,GAP或X,REC_NOT_GAP,另一个全是X,REC_NOT_GAP
FORCE INDEX真能解决问题?小心从“分散锁”变成“热点锁”
强制走某个索引(如DELETE FROM t WHERE status = 0 FORCE INDEX(idx_status))确实能避免全表扫描,但未必更安全:
- 如果
idx_status是非唯一索引,WHERE status = 0仍会触发Next-Key Lock,锁住所有status = 0行及其前隙,可能比全表扫描锁得更“精准却更致命” - 若该索引选择性极差(比如
status只有0/1两个值),强制使用后反而把锁争用集中到少数几个索引页上,CPU没涨,锁等待队列暴涨 -
EXPLAIN FORMAT=TRADITIONAL必须看key_len和rows:若rows仍接近总行数,说明索引无效,FORCE只是自欺欺人 - 高并发定时任务(如每5分钟
SELECT ... FOR UPDATE LIMIT 1)一旦强制走低选择性索引,极易形成“排队抢第一个间隙”的死锁链
真正有效的索引覆盖策略
覆盖索引的目标不是“让语句走索引”,而是“让语句不用回表、且锁范围最小”。关键在字段组合与查询模式匹配:
- 对
UPDATE t SET status = 1 WHERE user_id = ? AND status = 0,理想索引是(user_id, status)——最左前缀匹配user_id,等值过滤status,且该联合索引已包含两字段,无需回表 - 若还需更新
update_time,而update_time不在索引中,回表不可避免,此时应考虑扩展索引为(user_id, status, update_time)(注意顺序) - 对
DELETE语句,覆盖索引意义有限,因为删除只需定位,不读数据;重点应放在“能否转为唯一条件”(如用id代替order_no)或“是否可拆分为先查后删两阶段” - 线上紧急场景下,
SELECT ... FOR UPDATE SKIP LOCKED(MySQL 8.0+)比强行索引更治本——它让并发任务跳过已被锁的行,彻底打破等待链
最易被忽略的一点:索引是否生效,不取决于你写了FORCE INDEX,而取决于参数类型是否与字段定义完全一致。哪怕只差一个字符集(utf8mb4_0900_as_cs vs utf8mb4_general_ci),或一个隐式CAST,索引就直接失效——这种失效不会报错,只会安静地全表扫描。


















