ON DUPLICATE KEY UPDATE 会锁所有扫描到的唯一索引记录及其间隙,而非仅冲突行;并发插入可能因多唯一索引加锁顺序不一致导致死锁,需通过统一访问顺序、减少唯一约束、捕获死锁后重试来缓解。
它不是只锁“将要插入的那条记录”,而是先按 INSERT 路径走:对唯一索引(PRIMARY KEY 或 UNIQUE KEY)匹配的冲突行加 INSERT intention lock,再升级为 X lock;如果没冲突,则对插入位置加 gap lock 或 next-key lock。关键点在于——只要涉及唯一索引查找,InnoDB 就会对**所有扫描到的唯一索引记录及其间隙**加锁,哪怕最终只更新其中一条。
常见误判是认为“只锁冲突行”,实际中:INSERT ... ON DUPLICATE KEY UPDATE 在唯一索引上执行时,会触发和 SELECT ... FOR UPDATE 类似的加锁行为,尤其当唯一索引非主键、或存在多个唯一约束时,锁范围可能意外扩大。
典型死锁场景:事务 A 和 B 同时执行相同语句,但扫描/加锁顺序不同(比如因索引 B+ 树分裂、页分裂导致遍历路径不一致),或它们分别命中了不同唯一索引(如一个走 uk_email,另一个走 uk_phone),就可能形成循环等待。
uk_email 上的某条记录 X,再尝试获取 uk_phone 上的记录 Yuk_phone 上的 Y,再尝试获取 uk_email 上的 XMySQL 无法预判这种跨索引的锁依赖,只能在加锁失败时检测并回滚其中一个事务。这类死锁不会报 Lock wait timeout,而是直接返回 Deadlock found when trying to get lock。
不影响加锁范围,只影响是否释放锁。无论 UPDATE 子句有没有实际修改字段值(比如 SET status = status),只要语句进入 UPDATE 分支,就会持有被更新行的 X lock 直到事务结束。但注意:UPDATE 中引用的非唯一字段(如普通二级索引列)不会额外加锁,除非该字段出现在 WHERE 条件里且触发了索引扫描。
一个易忽略点:ON DUPLICATE KEY UPDATE 的 UPDATE 部分不支持子查询或函数调用(如 SET ts = NOW() 是允许的,但 SET val = (SELECT ...) 会报错),这限制了部分动态赋值场景,也间接减少了因子查询引入的额外锁。
核心思路是让并发操作尽可能按相同顺序访问索引,减少不确定性。具体可做:
INSERT 的值在唯一索引上有稳定排序(例如插入前对 key 做哈希或归一化处理,避免随机字符串
REPLACE INTO 替代?不行——它本质是 DELETE + INSERT,锁更重、还可能触发外键级联和触发器,死锁风险更高Deadlock found when trying to get lock 后退避重试(指数退避,最多 3 次)真正难调试的是那些不常复现的间隙锁竞争——比如两个事务恰好落在同一个 gap 区间内插入不同值,又都触发了 ON DUPLICATE 分支,此时锁行为高度依赖当前索引页状态,连 SHOW ENGINE INNODB STATUS 里的 TRANSACTIONS 都不一定能还原全貌。