insert被阻塞实为间隙锁(gap或next-key锁)所致,而非插入意向锁本身互斥;需通过performance_schema.data_locks定位lock_data覆盖目标值的gap锁,并结合innodb_trx找到未提交事务。

INSERT卡在INSERT intention lock,其实不是它在阻塞你
看到SHOW ENGINE INNODB STATUS里显示“WAITING FOR THIS LOCK TO BE GRANTED: INSERT intention lock”,别急着优化INSERT语句——插入意向锁本身不互斥,真正拦路的是别的事务持有的GAP或NEXT-KEY锁。
常见错误现象是:查INNODB_TRX发现一堆RUNNING事务,但没一个明显在改数据;查performance_schema.data_locks却能看到某行LOCK_MODE含GAP,且LOCK_DATA覆盖你要插的值(比如你要插id = 105,它锁着(100, 110))。
- RR隔离级别下,
SELECT ... FOR UPDATE或UPDATE ... WHERE range_cond未提交,会持续持有所扫描范围的GAP锁 - 空表执行
SELECT * FROM t WHERE id = 5 FOR UPDATE会锁(−∞, +∞)整个主键空间 - 唯一键冲突后事务没显式
ROLLBACK,InnoDB可能残留未清理的锁等待链
用performance_schema.data_locks精准定位挡路的GAP锁
MySQL 8.0+已废弃INNODB_LOCKS,必须用performance_schema.data_locks查真实锁状态。重点过滤LOCK_TYPE = 'RECORD'且LOCK_MODE匹配GAP或NEXT-KEY的行。
执行这条语句:
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) - 你要插
id = 105,而某行LOCK_DATA是100, 110→ 就是它 -
ENGINE_TRANSACTION_ID对应INNODB_TRX.TRX_ID,可顺藤摸瓜找到未提交事务
INSERT IGNORE和ON DUPLICATE KEY UPDATE也逃不开Gap锁
别以为加了IGNORE或ON DUPLICATE KEY UPDATE就安全——它们底层仍要申请插入意向锁,并触发唯一索引上的record lock检查。并发插入相同唯一键时,极易因gap lock与X lock等待形成死锁。
典型踩坑点:
- 联合唯一索引
(a,b)中b为NULL,不同事务插入(1, NULL)和(1, 2),因NULL不参与比较,但gap lock范围重叠 -
INSERT IGNORE和INSERT ... ON DUPLICATE KEY UPDATE混用,加锁顺序不一致 -
REPLACE INTO风险更高:先删后插,需同时持有旧行X lock和新行插入意向锁,锁范围更大、持续更久
绕过Gap锁冲突的实操选择有限但明确
根本解法不是“怎么让INSERT更快”,而是“怎么不让GAP锁被意外持有”。RC隔离级别能基本禁用Gap锁,但代价是幻读;业务层控制仍是更稳妥的路径。
- 把
SELECT ... FOR UPDATE类校验逻辑移到应用层做缓存或分布式锁,避免在DB层加范围锁 - 批量导入时,按主键/唯一键排序后再插入,减少间隙重叠概率
- 确认是否真需要RR级别:若无强一致性要求,
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED可直接规避大部分Gap锁问题 - 监控长期未提交事务:
SELECT * FROM information_schema.INNODB_TRX WHERE TRX_STATE = 'RUNNING' AND TRX_STARTED
Gap锁机制本身没毛病,问题总出在“本不该持锁的地方持了太久”——查锁、砍长事务、收窄查询范围,这三件事做完,INSERT意向锁等待自然消失。











