mysql在rr级别下,update/delete执行当前读且where走索引时必加next-key锁(记录锁+间隙锁)以防止幻读,这是实现隔离语义的刚性机制,非配置或优化问题。

UPDATE 或 DELETE 在 RR 下加间隙锁是防幻读的刚性行为
MySQL 在 REPEATABLE READ 隔离级别下,只要执行的是当前读(UPDATE、DELETE、SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE),且 WHERE 条件走索引、能界定范围,InnoDB 就会按 next-key 锁规则加锁——即「记录锁 + 间隙锁」组合。这不是配置错误或优化缺失,而是实现 RR 语义的底层机制。
常见误解是“只更新存在的行就安全”,但事实是:
- 对非唯一索引做等值更新(如
UPDATE t SET status=1 WHERE name='pending'),哪怕只命中一行,也会锁住该值前后的间隙 - 对唯一索引做等值更新但记录不存在(如
UPDATE t SET x=1 WHERE id=5,而id=5不存在),InnoDB 会定位到相邻索引值之间的 gap(如 (3,7)),并对整个间隙加gap lock - 使用范围条件(
>、、<code>BETWEEN、LIKE 'abc%')时,即使表中无任何匹配数据,也会锁定扫描覆盖的整个索引区间
为什么 INSERT 被阻塞?关键在插入意向锁冲突
间隙锁本身是兼容的——多个事务可以同时持有同一 gap 的 gap lock。但它和 insert intention lock(插入意向锁)互斥。这就是 INSERT 被卡住的根本原因。
典型死锁链路:
- 事务 A 执行
UPDATE t SET a=1 WHERE id=5(id=5不存在)→ 在 (3,7) 加gap lock - 事务 B 同样执行该语句 → 也在 (3,7) 加
gap lock(允许) - 两事务都尝试
INSERT INTO t VALUES (5, 'x')→ 各自申请 (3,7) 上的insert intention lock - 但该锁与对方已持有的
gap lock冲突 → 双方等待,触发Deadlock found when trying to get lock,日志里常带lock_mode X locks gap before rec和insert intention waiting
哪些写操作一定会触发间隙锁(即使没命中数据)
不是所有“没查到”都会加间隙锁,触发前提是:RR 隔离级别 + 当前读 + 查询条件走索引 + 能界定范围。典型场景包括:
-
UPDATE t SET c=1 WHERE create_time > '2026-09-01':哪怕当前无满足条件的记录,也会锁住索引中该时间点之后的所有间隙 -
DELETE FROM t WHERE status = 'timeout'(status是非唯一索引):即使只删一行,也锁前后间隙 -
INSERT INTO t (uid) VALUES (123)(uid是唯一索引):InnoDB 先加对应间隙锁防止并发重复插入,再尝试加记录锁 -
UPDATE t SET x=1 WHERE id=100(id是主键,但该行不存在):锁定 (prev_id, next_id) 区间,例如 (98,102)
想绕过间隙锁?只有两条路,且都有代价
真正让 UPDATE / DELETE 不加间隙锁的实操路径极窄:
- 把隔离级别降为
READ COMMITTED:此时间隙锁被禁用,UPDATE只锁实际命中的行(存在即锁,不存在不锁)。但代价是允许幻读——比如你先查出 10 条待处理订单,另一事务插入第 11 条,你后续再查就可能看到它 - 确保 WHERE 条件严格命中「唯一索引 + 等值 + 记录存在」:例如
UPDATE t SET a=1 WHERE id=100,其中id是主键或UNIQUE KEY,且该行真实存在。此时只加记录锁,不加间隙锁
最容易被忽略的是:即使用了唯一索引,只要条件没走索引(比如隐式类型转换、函数包裹字段),就会退化为全表扫描,每行加记录锁 + 聚簇索引页间隐式间隙锁,效果接近全表阻塞。











