高併發系統中,「判斷存在則更新,不存在則插入」是高頻需求。傳統「先 SELECT 再 INSERT/UPDATE」存在競態條件,且雙倍 RTT。MySQL 提供原子級解決方案:**唯一索引 + INSERT ... ON DUPLICATE KEY UPDATE**,單條語句完成冪等寫入,保證主鍵 ID 穩定、Binlog 幂等、觸發器語義正確。本文深度解析原理、語法變體、性能特徵、常見陷阱與生產級最佳實踐。
ER_DUP_ENTRY (1062),改為執行 UPDATE 子句INSERT INTO `device_token` (`user_id`, `device_id`, `token`, `online`, `updated_at`)
VALUES (2479031, 'dev_abc', 'tok_xyz', 1, NOW())
ON DUPLICATE KEY UPDATE
`token` = VALUES(`token`),
`online` = VALUES(`online`),
`updated_at` = VALUES(`updated_at`);
VALUES(col) 返回 VALUES 子句中該列的值,避免重複書寫。
INSERT INTO `user_stats` (`uid`, `login_cnt`, `last_login`)
VALUES (1001, 1, NOW())
ON DUPLICATE KEY UPDATE
`login_cnt` = `login_cnt` + 1,
`last_login` = VALUES(`last_login`);
INSERT INTO `user_stats` (`uid`, `login_cnt`, `last_login`) VALUES
(1001, 1, NOW()),
(1002, 1, NOW()),
(1003, 1, NOW())
ON DUPLICATE KEY UPDATE
`login_cnt` = `login_cnt` + VALUES(`login_cnt`),
`last_login` = VALUES(`last_login`);
-- 每行獨立判斷衝突並更新
INSERT INTO `target` (`id`, `data`)
SELECT `id`, `data` FROM `staging`
ON DUPLICATE KEY UPDATE `data` = VALUES(`data`);
| 特性 | ON DUPLICATE KEY UPDATE | REPLACE INTO | INSERT IGNORE |
|---|---|---|---|
| 主鍵 ID 保持 | ✅ 是 | ❌ 新 ID | ✅ 是 |
| 外鍵安全 | ✅ 安全 | ❌ 斷裂 | ✅ 安全 |
| 部分列更新 | ✅ 靈活 | ❌ 全行覆蓋 | ❌ 不更新 |
| 觸發器 | UPDATE 觸發器 | DELETE+INSERT | 不觸發 |
| Binlog 事件 | UPDATE (ROW模式幂等) | DELETE+INSERT | 不記錄/IGNORE |
| 自增 ID 消耗 | 無額外消耗 | 每次消耗 | 衝突時消耗 |
| 適用場景 | 95% Upsert | 全行覆蓋無外鍵 | 純去重、不更新 |
CREATE TABLE `device_token` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '業務主鍵',
`device_id` VARCHAR(64) NOT NULL,
`token` VARCHAR(512) NOT NULL,
`online` TINYINT NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_device` (`user_id`, `device_id`) -- 組合唯一索引觸發 ODKU
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@Repository
public interface DeviceTokenMapper {
@Insert("""
INSERT INTO device_token (user_id, device_id, token, online, updated_at)
VALUES (#{userId}, #{deviceId}, #{token}, #{online}, NOW())
ON DUPLICATE KEY UPDATE
token = VALUES(token),
online = VALUES(online),
updated_at = VALUES(updated_at)
""")
int upsert(DeviceTokenDTO dto); // 返回 1/2/0
}
INSERT ... ON DUPLICATE KEY UPDATE 執行次數 (Com_insert + Com_update)performance_schema.events_waits_summary_global_by_event_name 監控 wait/lock/table/sql/handlerVALUES(col) 僅在 ODKU 子句中有效,其他位置返回 NULLinnodb_autoinc_lock_mode=1 時,ODKU 衝突也會消耗自增值 (8.0 修復)極端寫入熱點 (如秒殺) 下,行鎖競爭導致 ODKU 吞吐下降。可採用:
-- 1. 嘗試插入 (極快,無鎖競爭)
INSERT IGNORE INTO `seckill_stock` (`goods_id`, `stock`) VALUES (1001, 10000);
-- 2. 異步/定時任務補償更新 (非實時一致性可接受)
UPDATE `seckill_stock` SET `stock` = 10000 WHERE `goods_id` = 1001 AND `stock` != 10000;
適用:庫存預扣、計數器近似、最終一致性業務。
唯一索引 + ON DUPLICATE KEY UPDATE 是 MySQL 原生、高性能、語義正確的冪等寫入方案。掌握其底層行為 (鎖、觸發器、Binlog、自增 ID)、熟練運用 VALUES()、批量語法、表達式更新,並建立監控與避坑清單,是後端工程師處理高併發寫入的核心技能。