在mysql 5.7的rr隔离级别下,非唯一索引等值查询会加next-key锁(行锁+左侧间隙锁),如a=6时锁定(3,6];该机制易引发死锁,尤其当多事务交叉访问重叠间隙时。

非唯一索引等值查询会同时加行锁 + 间隙锁
在 MySQL 5.7 的 REPEATABLE READ 隔离级别下,对非唯一索引字段执行 SELECT ... FOR UPDATE 或 UPDATE ... WHERE 时,InnoDB 不会只锁住匹配的行,而是用 next-key lock(临键锁)——即「行锁 + 左侧间隙锁」组合。比如表有 a 字段为普通索引,数据为 (1),(3),(6),(12),(24),执行 SELECT * FROM t WHERE a = 6 FOR UPDATE,实际锁定的是区间 (3, 6]:其中 6 是行锁,(3, 6) 是间隙锁。
这个行为本身不直接导致死锁,但它是死锁发生的基础设施——因为多个事务若以不同顺序访问重叠的间隙,就容易形成循环等待。
两个事务交叉更新非唯一索引字段时极易触发循环等待
典型死锁场景是两个事务分别按不同顺序更新同一非唯一索引值范围内的记录。例如:
- 事务 A 执行
UPDATE t SET b = 1 WHERE a = 6→ 锁定(3, 6] - 事务 B 同时执行
UPDATE t SET b = 2 WHERE a = 12→ 锁定(6, 12] - 紧接着事务 A 尝试插入
a = 10(落在(6, 12)间隙内)→ 等待事务 B 释放(6, 12]的间隙锁 - 事务 B 同时尝试插入
a = 9(落在(3, 6)间隙内)→ 等待事务 A 释放(3, 6]的间隙锁
此时 InnoDB 检测到等待图成环,立即回滚其中一个事务。注意:即使插入语句没显式加锁,只要涉及 INSERT,就会先申请 insert intention lock(一种特殊的间隙锁),而它与普通间隙锁冲突。
RC 隔离级别也不能完全避免间隙锁死锁
很多人误以为切换到 READ COMMITTED 就能禁用间隙锁,其实不准确:
-
RC下确实不为普通SELECT或UPDATE加间隙锁,但仍有例外 - 外键约束检查、唯一键冲突检测(如
INSERT ... ON DUPLICATE KEY UPDATE)仍会使用间隙锁 - 如果表上有外键,或语句触发了唯一索引重复校验(哪怕字段本身是非唯一的),间隙锁依然存在
- 死锁日志里看到
GAP before rec或insert intention并不意味着隔离级别设错了,很可能是隐式约束触发的
建联合索引未必能绕过,顺序错反而更危险
有人试图用 FORCE INDEX 或新建单列索引“引导”优化器,但实际无效甚至有害:
-
FORCE INDEX (idx_a)对WHERE a = ? AND b > ?无用,除非idx_a是联合索引且满足最左前缀 - 建了
INDEX (b, a)却写WHERE a = 1 AND b > '2024',优化器可能拒绝使用,退化为全表扫描+全表行锁 - 更糟的是:如果建了
INDEX (a, b),而业务中大量执行WHERE a = ? ORDER BY b DESC LIMIT 1,InnoDB 可能因扫描方向不同,在相同间隙上加出方向相反的 next-key 锁,增加死锁概率
真正可控的解法只有两个:一是把非唯一索引字段改造成唯一约束(如加业务唯一标识拼接),二是确保所有涉及该字段的 DML 语句按固定顺序(如主键升序)批量处理,避免交叉。











