next-key lock是mysql在可重复读隔离级别下为范围查询使用的“行锁+间隙锁”组合,锁定左开右闭区间,防止幻读并引发死锁;其加锁范围由索引结构和实际扫描路径共同决定,而非仅由where条件字面值决定。

Next-Key Lock 是什么,和普通范围查询有什么关系
MySQL 在可重复读(RR)隔离级别下,SELECT ... FOR UPDATE 或 UPDATE 带范围条件(如 WHERE id > 10)时,并不会只锁匹配的行,而是会使用 Next-Key Lock:即「行锁 + 间隙锁」的组合。它锁定的是一个左开右闭区间,比如索引中存在值 10, 20, 30,那么 WHERE id > 15 可能会锁住 (10, 20] 和 (20, 30] 这样的范围——注意,(10, 20] 表示不包含 10、但包含 20,且包含 10 和 20 之间的所有空隙。
这直接导致两个事务即使操作“看似不重叠”的记录,也可能因覆盖相同间隙而互相等待。
典型死锁场景:两个事务按不同顺序扫描同一索引范围
常见触发模式是:两个事务都执行范围更新,但扫描方向或起始点不同,最终在某个间隙上形成循环等待。
例如:
- 事务 A 执行:
UPDATE t SET name='a' WHERE id BETWEEN 10 AND 30 - 事务 B 执行:
UPDATE t SET name='b' WHERE id BETWEEN 20 AND 40
如果表有主键索引 id,且当前数据为 10, 20, 30, 40,那么:
- 事务 A 实际加锁区间可能包括:
(-, 10], (10, 20], (20, 30], (30, 40](取决于优化器选择的扫描路径和是否命中边界) - 事务 B 加锁可能覆盖:
(10, 20], (20, 30], (30, 40], (40, +∞)
二者在 (20, 30] 和 (30, 40] 上发生重叠竞争;若 A 先持有了 (20, 30] 等待 (30, 40],而 B 先持有了 (30, 40] 等待 (20, 30],死锁就产生了。
关键点:
-
Next-Key Lock的范围不是由 WHERE 条件“字面意思”决定的,而是由索引结构 + 查询实际走的扫描路径共同决定 - 即使 WHERE 条件无重叠(如
id > 100vsid ),只要底层扫描触及同一间隙(比如都从索引最左开始向右遍历),仍可能锁相同间隙
如何验证是不是 Next-Key Lock 引发的死锁
MySQL 的死锁日志(SHOW ENGINE INNODB STATUS\G)里会明确写出每个事务持有的锁类型和区间。重点关注:
-
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:下的lock_mode X locks gap before rec(间隙锁)、lock_mode X locks rec but not gap(仅行锁)、lock_mode X locks gap before rec insert intention(插入意向锁)等描述 -
*** (2) HOLDS THE LOCK(S):中列出的next key lock区间,例如space id 123 page no 17 n bits 72后跟着record lock, heap no 5和supremum pseudo-record,说明已锁到 supremum(最大虚记录),这是范围查询扫到末尾的典型标志
另外,用 SELECT * FROM performance_schema.data_locks(MySQL 8.0+)可实时查看当前各事务持有的锁,配合 data_lock_waits 查等待链。
减少这类死锁的实操建议
根本思路是降低锁粒度与冲突概率,而不是简单“避免范围查询”:
- 尽量让范围条件能命中唯一索引 + 精确值,例如把
WHERE status = 'pending' AND created_at > '2026-01-01'改为先查出 ID 列表,再用WHERE id IN (…)批量更新——前提是 ID 数量可控(几百以内),否则 IN 太大会拖慢性能或触发优化器放弃索引 - 在范围查询前,用
SELECT ... LOCK IN SHARE MODE预判并提前报错,比直接FOR UPDATE更早暴露并发冲突 - 检查是否真的需要 RR 隔离级别;若业务允许,降级到读已提交(RC)后,
Next-Key Lock退化为纯行锁(gap lock 仅在唯一索引等极少数情况保留),大幅降低死锁率 - 确保范围查询始终按相同顺序访问索引,比如强制使用
FORCE INDEX指定主键,避免优化器对同一 SQL 选择不同索引路径
最易被忽略的一点:死锁检测本身不阻塞事务,但一旦触发,至少一个事务会被回滚;而回滚后若应用层盲目重试,可能以同样顺序再次进入死锁循环。必须在重试逻辑中加入随机退避或顺序调整(如按 ID 升序重排批量更新顺序)。











