insert ... on duplicate key update 是原子操作,仅在主键或任意唯一索引冲突时触发更新,避免并发下select+insert的脏状态,且比replace into更安全可控。

直接用 INSERT ... ON DUPLICATE KEY UPDATE,别绕弯子写先查后插、也别用 INSERT IGNORE 或 REPLACE INTO —— 前者并发不安全,后两者副作用太隐蔽。
为什么不能用 SELECT + INSERT 判断存在性
看似逻辑清晰,实际在高并发下必然出错。两个请求几乎同时执行 SELECT,都看到“不存在”,接着都执行 INSERT,第二个必然触发唯一键冲突(ERROR 1062),但此时第一个已提交,业务状态已脏。
- 即使加了应用层锁或 Redis 分布式锁,也无法覆盖数据库事务隔离的间隙(gap lock)窗口
- MySQL 的
SELECT ... FOR UPDATE能解决,但会显著降低吞吐,且必须严格控制事务边界 - 真正原子、轻量、可预测的方案,只有依赖数据库原生的冲突处理机制
ON DUPLICATE KEY UPDATE 的行为边界必须清楚
它只响应主键或任意一个 UNIQUE 索引冲突,其他错误(如 NOT NULL 违反、外键失败、字段截断)仍会报错中止。
- 如果表有多个唯一索引(比如
uk_email和uk_phone),只要插入值命中其中任意一个,就触发UPDATE - 冲突检测顺序:优先匹配主键,再按索引定义顺序扫描;不会“选择性地”按某个索引生效
-
VALUES(column_name)是关键语法糖——它取的是本次INSERT尝试中的值,不是原记录值,避免手写冗余参数 - 受影响行数返回
0表示“冲突了,但更新字段值和原值完全一样”,不是失败,需在业务逻辑里区分处理
MyBatis 中拼接批量 ODKU 时的实操要点
XML 映射里用 <foreach></foreach> 拼多值没问题,但要注意 MySQL 单条语句长度限制和性能拐点。
- 单次批量建议控制在 500 行以内,超过易触发
max_allowed_packet错误或锁等待加剧 -
ON DUPLICATE KEY UPDATE子句里的字段赋值,统一用VALUES(col),别混用#{item.col}—— 后者在批量场景下无法绑定到对应行 - 如果目标表有自增主键,且你用的是
INSERT ... SELECT形式(如从临时表导入),确保SELECT结果中主键列不为NULL,否则可能意外触发新 ID 分配 - 不要在
UPDATE子句里写count = count + 1这类表达式——并发下结果不可靠,应改用INSERT ... ON DUPLICATE KEY UPDATE count = VALUES(count) + 1并配合行锁或乐观版本号
最容易被忽略的一点:ODKU 不是万能 UPSERT,它不改变主键值、不重置时间戳、不触发 DELETE 相关逻辑,但也因此无法替代需要“先删后插”的业务语义。如果你的场景真需要删旧插新(比如全量覆盖配置),那得明确接受 REPLACE INTO 带来的自增 ID 浪费和触发器双触发风险,而不是假装它只是“更新”。











