INSERT IGNORE不能跳过加锁过程,因其底层仍需加插入意向锁和唯一键记录锁,并发插入相同键值时易因gap锁与X锁等待形成循环等待。

INSERT IGNORE 为什么不能跳过加锁过程
它名字里有“IGNORE”,但底层根本不会绕过锁机制。InnoDB 仍要先申请 INSERT intention lock(插入意向锁),再尝试对唯一键值加 record lock 或 next-key lock。一旦两个事务同时盯上同一个唯一值(比如都插 email = 'a@test.com'),就可能一个卡在等待对方释放 gap 锁,另一个卡在等待 record 锁 → 循环等待成立。
典型表现是:日志里没看到 ERROR 1062 (23000): Duplicate entry,却报了 Deadlock found when trying to get lock——说明冲突没被“忽略”,而是死在锁排队环节了。
- 联合唯一索引(如
(a, b))中含NULL值时,因NULL不参与唯一性比较,gap lock 范围易重叠 - 函数索引或前缀索引会让实际匹配行为偏离预期,导致锁范围扩大
- RR 隔离级别下,哪怕只用等值查询
=,只要唯一索引不是主键,InnoDB 仍可能加 next-key lock
ON DUPLICATE KEY UPDATE 的隐式锁升级风险
这个语法看似安全,实则是死锁高频区。当两个事务并发执行 INSERT ... ON DUPLICATE KEY UPDATE 到同一唯一键时,InnoDB 会先加 gap 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 循环折腾,解决不了本质问题。
- 对写入热点唯一键(如手机号、订单号),必须在应用层做幂等判断:
SELECT ... FOR UPDATE查是否存在,再决定 INSERT 或 UPDATE - 所有多表事务必须约定全局统一的表访问顺序(例如总是
users → orders → order_items),否则交叉更新必死锁 - 检查唯一索引是否冗余:如果已有联合索引能覆盖查询条件,单独建唯一索引反而扩大 gap lock 范围
最常被忽略的一点:唯一索引字段上用了函数(如 UPPER(email))或前缀(如 email(32)),会导致锁行为与业务预期严重脱节——这时看执行计划都没用,得直接查 SHOW ENGINE INNODB STATUS 里的锁记录细节。


















