之所以 INSERT IGNORE 更容易触发死锁,是因为在唯一索引冲突出现时,它仍会持续持有插入意向锁,并与唯一索引上的锁产生冲突;而使用 ON DUPLICATE KEY UPDATE 时,则需要特别避免“空更新”;更稳妥、也更适合线上高并发场景的根治方案,是先通过 SELECT ... FOR UPDATE 显式加锁,再执行后续写操作。

INSERT IGNORE 为什么反而更容易死锁?
很多人以为它插入时“不加锁”,看起来更轻量,但一旦发生唯一键冲突,实际上会触发隐式锁竞争:InnoDB 会先尝试插入,遇到 ERROR 1062 后再回退;在这个过程中,事务往往会持续持有插入意向锁(Insert Intention Lock)。而该锁会与唯一索引上的 S/X 锁产生兼容性冲突。当多个事务同时针对同一唯一键范围(例如手机号、订单号、业务唯一编码)执行 INSERT IGNORE 时,就很容易因为加锁顺序不一致,最终形成循环等待,导致 MySQL 死锁。
常见报错表现为:Deadlock found when trying to get lock 频繁出现,并且通常集中在某个热点唯一键值附近,例如 uk_value = 12345。
- 并不是“完全没锁”,而是加锁时间短、行为不显式、且难以控制
- 无法像普通更新流程那样通过
SELECT ... FOR UPDATE提前锁定目标,因为INSERT IGNORE不会触发一致性读,也不会阻塞其他事务查询该唯一键 - 即使在 MySQL 8.0+ 中依然可能出现,与事务隔离级别关系不大,RR 和 RC 下都存在风险
ON DUPLICATE KEY UPDATE 要怎么写才不踩坑?
相比 INSERT IGNORE,它通常更可控,也更适合处理 MySQL 唯一索引冲突场景,但默认写法里同样有容易忽略的陷阱:如果 UPDATE 子句中所有字段的新值与当前记录完全一致(例如只写了 updated_at = NOW(),但因为时间精度一致,最终并没有真实变更),InnoDB 可能会触发“无实际更新”的优化,导致后续语句依然去竞争同一个间隙锁或记录锁,从而增加死锁概率。
实操建议:
- 必须显式更新至少一个字段,即使只是
updated_at = updated_at + 0或version = version + 1 - 尽量避免空
UPDATE,例如ON DUPLICATE KEY UPDATE id = id在某些 MySQL 版本中几乎等同于无操作,可能带来异常锁行为 - 更新字段范围要尽量小——只修改业务确实需要更新的列,减少锁持有时间和影响范围
- 不要用
REPLACE INTO来替代,它本质上是DELETE + INSERT,会先释放原有行锁再重新申请新锁,反而放大死锁窗口
应用层幂等判断 + 显式加锁才是根治方案
单纯依赖 SQL 语法很难彻底绕开锁竞争。真正能降低 MySQL 唯一索引死锁概率的做法,是把“数据是否已存在”的判断前置到事务开始阶段,并通过确定性的加锁方式锁定目标记录或目标范围。
典型流程:
- 先执行
SELECT id FROM t WHERE uk_value = ? FOR UPDATE - 如果查到了记录,就执行
UPDATE;如果没查到,再执行INSERT - 确保所有业务路径都按照完全相同的顺序访问索引,例如总是先查
uk_value,再处理其他字段,避免锁顺序不一致 - 避免在
FOR UPDATE之后夹杂非数据库操作,例如 HTTP 请求、远程服务调用、复杂日志写入,否则会明显拉长锁持有时间,增加并发冲突概率
注意:这个方案的前提是唯一键字段上必须存在有效索引,否则 FOR UPDATE 可能退化为表锁,或者产生大量间隙锁,进而引发更严重的性能和并发问题。
死锁发生后不能只看报错,要抓真实锁链
应用日志中看到 ERROR 1213 (40001) 只是死锁结果,并不能直接说明根因。真正需要定位的是:到底是哪两个或多个事务、在哪个索引上、按照什么顺序申请了哪些锁,最终形成了循环依赖。
关键动作:
- 第一时间执行
SHOW ENGINE INNODB STATUSG,重点查看LATEST DETECTED DEADLOCK这一段 - 重点分析
WAITING FOR THIS LOCK TO BE GRANTED与HOLDS THE LOCK(S)中对应的索引名称(例如uk_value)以及记录值(例如hex 6162632d3133302d737a解码后为abc-130-sz) - 检查是否有多个事务在同一数据页上(
page no相同)争抢相邻记录,这通常就是典型的唯一索引热点写入冲突
一个特别容易被忽略的细节是:死锁日志中的 heap no 表示页内偏移,并不是主键值本身;同一个唯一键冲突既可能分布在不同数据页,也可能集中挤在同一页中——后一种情况往往意味着更高的死锁密度,通常需要优先考虑拆分热点写入、降低单页竞争强度。
