确认INSERT被Gap Lock卡住需先查INNODB_TRX中TRX_STATE='LOCK WAIT'的事务,再用其TRX_ID查data_locks中LOCK_MODE为GAP或NEXT-KEY且LOCK_DATA显示如(100,110)区间的记录;若LOCK_DATA为NULL,则可能是无主键或索引失效导致全范围间隙锁。

怎么确认INSERT正被Gap Lock卡住
别等超时再翻错误日志——Lock wait timeout exceeded只是表象,真正要查的是锁等待链。直接执行:SELECT * FROM information_schema.INNODB_TRX WHERE TRX_STATE = 'LOCK WAIT',拿到阻塞事务的TRX_ID后,立刻查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)被锁;你要插id = 105,就正好撞上。若LOCK_DATA为NULL,大概率是表没主键或索引失效,InnoDB退化成全范围扫描,间隙锁覆盖(−∞, +∞)。
哪些SQL最容易引发Gap Lock连锁阻塞
不是所有查询都危险,关键看是否触发Next-Key Lock且没走唯一索引:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
-
SELECT * FROM t WHERE status = 'pending' FOR UPDATE(status是非唯一二级索引)→ 锁整个匹配值对应的所有主键间隙 -
UPDATE t SET x=1 WHERE create_time > '2024-01-01'(create_time无索引或索引失效)→ 全表扫描 → 锁(−∞, +∞) -
SELECT id FROM t WHERE id > 100 ORDER BY id LIMIT 1 FOR UPDATE→ 即使只取一行,仍锁(100, next_id)这个间隙 - 空表执行
SELECT * FROM t WHERE id = 5 FOR UPDATE→ 直接锁(−∞, +∞),后续任何INSERT都可能被拦
绕过Gap Lock的实操手段
核心目标是让InnoDB尽量只加记录锁,避免扫间隙:
- 给高频范围查询字段建联合索引,比如把
(status, id)替代单列status索引,锁能精准落到具体记录,而非整个status值区间 - 避免“查完就插”模式:不要
SELECT ... FOR UPDATE后紧跟INSERT;改用先查出id,再用INSERT ... ON DUPLICATE KEY UPDATE或INSERT IGNORE - 业务允许且能接受幻读时,临时将隔离级别降为
READ COMMITTED——此级别下Gap Lock被禁用,但注意INSERT仍可能因唯一键冲突重试 - 对空表或低数据量表并发插入,自增主键不等于免疫死锁;建议改用有序单条提交,减少间隙重叠概率
为什么SHOW ENGINE INNODB STATUS有时找不到线索
MySQL 8.0+已废弃INNODB_LOCKS,SHOW ENGINE INNODB STATUS输出的锁信息是快照且不完整,尤其在高并发下容易丢失关键上下文。它可能只显示“WAITING FOR THIS LOCK TO BE GRANTED: INSERT intention lock”,但不会告诉你谁在持锁、锁在哪段索引上。真正可靠的路径是:INNODB_TRX → data_locks → threads → events_statements_history逐层下钻。最易被忽略的是LOCK_DATA的语义:它显示的是相邻记录构成的开区间端点,不是你INSERT的目标值本身——你要插105,它锁(100, 110),这中间的偏差就是排查盲区所在。

















