gap lock在空表上会锁住整个主键空间:innodb将supremum pseudo-record作为锚点,在(-∞, +∞)加x,gap锁,阻止所有insert,因其插入意向锁与该锁冲突;此为rr级别防幻读的正常行为。

Gap Lock在空表上会锁住整个主键空间
空表执行 SELECT ... FOR UPDATE 后,InnoDB 会把 supremum pseudo-record 当作唯一锚点,在 (-∞, +∞) 上加 X,GAP 锁。这不是 bug,是 RR 隔离级别下防止幻读的强制行为。哪怕表里一条记录都没有,所有 INSERT 都会被拦住——因为插入意向锁和这个全范围 Gap 锁直接冲突。
常见误判是:“没查到数据=没加锁”,结果 INSERT 卡住却找不到源头。验证方式很简单:
SELECT OBJECT_NAME, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_DATA FROM performance_schema.data_locks WHERE OBJECT_NAME = 'your_table';看到
LOCK_MODE 是 X,GAP、LOCK_DATA 是 supremum pseudo-record,就坐实了。
INSERT本身不主动加Gap Lock,但会触发检查并被拦
INSERT 语句不会自己申请 Gap 锁,但它必须先确认插入位置是否安全:是否违反唯一约束?是否落在已被锁定的间隙内?这个检查过程会尝试获取插入意向锁(Insert Intention Lock),而该锁与已存在的 X,GAP 锁互斥。
- 事务 A 执行
SELECT id BETWEEN 10 AND 20 FOR UPDATE→ 锁住 (10, 20) - 事务 B 执行
INSERT INTO t VALUES (15, 'x')→ 尝试在 (10, 20) 加插入意向锁 → 被阻塞 - 即使
id = 15当前不存在,只要它落在 (10, 20) 内,就拦
所以不是 Insert 在抢锁,而是它“路过”时被已有 Gap 锁卡住。这也是为什么死锁链里总能看到一个事务在等 gap before lock。
唯一索引和非唯一索引对Gap Lock的影响差异很大
主键或唯一索引能让 InnoDB 精确定位插入点,只锁紧邻间隙,比如相邻记录是 10 和 20,就只锁 (10, 20);而非唯一索引(如 status 字段)可能触发更大范围的 Gap 锁,尤其当查询条件没走索引时,甚至退化为全表扫描+大范围锁。
更隐蔽的是联合索引场景:如果建了 (status, create_time) 普通索引,WHERE status = 'pending' AND create_time > '2024-01-01' 仍可能锁住大片间隙;但改成唯一联合索引(业务可保证组合值唯一),InnoDB 就可能只加记录锁,跳过 Gap 锁。
自增主键是例外:正常顺序插入几乎不触发 Gap 锁冲突,但若用时间戳生成订单号这类业务主键,并发写入非连续值,间隙重叠概率陡增。
READ COMMITTED能绕过Gap Lock,但代价明确
在 RC 隔离级别下,InnoDB 基本禁用 Gap 锁(除外键约束等极少数情况),INSERT 不再因间隙被拦。但代价是允许幻读:同一个事务中两次 SELECT 可能返回不同数量的行。
如果业务逻辑不依赖严格可重复读(比如日志类、统计类场景),降级到 RC 是最直接的解法。需同步确认 binlog_format 是 ROW,否则主从一致性可能出问题。
另一个轻量方案是改写查询:把 SELECT ... WHERE status = ? FOR UPDATE 拆成两步——先用唯一索引查出 id,再用 SELECT ... WHERE id = ? FOR UPDATE。这样只加记录锁,彻底避开 Gap 锁。
Gap 锁的“不可见性”是最容易被忽略的点:它不锁任何真实数据,却让插入彻底失效;它不报错,只沉默等待;它在空表、无数据查询时照常生效。排查时别盯着 INSERT 本身,要顺着 performance_schema.data_locks 去找那个安静躺在那里的 X,GAP。











