insert ignore 不能避免唯一索引死锁,因其底层仍需加插入意向锁和唯一键记录锁,并发插入相同键值时易因gap锁与x锁等待形成循环等待。

INSERT IGNORE 为什么不能避免唯一索引死锁
INSERT IGNORE 看似“跳过冲突”,但底层仍会尝试加锁。当多个事务并发插入相同唯一键值时,InnoDB 会在唯一索引上先加 INSERT intention lock(插入意向锁),再尝试获取 duplicate key record lock;若此时另一事务已持有该记录的 X lock 或 gap lock,就可能形成循环等待。
典型现象是:应用日志里没报 Duplicate entry,却出现 Deadlock found when trying to get lock 错误——说明冲突没被“忽略”,而是卡在锁等待环节了。
常见触发场景包括:
- 联合唯一索引(如
(a,b)),事务 A 插入(1, null),事务 B 插入(1, 2),因NULL不参与唯一性比较,但 gap lock 范围重叠 - 批量
INSERT IGNORE+ 单条INSERT ... ON DUPLICATE KEY UPDATE混用,加锁顺序不一致 - 唯一索引字段存在函数索引或前缀索引,导致实际匹配行为与预期不符
REPLACE INTO 的死锁风险比 INSERT IGNORE 更高
REPLACE INTO 实际执行的是“删 + 插”两步(除非满足极严苛的优化条件),这意味着它要先获取待删除行的 X lock,再申请新行的插入意向锁。当两个事务同时 REPLACE 同一唯一键时,极易出现:
- 事务 A 持有行 R1 的
X lock,等待插入 R2 的意向锁 - 事务 B 持有行 R2 的
X lock,等待插入 R1 的意向锁 - → 直接构成死锁回路
尤其在二级唯一索引(非主键)上,REPLACE 还会额外对主键索引加锁,锁范围更大、持续时间更长。线上环境应避免在高并发写入路径中使用 REPLACE INTO。
真正降低死锁概率的实操方案
不是换语法,而是控制并发粒度和锁行为:
- 对写入热点唯一键(如用户手机号、订单号),提前在应用层做幂等判断:
SELECT ... FOR UPDATE查是否存在,再决定走INSERT还是UPDATE,确保所有事务按同一顺序访问索引 - 把
INSERT IGNORE改为带重试的INSERT ... ON DUPLICATE KEY UPDATE,并显式指定更新字段(哪怕只设updated_at = updated_at),避免 InnoDB 因“无实际更新”跳过锁优化 - 检查唯一索引是否冗余:如果已有主键或联合索引能覆盖查询条件,单独建唯一索引反而扩大 gap lock 范围
- 在 RR 隔离级别下,对唯一索引字段尽量用等值查询(
=),避免IN、OR或函数包裹,否则 gap lock 可能意外扩展
死锁发生后怎么快速定位根因
别只看错误日志里的 SQL,重点查 SHOW ENGINE INNODB STATUS\G 输出中的 LATEST DETECTED DEADLOCK 区块:
- 看
TRANSACTION行的active xxx sec—— 活跃时间越长,越可能是长事务拖累 - 对比两个事务的
lock_mode X locks rec but not gap和lock_mode X locks gap before rec—— 如果一方持 gap lock,另一方等 record lock,基本可断定是唯一索引间隙冲突 - 注意
index字段名:如果是二级唯一索引名(非PRIMARY),说明死锁发生在唯一约束校验阶段,而非主键写入阶段 - 检查
mysql tables in use 1, locked 1后面的数字:若locked值远大于 1,说明语句实际锁住了多行(比如没走索引导致全表扫描)
唯一索引死锁最隐蔽的一点是:它往往不暴露在业务 SQL 里,而藏在 InnoDB 自动加的 gap lock 中——你写的 INSERT 很干净,但 MySQL 为了保证唯一性,悄悄锁了一片区间。











