MySQL执行Insert时为何常遇死锁?间隙锁GapLock在流程中如何触发?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1153个文字,预计阅读时间需要5分钟。
在InnoDB中,`INSERT`语句本身不会直接加间隔锁,但会在插入前检查唯一性、定位插入位置时触发间隔锁。必须确保新记录不会破坏索引顺序或违反唯一约束。因为这将导致在目标位置的前后尝试加锁——如果该间隔锁已被其他事务使用`SELECT ... FOR UPDATE`或`UPDATE`锁定,则当前`INSERT`会阻塞,形成等待链**。
常见现象是:事务 A 执行 SELECT id BETWEEN 10 AND 20 FOR UPDATE,事务 B 紧接着执行 INSERT INTO t VALUES (15, ...),后者卡住不动,SHOW ENGINE INNODB STATUS 显示 “waiting for gap lock”。
- 只有主键或唯一索引列参与插入判断时,才会触发间隙锁(非唯一索引也可能,但逻辑更复杂)
- 无索引字段上的 Insert 不会触发间隙锁,但可能退化为表锁(极危险)
- 即使 Insert 的值在表中“不存在”,只要它落在已被锁定的间隙内,就会被拦住
死锁不是 Insert 和 Insert 之间发生的
死锁通常发生在 **Insert 与范围查询/更新事务的交叉等待** 中,而不是两个 Insert 直接抢同一个间隙。典型模式是:
事务 A:执行 SELECT * FROM t WHERE age > 20 FOR UPDATE → 锁住 (20, +∞) 间隙
事务 B:执行 INSERT INTO t (age) VALUES (25) → 等待该间隙释放
事务 A:又执行一条依赖事务 B 已持有锁的操作(比如查某个二级索引后更新),而事务 B 在插入前又需要读该二级索引 → 双方互相等待
- Insert 是“被动触发者”,真正持锁的是前面的 SELECT/UPDATE
- 死锁检测器(InnoDB deadlock detector)发现循环等待后,会回滚其中一个事务(通常是 Insert 所在事务,因代价小)
-
innodb_print_all_deadlocks = ON能捕获完整死锁日志,关键看 “TRANSACTION” 段里谁在等哪个 lock_mode=515(即LOCK_GAP + LOCK_X)
唯一索引 vs 非唯一索引对 Insert 死锁的影响
索引类型决定间隙锁的粒度和冲突概率:
对主键或唯一索引:InnoDB 能精确定位插入点,只锁紧邻间隙(如相邻记录是 10 和 20,则只锁 (10, 20))。冲突范围小,但一旦命中就必然阻塞。
对非唯一索引(如普通 INDEX(age)):InnoDB 必须扫描所有相同 age 值的记录,并对每个匹配区间的前后都加间隙锁。例如 INSERT ... age = 25 可能同时锁住 (20, 25) 和 (25, 30),甚至更多——这显著扩大了死锁面。
- 唯一索引下 Insert 死锁多见于“高并发写同一范围”的场景(如秒杀订单号连续生成)
- 非唯一索引下 Insert 死锁更容易出现在“批量导入+范围查询共存”的后台任务中
- 用
EXPLAIN FORMAT=tree可确认 SQL 是否走了预期索引;没走索引的 Insert 会全表扫描并加大量间隙锁
如何快速验证是不是间隙锁导致 Insert 卡住
别猜,直接查锁状态:
在卡住的 Insert 连接还活着时,执行:SELECT * FROM performance_schema.data_locks WHERE LOCK_TRX_ID = 'xxx';(把 xxx 替换为被阻塞事务 ID)
重点关注 LOCK_MODE 字段:若含 GAP,且 LOCK_DATA 显示类似 “(10, 20)” 或 “supremum pseudo-record”,就是间隙锁没跑了。
- 配合
SELECT * FROM information_schema.INNODB_TRX找出运行时间长、状态为LOCK WAIT的事务 - 注意:READ COMMITTED 隔离级别下
LOCK_MODE永远不会出现 GAP,这是最快速的排除法 - 如果看到多个事务的
LOCK_DATA区间重叠(如都含 (15, 25)),基本可断定是间隙锁交叉导致的死锁温床
间隙锁的“不可见性”是最容易被忽略的——它不锁数据行,不拦 UPDATE/DELETE 现有记录,却能让 INSERT 在毫无报错的情况下无限期挂起。排查时紧盯 LOCK_MODE 和区间端点,比翻业务日志高效得多。
本文共计1153个文字,预计阅读时间需要5分钟。
在InnoDB中,`INSERT`语句本身不会直接加间隔锁,但会在插入前检查唯一性、定位插入位置时触发间隔锁。必须确保新记录不会破坏索引顺序或违反唯一约束。因为这将导致在目标位置的前后尝试加锁——如果该间隔锁已被其他事务使用`SELECT ... FOR UPDATE`或`UPDATE`锁定,则当前`INSERT`会阻塞,形成等待链**。
常见现象是:事务 A 执行 SELECT id BETWEEN 10 AND 20 FOR UPDATE,事务 B 紧接着执行 INSERT INTO t VALUES (15, ...),后者卡住不动,SHOW ENGINE INNODB STATUS 显示 “waiting for gap lock”。
- 只有主键或唯一索引列参与插入判断时,才会触发间隙锁(非唯一索引也可能,但逻辑更复杂)
- 无索引字段上的 Insert 不会触发间隙锁,但可能退化为表锁(极危险)
- 即使 Insert 的值在表中“不存在”,只要它落在已被锁定的间隙内,就会被拦住
死锁不是 Insert 和 Insert 之间发生的
死锁通常发生在 **Insert 与范围查询/更新事务的交叉等待** 中,而不是两个 Insert 直接抢同一个间隙。典型模式是:
事务 A:执行 SELECT * FROM t WHERE age > 20 FOR UPDATE → 锁住 (20, +∞) 间隙
事务 B:执行 INSERT INTO t (age) VALUES (25) → 等待该间隙释放
事务 A:又执行一条依赖事务 B 已持有锁的操作(比如查某个二级索引后更新),而事务 B 在插入前又需要读该二级索引 → 双方互相等待
- Insert 是“被动触发者”,真正持锁的是前面的 SELECT/UPDATE
- 死锁检测器(InnoDB deadlock detector)发现循环等待后,会回滚其中一个事务(通常是 Insert 所在事务,因代价小)
-
innodb_print_all_deadlocks = ON能捕获完整死锁日志,关键看 “TRANSACTION” 段里谁在等哪个 lock_mode=515(即LOCK_GAP + LOCK_X)
唯一索引 vs 非唯一索引对 Insert 死锁的影响
索引类型决定间隙锁的粒度和冲突概率:
对主键或唯一索引:InnoDB 能精确定位插入点,只锁紧邻间隙(如相邻记录是 10 和 20,则只锁 (10, 20))。冲突范围小,但一旦命中就必然阻塞。
对非唯一索引(如普通 INDEX(age)):InnoDB 必须扫描所有相同 age 值的记录,并对每个匹配区间的前后都加间隙锁。例如 INSERT ... age = 25 可能同时锁住 (20, 25) 和 (25, 30),甚至更多——这显著扩大了死锁面。
- 唯一索引下 Insert 死锁多见于“高并发写同一范围”的场景(如秒杀订单号连续生成)
- 非唯一索引下 Insert 死锁更容易出现在“批量导入+范围查询共存”的后台任务中
- 用
EXPLAIN FORMAT=tree可确认 SQL 是否走了预期索引;没走索引的 Insert 会全表扫描并加大量间隙锁
如何快速验证是不是间隙锁导致 Insert 卡住
别猜,直接查锁状态:
在卡住的 Insert 连接还活着时,执行:SELECT * FROM performance_schema.data_locks WHERE LOCK_TRX_ID = 'xxx';(把 xxx 替换为被阻塞事务 ID)
重点关注 LOCK_MODE 字段:若含 GAP,且 LOCK_DATA 显示类似 “(10, 20)” 或 “supremum pseudo-record”,就是间隙锁没跑了。
- 配合
SELECT * FROM information_schema.INNODB_TRX找出运行时间长、状态为LOCK WAIT的事务 - 注意:READ COMMITTED 隔离级别下
LOCK_MODE永远不会出现 GAP,这是最快速的排除法 - 如果看到多个事务的
LOCK_DATA区间重叠(如都含 (15, 25)),基本可断定是间隙锁交叉导致的死锁温床
间隙锁的“不可见性”是最容易被忽略的——它不锁数据行,不拦 UPDATE/DELETE 现有记录,却能让 INSERT 在毫无报错的情况下无限期挂起。排查时紧盯 LOCK_MODE 和区间端点,比翻业务日志高效得多。

