insert ignore不能跳过加锁,因其底层仍需申请插入意向锁和唯一键记录锁或next-key锁,并发插入相同唯一值时易因gap锁与record锁等待形成循环等待,导致死锁。

INSERT IGNORE 为什么不能跳过加锁
它名字里有“IGNORE”,但底层根本不会绕过锁机制。InnoDB 仍要先申请 INSERT intention lock(插入意向锁),再尝试对唯一键值加 record lock 或 next-key lock。一旦两个事务同时盯上同一个唯一值(比如都插 email = 'a@test.com'),就可能一个卡在等待对方释放 gap lock,另一个卡在等待 record lock → 循环等待成立。
典型表现是:日志里没看到 ERROR 1062 (23000): Duplicate entry,却报了 Deadlock found when trying to get lock ——说明冲突没被“忽略”,而是死在锁排队环节了。
- 即使只插入一条记录,只要唯一索引不是主键,
RR隔离级别下仍可能加next-key lock - 联合唯一索引(如
(a, b))中含NULL值时,因NULL不参与唯一性比较,gap lock范围易重叠 - 函数索引或前缀索引会让实际匹配行为偏离预期,导致锁范围扩大
ON DUPLICATE KEY UPDATE 的隐式锁升级风险
这个语法看似安全,实则是死锁高频区。当两个事务并发执行 INSERT ... ON DUPLICATE KEY UPDATE 到同一唯一键时,InnoDB 会先加 gap lock + insert intention lock,再试图升级为 record lock。若双方都卡在 gap lock 阶段,又互相等待对方释放,就直接构成死锁回路。
更麻烦的是:如果 UPDATE 字段没实际变化(比如只更新 updated_at = NOW() 但时间戳精度相同),InnoDB 可能跳过锁优化逻辑,让锁行为更不可控。
- 务必确保
WHERE条件走聚簇索引(即主键),而非二级唯一索引 - 显式指定至少一个字段更新,避免因“无变更”触发锁策略降级
- 线上已发死锁时,临时方案是改用
SELECT ... FOR UPDATE显式锁住主键行,把冲突前置到读阶段
REPLACE INTO 比 INSERT IGNORE 更危险
REPLACE INTO 实际执行的是“删 + 插”两步(极少数优化场景除外)。这意味着它要先获取待删除行的 X lock,再申请新行的插入意向锁。当两个事务同时 REPLACE 同一唯一键时,极易出现:
- 事务 A 持有行 R1 的
X lock,等待插入 R2 的意向锁 - 事务 B 持有行 R2 的
X lock,等待插入 R1 的意向锁 → 直接形成死锁回路 - 二级唯一索引上
REPLACE还会额外对主键索引加锁,锁范围更大、持续时间更长 - 即使表结构简单,只要存在外键或触发器,
REPLACE就无法走优化路径,必然触发delete + insert
高并发写入路径中应彻底禁用 REPLACE INTO,改用应用层幂等控制。
真正有效的规避方式不是换 SQL 语法
所有“语法糖”(INSERT IGNORE、ON DUPLICATE KEY UPDATE、REPLACE INTO)在并发唯一键冲突场景下,都绕不开 InnoDB 底层的锁申请流程。它们只是把“冲突处理逻辑”从应用层移到了 server 层,但锁竞争本质没变。
最容易被忽略的一点是:**唯一键冲突引发的死锁,往往不是因为“写同一条记录”,而是因为“写相邻键值”或“写不同索引上的相同逻辑键”触发了重叠的 gap lock 范围。** 比如 uk_value = 5 和 uk_value = 6 在二级唯一索引上可能落在同一个 next-key lock 区间内,此时并发插入就极易形成循环等待。











