on duplicate key update 依赖表的primary key或unique索引触发冲突检测与原子更新,无约束则退化为普通insert;update部分用values(col)引用新值,不触发insert触发器,且并发易死锁。

INSERT ON DUPLICATE KEY UPDATE 依赖唯一约束才能触发原子检查
它不是靠语句里写条件来判断“有没有重复”,而是完全依赖表上已有的 UNIQUE 索引或 PRIMARY KEY。MySQL 在执行 INSERT 的瞬间,一旦发现违反这些约束,就自动转为 UPDATE,整个过程在单次引擎层操作中完成,没有应用层可见的时间窗口。
常见错误是建了表但忘了加唯一索引,结果 ON DUPLICATE KEY UPDATE 完全不生效,还误以为逻辑没跑通——其实压根没进冲突分支。
- 多列唯一索引(如
(user_id, event_type))只要任意一组值已存在,就会触发更新 -
UPDATE部分不能引用子查询、不能跨表,只能更新当前行字段 - 如果用的是自增主键,而冲突发生在其他唯一键上,
INSERT仍会消耗一个自增值(这点和INSERT IGNORE一样)
INSERT IGNORE 也能跳过冲突,但返回值和副作用不同
INSERT IGNORE 在遇到主键或唯一键冲突时不会报错,而是发一条警告、返回 affected rows = 0,适合“只管插、不关心成败”的场景,比如日志去重、缓存预热。
但它和 ON DUPLICATE KEY UPDATE 不是一个东西:前者是“静默丢弃”,后者是“冲突即更新”。选错会导致业务语义出错——比如用户注册时本该提示“账号已存在”,结果被 IGNORE 后悄无声息失败。
- 即使被忽略,自增 ID 依然递增,高并发下可能造成 ID 跳跃明显
- 某些客户端(如旧版 PHP mysqli)默认不开启
MYSQLI_CLIENT_FOUND_ROWS,导致无法区分“真插入”和“假忽略” - 不适用于需要根据冲突做差异化处理的逻辑(例如:冲突时更新最后登录时间)
别用 SELECT + INSERT 模拟唯一检查
这是最常踩的坑。先 SELECT 查是否存在,再决定是否 INSERT,看似清晰,但在并发下必然出现幻读:两个请求几乎同时查到“不存在”,然后都执行 INSERT,最终触发主键冲突或写入重复数据。
加 SELECT ... FOR UPDATE 也救不了——如果查不到记录,InnoDB 不会加任何行锁,只会在插入时才锁间隙,此时竞态早已发生。
- 悲观锁只对“已存在”的记录有效;对“将要插入”的位置,锁的是间隙(gap lock),但
SELECT本身不触发间隙锁 - 乐观锁(如 version 字段)在这里不适用,因为初始状态就是无记录,没法读出 version 做比对
- 分布式系统里,靠应用层加 Redis 锁成本高、可靠性差,且无法规避数据库层的约束校验
事务内部分语句失败 ≠ 事务原子性被破坏
很多人看到 source 执行 SQL 脚本时某条 INSERT 报 ERROR 1062 (23000): Duplicate entry,但后面语句仍继续执行,就以为“事务没回滚”。其实这是 MySQL 客户端行为:默认以语句为单位执行,单条语句失败不影响后续语句运行,和事务边界无关。
真正要保证原子性,必须显式使用 START TRANSACTION,并在所有语句后统一 COMMIT 或 ROLLBACK。否则哪怕写了 BEGIN,只要中间某条语句出错,MySQL 默认不会自动回滚已执行的成功语句。
- DDL 语句(如
CREATE TABLE)会隐式提交当前事务,切记不要混在业务事务里 - 存储过程中若未捕获异常,出错后事务状态不确定,建议用
DECLARE EXIT HANDLER显式控制 - 应用代码里用 ORM(如 Django、MyBatis)时,注意其事务传播行为是否覆盖全部操作
affected rows 的解析逻辑,不同驱动返回规则不一致,得实测验证。











