insert被gap锁卡住并非数据冲突,而是插入位置落入其他事务已锁定的索引间隙(如(5,10));innodb在repeatable read下默认用next-key lock防幻读,即使值不存在,只要落在已锁间隙内即被阻塞。

Gap Lock 为什么会让 INSERT 卡住
不是数据冲突,是插入位置掉进了别人已锁定的索引空隙里。InnoDB 在 REPEATABLE READ 下默认用 Next-Key Lock(行锁 + 间隙锁)防幻读,哪怕你要插的值当前不存在,只要它落在某个事务已加了 GAP 或 NEXT-KEY 锁的开区间内(比如 (5, 10)),就会被阻塞。
典型表现:INSERT 一直不返回、超时报 Lock wait timeout exceeded,或者 SHOW ENGINE INNODB STATUS 里看到 waiting for gap before rec insert intention waiting。
- 主键/唯一索引等值插入(如
INSERT INTO t VALUES (5, 'x'))通常只加记录锁,不触发间隙锁 -
INSERT ... SELECT、带子查询的插入、或目标表没主键/合适索引时,InnoDB 可能退化为隐式聚簇索引扫描,间隙范围更难控制 - 非唯一二级索引上的
WHERE status = 'pending'会锁住整个匹配索引段,后续同条件插入全被拦住
怎么快速定位是哪个 Gap Lock 在拦路
别猜,直接查系统表。先看谁在等锁:
SELECT * FROM information_schema.INNODB_TRX WHERE TRX_STATE = 'LOCK WAIT';
拿到 TRX_ID 后,再关联锁等待链:
SELECT * FROM performance_schema.data_locks WHERE LOCK_TRX_ID = 'xxx';
重点看 LOCK_MODE 为 GAP 或 NEXT-KEY 的记录,以及 LOCK_DATA 字段——它显示被锁的索引值边界,比如 5, 10 代表开区间 (5, 10) 已被占用。
- 如果
LOCK_DATA显示NULL或空值,大概率是全表扫描导致的隐式间隙锁,说明缺少索引 - 配合
EXPLAIN FORMAT=tree确认当前插入语句是否走了索引、是否触发了Using index condition
READ COMMITTED 能彻底禁用 Gap Lock 吗
基本可以。在 READ COMMITTED 隔离级别下,InnoDB 默认只加 LOCK_REC_NOT_GAP(纯记录锁),不加间隙锁——这是性能提升的核心来源。但以下三点必须手动验证:
- 确认
binlog_format是ROW:SHOW VARIABLES LIKE 'binlog_format';,否则主从可能不一致 - 检查是否有
UNIQUE KEY或外键:它们会在RC下仍触发间隙锁用于冲突检测,INSERT相同唯一值时照样阻塞 - 业务代码中避免“读-判-写”逻辑(如先
SELECT count(*)再INSERT):RC下两次读可能看到不同结果,需改用INSERT ... ON DUPLICATE KEY UPDATE或加应用层幂等控制
不降级隔离级别,怎么缩小 Gap Lock 范围
Gap Lock 锁的是索引树上的“间隙”,所以索引越精准、扫描范围越小,被锁住的间隙就越少。比如单列 status 索引,WHERE status = 'pending' 会锁住整个 status='pending' 的索引段;但如果改成联合索引 (status, created_at) 并带上时间条件,就能把锁压缩到更小物理区间。
- 避免在
WHERE或ORDER BY中对字段做函数操作(如WHERE DATE(created_at) = '2026-05-01'),会导致索引失效、间隙扩大 - 联合索引顺序要匹配查询模式:高频等值过滤字段放前面,范围查询字段放后面
- 慎用
INSERT ... SELECT,尤其当SELECT部分走的是范围扫描时,它可能隐式持有更大间隙锁
真正难处理的从来不是 Gap Lock 本身,而是它和索引设计、查询写法、事务边界三者交织后产生的“不可见锁范围”。一个没走索引的 UPDATE,可能比十条显式 SELECT ... FOR UPDATE 更容易引发连锁阻塞。











