INSERT被Gap锁阻塞是因为插入位置落在其他事务已锁定的索引间隙(如(5,10))内;InnoDB在REPEATABLE READ下默认用Next-Key Lock防幻读,即使值不存在也会阻塞;可通过information_schema.INNODB_TRX与performance_schema.data_locks定位具体Gap锁及LOCK_DATA边界,建精准联合索引、避免函数操作、慎用INSERT...SELECT可有效减少间隙锁范围。

为什么INSERT会被Gap锁卡住
不是数据冲突,是插入位置落在了别人已锁定的索引间隙里。InnoDB在REPEATABLE READ隔离级别下默认用Next-Key Lock(行锁 + 间隙锁)防幻读,哪怕你要插的值当前不存在,只要它处于某个事务已加了GAP或NEXT-KEY锁的开区间内,就会被阻塞。
典型表现:INSERT一直不返回、超时报Lock wait timeout exceeded,或者SHOW ENGINE INNODB STATUS里看到waiting for gap before rec insert intention waiting。
- 主键/唯一索引等值插入(如
INSERT INTO t VALUES (5, 'x'))通常只加记录锁,不触发间隙锁 - 但
INSERT ... SELECT、带子查询的插入、或目标表没有主键/合适索引时,InnoDB可能退化为隐式聚簇索引扫描,间隙范围更难控制 - 非唯一二级索引上的范围查询(如
WHERE status = 'pending')会锁住整个匹配区间,后续同区间插入全被拦住
怎么快速定位是哪个Gap锁在拦路
别猜,直接查系统表。先看谁在等锁:
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)已被占用。
-
performance_schema.data_locks是MySQL 8.0+的替代方案,INNODB_LOCKS已废弃 - 如果
LOCK_DATA显示NULL或空值,大概率是全表扫描导致的隐式间隙锁,说明缺少索引 - 配合
EXPLAIN FORMAT=tree确认当前插入语句是否走了索引、是否触发了Using index condition
建什么索引能减少Gap锁范围
核心是让InnoDB能“精准定位”,避免扫描和间隙锁定。不是越多索引越好,而是要让写操作尽量走唯一路径。
- 给
INSERT ... SELECT中的WHERE条件字段建联合索引,且把高频过滤字段放最左,例如WHERE user_id = ? AND status = ?→ 建INDEX idx_uid_status (user_id, status) - 删除低频、高重复值的单列索引(如只有
'pending'/'done'/'cancel'三值的status索引),它产生的间隙锁范围大、竞争强 - 避免对索引字段做函数操作:比如
WHERE DATE(created_at) = '2024-01-01'会让索引失效,被迫全表扫描并锁所有间隙 - 批量插入优先用
INSERT INTO t VALUES (), (), ()而非INSERT INTO t SELECT ...,前者可合并加锁,后者逐行处理、锁序更难控
还有哪些容易被忽略的细节
调整索引只是第一步,真正上线前必须验证副作用。
比如把idx_status_created改成idx_created_status,虽然降低了插入死锁概率,但原来WHERE status = ?的查询可能从ref退化成range甚至index,key_len和rows都得重看。
- 所有索引变更后,必须跑一遍
EXPLAIN对比关键查询的执行计划 -
READ COMMITTED隔离级别可禁用间隙锁,但需确保binlog_format = ROW,否则主从不一致 - 乐观锁(
UPDATE ... WHERE version = ?)完全绕过间隙锁,适合允许重试的业务场景,但要注意版本号不能长期滞留











