根本原因是gap lock锁定索引间隙而非数据本身,insert插入位置若落入其他事务持有的gap或next-key锁覆盖的开区间(如(5,10)),即使该位置当前为空,也会被阻塞;需通过innodb_trx与data_locks定位lock_mode为gap/next-key且lock_data包含目标值的记录。

MySQL加间隙锁会导致并发INSERT阻塞,根本原因不是数据冲突,而是你插的位置“掉进了别人划好的禁区”——哪怕那块地现在空着,InnoDB 也不让你进。
Gap Lock 怎么让 INSERT 卡住
在 REPEATABLE READ 隔离级别下,InnoDB 默认用 Next-Key Lock(记录锁 + 间隙锁)防幻读。你要插的值即使当前不存在,只要它落在某个事务已持有的 GAP 或 NEXT-KEY 锁覆盖的开区间内,就会被阻塞。
- 典型表现:
Lock wait timeout exceeded,或SHOW ENGINE INNODB STATUS里看到waiting for gap before rec insert intention -
INSERT INTO t VALUES (7, 'x')(主键等值插入)通常只加记录锁,不触发间隙锁 - 但
INSERT ... SELECT、目标表无主键、或 WHERE 条件没走索引时,InnoDB 可能退化为隐式聚簇索引扫描,间隙范围失控 - 非唯一二级索引上执行
SELECT * FROM t WHERE status = 'pending' FOR UPDATE,会锁住整个(pending, pending]对应的所有主键间隙,后续所有同条件 INSERT 都会被拦
怎么确认是 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)被锁了;你要插id = 7,就正好撞上 - 如果
LOCK_DATA是NULL或空值,大概率是全表扫描导致的隐式间隙锁,说明目标表缺索引
哪些 INSERT 场景最容易触发 Gap Lock 阻塞
不是所有 INSERT 都会惹上这事,关键看是否“被迫扫范围”:
-
INSERT ... SELECT:源表扫描若没走索引,≈锁表级间隙 - 目标表没有主键或唯一索引:InnoDB 用隐式聚簇索引,间隙锁覆盖整个主键空间
- 在非唯一二级索引字段上做范围查询(如
WHERE status = 'pending')后立刻INSERT:新行极易落入已被锁住的间隙 -
ON DUPLICATE KEY UPDATE:先按唯一索引做一次SELECT FOR UPDATE级查找,锁已持,再决定插还是更
绕过 Gap Lock 的实操要点
核心思路是让 InnoDB 尽量只加记录锁,少扫、少锁间隙:
- 给常用查询条件建联合索引,比如把
(status, create_time)替代单列status索引,能让锁精准落在具体记录上,而非整个值区间 - 避免“查完就插”模式:不要
SELECT ... FOR UPDATE后紧跟INSERT;可拆成两步——先用唯一索引查出id,再用INSERT ... ON DUPLICATE KEY UPDATE - 若业务允许且能接受幻读,临时降级隔离级别到
READ COMMITTED(此时 Gap Lock 被禁用),但必须验证:binlog_format是ROW,且无 UNIQUE KEY 冲突风险 - 对空表或低数据量表并发插入,自增主键 ≠ 免疫死锁;建议改用有序单条提交,减少间隙重叠概率
真正容易被忽略的是:LOCK_DATA 显示的端点未必是你想插的值,而是相邻记录构成的开区间边界;间隙锁不依赖数据是否存在,只依赖索引结构 + 查询方式 + 隔离级别这三者的组合效果。











