INSERT被间隙锁卡住是因为在RR隔离级别下,InnoDB用Next-Key Lock(行锁+间隙锁)防止幻读,插入时若目标位置落在其他事务已锁定的间隙范围内就会阻塞。

为什么 INSERT 会被间隙锁(Gap Lock)卡住
MySQL 在可重复读(RR)隔离级别下,为防止幻读,默认启用 Next-Key Lock —— 它是行锁 + 间隙锁的组合。当你执行 INSERT 时,InnoDB 不仅检查目标记录是否存在,还会检查「插入位置前后是否存在其他事务正在锁定相邻间隙」。哪怕你要插的值当前不存在,只要它落在某个活跃事务已加了间隙锁的范围里,就会被阻塞。
典型现象:INSERT 一直卡住不返回,SHOW ENGINE INNODB STATUS 里看到 waiting for gap before lock 或类似提示;或者直接报错 Lock wait timeout exceeded。
- 常见于按非唯一索引(比如
status、created_at)做范围查询后紧接着插入 - 唯一索引上的等值插入(
INSERT ... VALUES (5))通常不会触发间隙锁,但INSERT ... SELECT或带子查询的插入可能绕过优化 - 如果表没主键或没合适索引,InnoDB 会用隐式聚簇索引,间隙锁范围更难预测
怎么快速定位哪个间隙锁在拦路
关键不是猜,是查。先确认当前阻塞链:
SELECT * FROM information_schema.INNODB_TRX WHERE TRX_STATE = 'LOCK WAIT';
拿到 TRX_ID 后,再查:
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
结合 INNODB_LOCKS(注意:8.0+ 已废弃该表,改用 performance_schema.data_locks)看锁类型和涉及的索引区间。重点找 LOCK_MODE 是 GAP 或 NEXT-KEY 的记录,以及 LOCK_DATA 显示的间隙边界(如 5, 10 表示开区间 (5,10) 被锁)。
- 别只盯
blocking_trx_id,要顺藤摸瓜看它执行的 SQL —— 往往是一条未提交的SELECT ... FOR UPDATE或UPDATE带了范围条件 -
performance_schema.data_locks中LOCK_TYPE = 'RECORD'且LOCK_DATA为空,大概率是间隙锁(因为没具体行) - 用
EXPLAIN FORMAT=tree看插入语句是否走了索引;没走索引 = 全表扫描 = 更大范围的间隙锁风险
如何避免或绕过间隙锁导致的插入失败
不能一概而论“关掉间隙锁”,而是根据场景选收敛策略:
- 业务允许的话,把隔离级别降为
READ COMMITTED:该级别下间隙锁仅用于外键检查和唯一约束,普通INSERT不受干扰(但需评估幻读影响) - 确保插入字段有高效索引,尤其是
WHERE条件或ORDER BY涉及的列;缺失索引会让间隙锁覆盖整段索引区间 - 避免在事务中长时间持有范围锁:把
SELECT ... FOR UPDATE尽量靠近实际修改点,不要提前查完就干别的事 - 用
INSERT IGNORE或ON DUPLICATE KEY UPDATE替代单纯INSERT,它们在遇到唯一冲突时行为明确,且部分场景下能减少锁等待(但不解决根本间隙冲突)
哪些操作看似安全实则暗藏间隙锁风险
很多写法看着没锁,其实悄悄加了间隙锁:
-
SELECT * FROM t WHERE id > 100 FOR UPDATE:不仅锁住所有匹配行,还锁住 (100, +∞) 这个间隙 -
UPDATE t SET x=1 WHERE status = 'pending':如果status是非唯一索引,会锁住所有满足条件的行及其之间的间隙 -
INSERT INTO t SELECT ... FROM s WHERE ...:源表扫描若走范围索引,目标表插入时也可能因唯一检查触发间隙锁 - 显式开启事务后执行
SELECT再INSERT,哪怕中间没修改,只要事务未提交,间隙锁就一直挂着
最易被忽略的是:间隙锁不依赖事务是否修改数据,只依赖查询是否用了范围条件 + 当前隔离级别。一个只读事务,只要执行了带范围的 SELECT ... LOCK IN SHARE MODE,就能拦住别人的插入。


















