最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
如何处理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)。
- 不是“没锁”,而是锁得短、不可控、不显式
- 无法通过
SELECT ... FOR UPDATE提前占位,因为INSERT IGNORE不触发一致性读也不阻塞其他事务查该键 - MySQL 8.0+ 中仍存在,与隔离级别无关(RR 和 RC 下都可能发生)
ON DUPLICATE KEY UPDATE 要怎么写才不踩坑?
它比 INSERT IGNORE 更可控,但默认行为有陷阱:如果 UPDATE 子句中所有字段值与当前行完全一致(比如只写 updated_at = NOW(),但时间精度相同导致无实际变更),InnoDB 可能跳过行锁优化,导致后续语句仍竞争同一间隙或记录锁。
实操建议:
- 必须显式指定至少一个字段更新,哪怕只是
updated_at = updated_at + 0或version = version + 1 - 避免空
UPDATE,例如ON DUPLICATE KEY UPDATE id = id在某些版本下等价于无操作,锁行为异常 - 更新字段尽量窄——只更新业务真正需要的列,减少锁持有范围
- 不要用
REPLACE INTO替代,它本质是DELETE + INSERT,会释放原行锁再申请新锁,放大死锁窗口
应用层幂等判断 + 显式加锁才是根治方案
靠 SQL 语法绕不开锁竞争,真正降低死锁概率的方式是把“判断是否存在”提前到事务最开始,并用确定性方式锁定目标行。
典型流程:
- 先执行
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 是页内偏移,不是主键值;同一个唯一键冲突可能分散在不同数据页,也可能挤在一页里——后者死锁密度更高,需优先拆分写入粒度。