INSERT卡住显示“waiting for INSERT intention lock”实际是在等未提交事务持有的GAP或NEXT-KEY锁,而非其他插入意向锁;根本原因是SELECT ... FOR UPDATE等语句锁住了间隙但未提交。

INSERT卡住显示“waiting for INSERT intention lock”到底在等谁
它根本不是在等另一个插入意向锁——插入意向锁之间天然兼容。你看到的阻塞,100%是某个未提交事务持有的GAP或NEXT-KEY锁挡了路。InnoDB不会让你盲目等待,而是在加插入意向锁那一刻就做校验:如果间隙已被锁,立刻阻塞或报死锁,防止幻读和锁链膨胀。
常见错觉是“INSERT语句本身有问题”,实际根源往往藏在几分钟前一条没提交的SELECT * FROM t WHERE status = 1 FOR UPDATE里——它锁住了整个匹配范围的间隙,但INNODB_TRX里只显示RUNNING,不暴露锁细节。
用performance_schema.data_locks精准定位挡路的GAP锁
MySQL 8.0+已废弃INNODB_LOCKS,必须靠performance_schema.data_locks查真实锁状态。执行这条语句:
SELECT ENGINE_TRANSACTION_ID, OBJECT_NAME, INDEX_NAME, LOCK_MODE, LOCK_DATA FROM performance_schema.data_locks WHERE LOCK_TYPE = 'RECORD' AND (LOCK_MODE LIKE '%GAP%' OR LOCK_MODE = 'NEXT-KEY');
关键看三列:
-
LOCK_DATA:显示锁住的索引值范围,如100, 110表示间隙(100, 110) -
LOCK_MODE:含GAP或直接为NEXT-KEY -
ENGINE_TRANSACTION_ID:关联INNODB_TRX.TRX_ID,顺藤摸瓜找到未提交事务
你要插id = 105,而某行LOCK_DATA是100, 110→ 就是它。
为什么INSERT IGNORE和ON DUPLICATE KEY UPDATE也逃不开Gap锁
它们底层仍要申请插入意向锁,并触发同一套间隙冲突检测逻辑。唯一键冲突后,InnoDB还会额外加隐式REC_NOT_GAP行锁做duplicate key检查——这时你看到的“waiting for INSERT intention lock”可能是误报,实际卡在record lock上。
真正危险的是并发REPLACE INTO:它先删后插,要同时持有原记录X锁和新间隙插入意向锁,极易与别人形成A→B→A死锁闭环,尤其在二级唯一索引上。
容易踩的坑:
- 空表执行
SELECT * FROM t WHERE id = 5 FOR UPDATE会锁(−∞, +∞)整个主键空间 - 唯一键冲突后事务没显式
ROLLBACK,InnoDB可能残留未清理的锁等待链 - 批量导入脚本用
SELECT ... FOR UPDATE做幂等校验,高并发时多个事务查同一范围 → 大量GAP锁堆积
怎么快速释放或绕过挡路的Gap锁
没有“绕过”一说,只有“清理源头”或“调整行为”。Gap锁不可见、不显式暴露,只能靠定位+干预:
- 找到
ENGINE_TRANSACTION_ID对应事务,用KILL终止长事务(慎用于生产) - 改应用逻辑:把
SELECT FOR UPDATE范围缩小到等值查询(=而非BETWEEN或IN),避免无谓扩大间隙 - 对写入热点字段(如手机号),提前在应用层做幂等判断:
SELECT ... FOR UPDATE查是否存在,再统一走INSERT ON DUPLICATE KEY UPDATE,确保所有事务按同一顺序访问索引 - 确认是否真需要RR隔离级别;若业务能接受RC,
innodb_locks_unsafe_for_binlog=ON可关闭Gap锁(但注意binlog格式和主从一致性)
最常被忽略的一点:Gap锁本身几乎不耗资源,但它会让插入意向锁的冲突检测变得极其敏感——一旦出现等待,说明上游已经有失控的范围锁逻辑,问题不在INSERT,而在那个忘了提交的FOR UPDATE。


















