非唯一索引范围查询必须用Next-Key Lock防幻读,因其无法唯一定位终点,需向右多探一个索引项并锁定所有相关记录和间隙,如age BETWEEN 25 AND 30会锁(20,25)、(25,25)、(25,30)、(30,35)等区间。

非唯一索引范围查询锁住的不只是匹配行,而是整段扫描路径上的记录 + 间隙,这是 InnoDB 防幻读的强制行为,不是配置错误或 bug。
非唯一索引范围查询为何必须用 Next-Key Lock
Next-Key Lock = Record Lock(行锁) + Gap Lock(间隙锁),它锁定的是「前开后闭」区间,比如 (25, 30]。非唯一索引无法靠键值唯一定位终点——哪怕当前扫到 age = 30,也不能排除下一页还有另一个 age = 30 或 age = 28。所以 InnoDB 必须向右多探一个索引项,把整个可能插入新记录的“空档”都封死。
常见错误现象:
- 执行
SELECT * FROM users WHERE age BETWEEN 25 AND 30 FOR UPDATE,实际锁了age = 25所有行、age = 30所有行,以及(20, 25)、(25, 25)、(25, 30)、(30, 35)四个间隙 -
(25, 25)是重复键值之间的“虚拟间隙”,也会被锁,这是防止在两个相同值之间插入新记录
唯一索引 vs 非唯一索引:锁范围差异的关键点
唯一索引能精确判断边界,非唯一索引不能。这个差别直接决定锁是否退化:
-
WHERE id > 100(主键):扫描到第一个id = 101后,若下一个是id = 105,只锁(100, 101]和(101, 105],不会盲目延伸 -
WHERE age > 25(非唯一索引):即使当前页最大age = 30,InnoDB 仍要确认后续页是否还有age = 26,于是可能锁住(30, +∞) - 唯一索引等值查询(如
id = 123)可退化为纯 Record Lock;非唯一索引等值查询(如name = 'Alice')默认仍是 Next-Key Lock,除非满足优化2条件
如何验证你正被非唯一索引的间隙锁拖慢
别猜,直接查锁状态和执行计划:
- 运行
SELECT * FROM performance_schema.data_locks,重点关注LOCK_MODE含GAP或NEXT-KEY的行 - 执行
EXPLAIN FORMAT=TRADITIONAL SELECT * FROM t WHERE age BETWEEN 25 AND 30 FOR UPDATE,如果rows显著大于实际匹配数,说明已锁多行+多间隙 - 另起事务执行
INSERT INTO t (age, ...) VALUES (26, ...),若被阻塞,说明(25, 30)间隙已被锁 -
SHOW ENGINE INNODB STATUS\G中的TRANSACTIONS段会显示等待的锁类型和持锁事务 ID
哪些写法会让锁范围雪上加霜
不是所有范围条件都一样危险,这些操作会彻底破坏索引利用,导致锁爆炸:
- 用函数包裹字段:
WHERE YEAR(create_time) = 2024→ 索引失效 → 全表扫描 → 所有主键 Next-Key Lock - 隐式类型转换:
WHERE status = 0但status是VARCHAR→ 索引失效 → 全表扫描加锁 - 联合索引顺序错:
INDEX (a, b, c),却查WHERE b = 2 AND c = 3→ 不满足最左前缀 → 索引失效 - ORDER BY 字段无索引:
WHERE status = 1 ORDER BY created_at LIMIT 10→ 文件排序 → 可能全表扫描加锁,哪怕只取 1 行
真正难处理的不是“锁得多”,而是“锁得不可预测”:非唯一索引的重复密度越高、数据分布越不均,InnoDB 向右多探的范围就越难估计。业务层加锁或限流只是兜底,根治还得回到索引定义本身——该建唯一索引的别图省事,该走覆盖索引的别写 SELECT *。


















