根本原因在于innodb在repeatable read隔离级别下,为防止幻读和保证唯一性,执行insert on duplicate key update时必须对冲突唯一索引值所在间隙加next-key lock;该锁是冲突检测的强制环节,非可选行为。

INSERT ON DUPLICATE KEY UPDATE 为什么会在唯一键上加间隙锁
根本原因在于:InnoDB 在 REPEATABLE READ 隔离级别下,执行该语句时会先尝试插入,若检测到唯一键冲突(Duplicate entry),就必须对**冲突索引值所在间隙**加 Next-Key Lock(即记录锁 + 间隙锁),以防止其他事务在该间隙插入“看似不冲突但实际会干扰判断”的数据。这个加锁动作不是可选的,而是唯一键冲突检查的强制环节。
比如表有唯一索引 uk_order_sn,当前已有 'SN_1001' 和 'SN_1005',此时并发插入 'SN_1003':所有事务都会在 (SN_1001, SN_1005) 这个间隙申请 S 类型的 Next-Key Lock —— 它们互相兼容,但若其中某个事务中途回滚或失败,剩余事务可能因锁等待顺序错乱而触发死锁检测。
三事务并发插入相同唯一值时的典型死锁链
这是最易复现的生产场景:三个事务几乎同时执行 INSERT INTO t (order_sn) VALUES ('SN_10086') ON DUPLICATE KEY UPDATE status = 1,且 order_sn 是唯一索引。
- Tx1 成功插入,持有该行的隐式 X 锁和对应间隙的 Next-Key Lock
- Tx2 和 Tx3 均检测到冲突,各自尝试获取
SN_10086所在间隙的 S 类 Next-Key Lock → 两者都阻塞在 Tx1 的锁上 - Tx1 异常回滚 → 释放所有锁
- Tx2 和 Tx3 同时被唤醒,转而竞争对该行的 X 锁(用于后续 UPDATE)→ 但此时它们各自已持有一个 S 类 Next-Key Lock,且加锁范围重叠,InnoDB 判定为循环等待,随机回滚其中一个
日志中常见报错就是 Deadlock found when trying to get lock; try restarting transaction,而 SHOW ENGINE INNODB STATUS 里会看到 lock_mode S locks gap before rec 或 lock_mode X locks gap before rec insert intention waiting。
为什么多个唯一索引会让问题更严重
当表上有多个唯一索引(如主键 + uk_phone + uk_email),INSERT ... ON DUPLICATE KEY UPDATE 的行为变得不确定:
- InnoDB 检查唯一键的顺序取决于索引创建顺序,不是按字母或定义顺序,而是按
CREATE INDEX时间先后 - 不同事务可能因检查顺序差异,最终锁定不同的索引路径上的不同间隙
- 主从复制若用基于语句的 binlog(SBR),主库和从库索引创建时间不一致,会导致同一语句在两边加锁范围不同 → 不仅死锁概率上升,还可能引发主从数据不一致
官方明确标记这类多唯一键场景为 unsafe(见 Bug #58637),不建议在高并发幂等写入中依赖它。
真正有效的绕过方式不是重试,而是控制锁粒度
靠应用层捕获死锁错误后重试,只是掩盖问题,不能降低锁冲突概率。关键是要让 INSERT 动作不触发间隙锁竞争:
- 降级隔离级别到
READ COMMITTED:全局或会话级执行SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,InnoDB 会禁用间隙锁,只锁实际插入/更新的行(代价是允许幻读) - 改用
INSERT IGNORE:冲突时静默跳过,不加 S 锁,也不触发 UPDATE;适合“只插入、不关心是否更新”的场景 - 预占位 + 显式唯一约束:确保业务字段有
UNIQUE KEY,直接走INSERT ... ON DUPLICATE KEY UPDATE,避免先SELECT FOR UPDATE再INSERT的两阶段锁 - 避免空表或极低数据量下的高频并发插入:空表时整个主键范围是
(-∞, +∞),所有 INSERT 都挤在同一个间隙争锁,是最坏情况
间隙锁本身不可见、不显式出现在 SHOW PROCESSLIST 中,排查时容易误判为“没锁”,必须结合 SHOW ENGINE INNODB STATUS 的 TRANSACTIONS 和 LOCK WAIT 段落交叉分析——这点最容易被忽略。











