
Gap Lock 是什么,为什么它不解决幻读而是制造了新问题
MySQL 的 REPEATABLE READ 隔离级别下,InnoDB 用 Gap Lock 防止其他事务在间隙中插入新行,从而“避免幻读”。但实际中,它常导致死锁、阻塞严重,甚至让本该并发的操作串行化。根本原因在于:Gap Lock 锁的是“值之间的空隙”,不是具体记录,而这个“间隙”边界容易被索引结构、查询条件、是否命中索引等细节悄悄改变。
常见错误现象:ERROR 1205 (40001): Deadlock found when trying to get lock;明明只查一条记录,却锁住一大片范围;SELECT ... FOR UPDATE 在非唯一索引上锁住远超预期的行。
- Gap Lock 只在
REPEATABLE READ下生效,READ COMMITTED下完全禁用(此时靠 MVCC + 行锁应对幻读,但可能看到新插入行) - 只有在使用**非唯一索引或无索引扫描**时,Gap Lock 才会扩大范围;主键/唯一索引等值查询通常只锁匹配行本身(next-key lock 退化为 record lock)
- 执行
SELECT ... FOR UPDATE或UPDATE时,如果WHERE条件没走索引,会升级为全表扫描+全间隙锁定,非常危险
如何确认当前语句触发了 Gap Lock
不能光看 SQL 写法,得看执行计划和锁信息。最直接的方式是复现后查 INFORMATION_SCHEMA.INNODB_TRX 和 INFORMATION_SCHEMA.INNODB_LOCKS(MySQL 8.0+ 已移除后者,改用 performance_schema.data_locks)。
实操建议:
- 先开两个会话,一个执行带锁语句(如
UPDATE t SET x=1 WHERE a > 10 AND a ),另一个立刻执行 <code>SELECT * FROM performance_schema.data_locks - 重点看
LOCK_MODE字段:含GAP即表示存在间隙锁;NEXT-KEY是 record + gap 组合锁 - 配合
EXPLAIN FORMAT=tree看是否走了索引——没走索引的范围查询几乎必带 Gap Lock,且范围不可控
怎么绕过或缩小 Gap Lock 影响范围
不是所有场景都需要强一致性幻读防护。多数业务能接受 READ COMMITTED,或者用应用层加分布式锁+幂等来替代数据库级间隙保护。
如果必须留在 REPEATABLE READ,优先从索引和查询写法入手:
- 确保
WHERE条件命中**唯一索引或主键**,例如把WHERE name = 'xxx'改成WHERE id = 123,可让 next-key lock 退化为纯 record lock - 避免在非唯一索引上做范围查询(如
WHERE status IN (1,2)),改用等值 + 应用层分页或预加载 - 显式关闭 Gap Lock:设置
innodb_locks_unsafe_for_binlog = ON(已废弃)或直接切到READ COMMITTED—— 这是最有效也最可控的方式 - 用
SELECT ... LOCK IN SHARE MODE替代FOR UPDATE时注意:它同样会加 Gap Lock,行为一致
为什么唯一索引等值查询有时还是出现 Gap Lock
这是最容易踩坑的地方:你以为 WHERE id = 100 只锁一行,但如果 id 是唯一索引,而这条记录**实际不存在**,InnoDB 仍会对该值所在间隙加 Gap Lock —— 目的是防止其他事务插入同 id 值,破坏唯一性约束。
典型场景:
-
SELECT * FROM t WHERE id = 100 FOR UPDATE,但表中没有id = 100的行 → 锁住 (99, 101) 这个间隙 - 这种“不存在即锁间隙”的行为,在唯一约束检查、INSERT … ON DUPLICATE KEY UPDATE、REPLACE INTO 中都存在
- 想验证?执行该语句后,另一个事务尝试
INSERT INTO t (id) VALUES (100)会被阻塞
这说明 Gap Lock 的逻辑始终服务于约束完整性,而非用户直觉中的“我要锁哪几行”。只要涉及唯一性校验,间隙就逃不掉。










