RC隔离级别下Gap Lock默认被禁用,仅在唯一索引冲突检测(如INSERT ON DUPLICATE KEY UPDATE)和外键约束检查(如父表范围扫描)两种例外场景中启用,目的非防幻读,而是保障约束一致性和避免死锁。

RC隔离级别下Gap Lock被禁用是默认行为,但有例外
MySQL在READ COMMITTED隔离级别下,InnoDB默认不使用Gap Lock——这是官方明确设计的取舍:用允许幻读来换更高的并发插入能力。普通范围查询、非唯一索引等值查询、UPDATE/DELETE语句都不会触发间隙锁,只对实际命中的记录加RECORD LOCK(即X,REC_NOT_GAP)。但有两个关键例外必须记住:UNIQUE索引的唯一性检查、外键约束的被引用表扫描,这两类场景仍会悄悄启用Gap Lock。
为什么UNIQUE索引冲突检测必须加Gap Lock?
哪怕隔离级别是RC,只要语句涉及唯一约束校验,InnoDB就必须提前锁定“可能插入成功”的间隙,否则两个事务同时执行INSERT ... ON DUPLICATE KEY UPDATE或REPLACE INTO,可能绕过唯一性检查导致重复键写入。例如表中只有id=100,执行INSERT INTO t (id) VALUES (99),InnoDB会锁住(-∞, 100)这个间隙,不是因为要防幻读,而是防止另一个事务也插99造成约束失效。
- 触发动作包括:
INSERT IGNORE、INSERT ... ON DUPLICATE KEY UPDATE、UPDATE中WHERE条件命中唯一索引但更新后违反约束 - 是否加锁与行是否存在无关,只取决于执行路径是否进入
row_ins_scan_sec_index_for_duplicate这类唯一性校验函数 - 多列唯一索引时,若WHERE只用其中一部分列(如索引是
(a,b),但查WHERE a = 5),仍会加Gap Lock
外键约束如何让RC也启用Gap Lock?
当子表有FOREIGN KEY指向父表时,InnoDB在RC下对父表做范围扫描(如SELECT * FROM parent WHERE id > 100 FOR UPDATE)会主动加Gap Lock,目的是阻止子表在该范围内插入新记录,从而保障后续删除父表记录时不破坏参照完整性。这个行为由innodb_locks_unsafe_for_binlog控制,默认为OFF,设为ON可禁用,但会牺牲主从一致性与外键安全性。
MySQL 数据库管理技能,能够通过自然语言查询和管理数据库,支持 SELECT/INSERT/UPDATE/DELETE、表管理、备份恢复等操作。当用户提及 MySQL、数据库查询、建表或数据备份时使用此技能。
- 典型现象:SHOW ENGINE INNODB STATUS里看到
lock_mode X,GAP且等待insert intention,基本可定位到外键或唯一约束 - 锁区间不依赖当前数据分布,比如父表最大
id=100,WHERE id > 100仍会锁(100, +supremum] - 即使父表没建索引,只要外键列参与范围条件,InnoDB仍会走全表扫描+隐式Gap Lock
怎么确认当前SQL是否触发了RC下的Gap Lock?
不能只看transaction_isolation变量,必须结合运行时锁信息判断。最可靠方式是复现问题后立刻查INFORMATION_SCHEMA.INNODB_TRX和INNODB_LOCK_WAITS,再对照SHOW ENGINE INNODB STATUS的TRANSACTIONS部分。重点关注LOCK_MODE字段:
-
X,GAP→ 纯间隙锁,基本来自唯一约束或外键 -
X,REC_NOT_GAP→ 正常记录锁,RC下多数写操作的表现 -
X(无后缀)→ Next-Key Lock,说明隔离级别实际是RR,或执行计划走了范围扫描+唯一索引退化
真正容易被忽略的是:你写的SQL看似简单,但优化器可能因缺失索引或统计信息偏差,把等值查询转成范围扫描;或者外键定义在你没注意的表上,导致锁行为完全偏离预期。

















