UPDATE在RR下加间隙锁是防幻读的刚性行为,即只要WHERE条件走索引且能界定范围(如范围查询、非唯一索引等值匹配、唯一索引等值但记录不存在),InnoDB必按next-key锁规则加间隙锁,以阻止其他事务插入新行。

UPDATE 在 RR 下加间隙锁是防幻读的刚性行为,不是误配置
MySQL 的 UPDATE 在 REPEATABLE READ 隔离级别下加间隙锁,根本目的只有一个:阻止其他事务在查询范围内插入新行,从而杜绝幻读。这不是可选逻辑,而是 InnoDB 实现 RR 语义的底层机制——只要 WHERE 条件走索引、能界定范围、且实际没命中记录(或命中的是非唯一索引等值匹配),InnoDB 就会按 next-key 锁规则加锁,其中必然包含间隙部分。
哪些 UPDATE 场景一定会触发间隙锁
常见错误是以为“只更新存在的行就不会锁间隙”,其实只要满足以下任一条件,UPDATE 就大概率加间隙锁:
-
WHERE条件使用范围操作符(>、<、BETWEEN、LIKE 'abc%'),即使当前表中无任何匹配数据,也会锁定扫描覆盖的整个索引区间 - 对**非唯一索引**做等值更新,如
UPDATE t SET status = 1 WHERE name = 'pending',哪怕只有一行匹配,也会锁住该值前后的间隙 - 对**唯一索引**做等值更新但记录不存在,如
UPDATE t SET x = 1 WHERE id = 5,而id = 5不存在,则锁定 (3, 7) 这类相邻索引值之间的 gap -
WHERE条件没走索引 → 全表扫描 → 每行加记录锁 + 聚簇索引页间隐式间隙锁,效果接近全表阻塞
为什么 INSERT 被阻塞?关键在插入意向锁冲突
间隙锁本身是兼容的(多个事务可同时持有同一 gap 的 gap lock),但它和插入意向锁(insert intention lock)互斥。当两个事务都执行了 SELECT * FROM t WHERE id = 5 FOR UPDATE(id = 5 不存在),它们各自在 (3, 7) 上加了间隙锁;随后又都尝试 INSERT INTO t VALUES (5, ...),这时双方都要申请插入意向锁,却都在等对方释放间隙锁——死锁立即形成。你看到的 Deadlock found when trying to get lock,日志里通常带 lock_mode X locks gap before rec 和 insert intention waiting。
绕过间隙锁的实操路径很窄,且有代价
想让 UPDATE 不加间隙锁,只有两条路可走,且都需权衡业务风险:
- 把隔离级别降为
READ COMMITTED:此时间隙锁被禁用,UPDATE只锁实际命中的行(存在即锁,不存在不锁),但代价是允许幻读——比如你先查出 10 条待处理订单,另一事务插入第 11 条,你后续再查就可能看到它 - 确保
WHERE条件严格命中**唯一索引 + 等值 + 记录存在**:例如UPDATE t SET a = 1 WHERE id = 100,id是主键或 UNIQUE KEY,且该行真实存在,此时只加 record lock,不碰间隙
别依赖 LOCK IN SHARE MODE 或改写 SQL 形式来“避开”间隙锁——只要在 RR 下走范围扫描或非唯一索引,InnoDB 就会按 next-key 规则落锁,和你是 UPDATE 还是 SELECT ... FOR UPDATE 无关。


















