一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

如何处理MySQL唯一索引冲突引发的死锁?

时间:2026-08-31 20:56:48 编辑:袖梨 来源:一聚教程网

INSERT IGNORE 更易死锁是因为冲突时仍持插入意向锁,与唯一索引锁冲突;ON DUPLICATE KEY UPDATE 需避免空更新;根治方案是先 SELECT ... FOR UPDATE 显式加锁再操作。

INSERT IGNORE 为什么反而更容易死锁?

它不加锁就插入,看起来很轻量,但实际在冲突时会触发隐式锁行为:InnoDB 先尝试插入,发现 ERROR 1062 后回退,过程中仍会持有插入意向锁(Insert Intention Lock),而这个锁和唯一索引上的 S/X 锁存在兼容性冲突。多个事务并发执行 INSERT IGNORE 到同一唯一键范围(比如手机号、订单号),极易因锁等待顺序错乱形成循环依赖。

常见错误现象:Deadlock found when trying to get lock 报错频繁,且往往集中在某个热点唯一键值附近(如 uk_value = 12345)。

  1. 不是“没锁”,而是锁得短、不可控、不显式
  2. 无法通过 SELECT ... FOR UPDATE 提前占位,因为 INSERT IGNORE 不触发一致性读也不阻塞其他事务查该键
  3. MySQL 8.0+ 中仍存在,与隔离级别无关(RR 和 RC 下都可能发生)

ON DUPLICATE KEY UPDATE 要怎么写才不踩坑?

它比 INSERT IGNORE 更可控,但默认行为有陷阱:如果 UPDATE 子句中所有字段值与当前行完全一致(比如只写 updated_at = NOW(),但时间精度相同导致无实际变更),InnoDB 可能跳过行锁优化,导致后续语句仍竞争同一间隙或记录锁。

实操建议:

  1. 必须显式指定至少一个字段更新,哪怕只是 updated_at = updated_at + 0version = version + 1
  2. 避免空 UPDATE,例如 ON DUPLICATE KEY UPDATE id = id 在某些版本下等价于无操作,锁行为异常
  3. 更新字段尽量窄——只更新业务真正需要的列,减少锁持有范围
  4. 不要用 REPLACE INTO 替代,它本质是 DELETE + INSERT,会释放原行锁再申请新锁,放大死锁窗口

应用层幂等判断 + 显式加锁才是根治方案

靠 SQL 语法绕不开锁竞争,真正降低死锁概率的方式是把“判断是否存在”提前到事务最开始,并用确定性方式锁定目标行。

典型流程:

  1. 先执行 SELECT id FROM t WHERE uk_value = ? FOR UPDATE
  2. 若查到记录,走 UPDATE;若没查到,走 INSERT
  3. 确保所有业务路径都按相同顺序访问索引(比如总是先查 uk_value,再操作其他字段)
  4. 避免在 FOR UPDATE 查询后夹杂非数据库操作(如 HTTP 调用、日志写入),否则锁持有时间拉长,冲突概率上升

注意:这个方案要求唯一键字段上有有效索引,否则 FOR UPDATE 会升级为表锁或大量间隙锁,引发更严重问题。

死锁发生后不能只看报错,要抓真实锁链

应用日志里看到 ERROR 1213 (40001) 只是结果,真正要定位的是哪两个事务、在哪条索引、以什么顺序加了哪些锁。

关键动作:

  1. 立刻执行 SHOW ENGINE INNODB STATUSG,找 LATEST DETECTED DEADLOCK
  2. 重点看 WAITING FOR THIS LOCK TO BE GRANTEDHOLDS THE LOCK(S) 对应的索引名(如 uk_value)、记录值(如 hex 6162632d3133302d737a 解码后是 abc-130-sz
  3. 检查是否多个事务在同一页(page no 相同)上争抢相邻记录,这是典型唯一索引热点冲突

最容易被忽略的一点:死锁日志里的 heap no 是页内偏移,不是主键值;同一个唯一键冲突可能分散在不同数据页,也可能挤在一页里——后者死锁密度更高,需优先拆分写入粒度。

热门栏目