INSERT ... ON DUPLICATE KEY UPDATE 本身就是原子操作,由MySQL引擎层原生保证:要么完整插入或更新一行,要么全部回滚;其执行依赖表存在PRIMARY KEY或UNIQUE索引,否则不触发更新逻辑。

为什么 INSERT ... ON DUPLICATE KEY UPDATE 本身就是原子的
它不是“实现原子性”,而是 MySQL 原生就保证这条语句整体执行的原子性——要么整条语句成功(插入或更新),要么完全回滚(比如遇到唯一键冲突但更新字段违反约束、或事务被中断)。关键前提是表必须有 PRIMARY KEY 或 UNIQUE 索引,否则 ON DUPLICATE KEY UPDATE 不会触发。
常见错误现象:语句执行后数据没变,也没报错——大概率是缺失唯一索引,或 UPDATE 子句里用了未定义的列名(如写成 SET name = VALUES(name) 但 name 不在 INSERT 列表中)。
- 只对触发冲突的那一行生效,不影响其他行
- 如果有多列唯一约束(如联合唯一索引),只要任一约束冲突就会进入
UPDATE分支 -
VALUES(col)函数只能引用INSERT子句中显式列出的列,不能引用表默认值或计算列
ON DUPLICATE KEY UPDATE 中如何安全引用新值
最容易踩的坑是误用 VALUES(col):它返回的是 INSERT 语句中该列提供的值,不是当前行旧值。想基于旧值做计算(比如计数器自增),必须显式写出列名。
例如向用户积分表插入记录并累加:
INSERT INTO user_points (user_id, points) VALUES (123, 10) ON DUPLICATE KEY UPDATE points = points + 10;
而不是 points = VALUES(points) + points——这会把新值加旧值,逻辑错乱。
-
VALUES(col)只在INSERT列表中存在该列时才有效;否则报错Unknown column 'col' in 'field list' - 若需条件更新(如仅当新值更大时才更新),用
CASE WHEN或IF()包裹,避免直接赋值 - 更新多列时,各列赋值相互独立,不共享中间状态(即不能用
col1 = col2 + 1, col2 = col1 + 1这类依赖)
事务中使用时要注意什么
这条语句在事务内执行时,MySQL 会对冲突的唯一键值加 next-key lock(间隙锁+记录锁),防止并发插入造成幻读或死锁。这意味着高并发下可能比单纯 INSERT 或 UPDATE 更容易阻塞。
- 如果表没有合适的唯一索引,语句会退化为普通
INSERT,且不加任何锁来保护“不存在”的行 - 在
READ COMMITTED隔离级别下,锁范围更小,但仍会锁定冲突的唯一值及其间隙 - 避免在大事务中频繁调用该语句,尤其当
UPDATE部分涉及子查询或函数时,可能延长锁持有时间
替代方案:什么时候不该用 ON DUPLICATE KEY UPDATE
它不适合需要严格区分“插入”和“更新”逻辑的场景,因为无法在单条语句中获取操作类型(是 insert 还是 update)。如果业务需要分别记录日志、触发不同回调或校验不同规则,应拆成 SELECT + INSERT/UPDATE 或用 REPLACE INTO(注意:REPLACE 是先删后插,会改变 AUTO_INCREMENT 值,且不满足原子性语义)。
-
REPLACE INTO会删除旧行再插入新行,触发DELETE和INSERT触发器,且外键关联行为不同 -
INSERT ... SELECT ... ON DUPLICATE KEY UPDATE中,SELECT部分若返回多行,每行单独判断冲突,不是整体原子 - MySQL 8.0+ 支持
INSERT ... ON CONFLICT DO UPDATE(PostgreSQL 风格),但 MySQL 并不支持该语法,仍须用原生ON DUPLICATE KEY UPDATE
真正要小心的是唯一索引的设计粒度——一个宽泛的联合唯一索引可能导致本意是更新 A 行,却因 B 行的索引冲突而意外更新了 B 行。确认索引覆盖的是你真正想“去重”的业务维度。


















