不会。同一行冲突时,INSERT ... ON DUPLICATE KEY UPDATE 走“查→锁→改”单路径,两并发事务均尝试对同一唯一索引记录加X锁,后到者直接阻塞,不形成循环等待;死锁实际源于不同事务操作相邻key或多个唯一索引导致间隙锁交叉、加锁顺序不一致。

ON DUPLICATE KEY UPDATE并发更新同一行会死锁吗?
不会。同一行冲突时,INSERT ... ON DUPLICATE KEY UPDATE 不会死锁——它走的是“查→锁→改”单路径,两个并发事务都会先尝试对**同一个唯一索引记录加 X 锁**,后到的事务直接阻塞等待,不会形成循环等待。这是最安全的场景,也是该语句设计初衷所在。
为什么实际业务中还是报 Deadlock found when trying to get lock?
真正触发死锁的,几乎都不是“更新同一行”,而是:多个事务操作不同但相邻的 key,且因间隙锁(gap lock)或多个唯一索引导致加锁范围交叉、顺序不一致。典型表现是空表或低数据量下插入连续 user_id(如 1001/1002/1003),InnoDB 对 (0,1001)、(1001,1003) 等间隙加锁,事务 A 持 (0,1001),事务 B 持 (1001,1003),又同时请求对方持有的间隙,闭环形成。
- 空表或稀疏数据下,
INSERT ... ON DUPLICATE KEY UPDATE会加间隙锁 + 插入意向锁,不是只锁“目标行” - 表有多个唯一索引(如
uk_user_id+uk_phone)时,不同事务可能按不同索引路径查找,锁住不同行再互相等待 - InnoDB 检查唯一键的顺序取决于索引创建顺序,主从不一致时还可能引发复制风险(Bug #58637)
怎么让并发 INSERT ... ON DUPLICATE KEY UPDATE 安全?
核心是消除加锁顺序的不确定性,而不是减少并发量。实操上优先做这三件事:
-
删掉冗余唯一索引:只保留一个唯一约束(最好是主键),避免跨索引加锁竞争。比如
user_balance表只需PRIMARY KEY(id)和UNIQUE KEY uk_user_id(user_id)二者留其一 -
插入前归一化 key 排序:如果批量插入多条,先按
user_idASC 排序再执行,确保所有事务扫描索引的 B+ 树路径一致 -
捕获错误并有限重试:应用层必须捕获
ERROR 1213 (40001),最多重试 2–3 次,每次延迟 10–50ms 随机抖动,避免重试风暴
什么情况下该换用 SELECT + UPDATE / INSERT?
当你的业务逻辑无法控制 key 的分布(比如用户注册用随机邮箱哈希值作唯一键),或表结构已存在多个不可删的唯一索引,且死锁频发时,INSERT ... ON DUPLICATE KEY UPDATE 就不再是“一条语句解决”的便利方案,而成了隐患源头。这时更稳的做法是:
- 先
SELECT ... FOR UPDATE查是否存在,再根据结果显式UPDATE或INSERT - 把整个判断+写入逻辑包在存储过程中,由 MySQL 服务端保证原子性和加锁顺序
- 或者干脆引入应用层分布式锁(如 Redis SETNX),对 key 维度串行化,代价是增加 RT 和依赖
真正难处理的从来不是“同一行”,而是“看似无关、实则锁域交叠”的那些相邻 key 和多索引组合。锁范围比你写的 SQL 看起来大得多,得靠 SHOW ENGINE INNODB STATUS\G 里 LATEST DETECTED DEADLOCK 的持锁详情去验证,而不是靠经验猜。


















