唯一索引仅检测冲突,不避免冲突;真正保障数据一致的是on duplicate key update等原子语句与事务控制,否则select then insert必因并发导致1062错误。

全局唯一索引本身不能“避免”冲突,它只负责检测冲突;真正避免数据错乱的,是配合原子写入语句(如 ON DUPLICATE KEY UPDATE)和合理事务控制——否则查再插、缓存预判、本地锁全无效。
为什么唯一索引 + SELECT THEN INSERT 必然失败
这是最常被误用的方案。两个并发请求几乎同时执行:SELECT 都返回“不存在”,接着都执行 INSERT,第二个必然触发 Duplicate entry 错误(MySQL 错误码 1062)。哪怕加了 Redis 分布式锁或本地 synchronized,只要数据库层没原子性保障,就存在时间窗口。
- 应用层无法靠“先查后插”实现幂等插入,这是数据库 ACID 的边界问题
- 即使加了
SELECT ... FOR UPDATE,若 WHERE 条件没走索引,InnoDB 可能升级为间隙锁甚至表锁,反而加剧阻塞 - 该模式在批量导入、注册去重、日志落库等场景下,错误率随并发线程数非线性上升
ON DUPLICATE KEY UPDATE 是高并发插入/更新的首选
它把“判断是否存在”和“写入动作”压进一条原子 SQL,在 InnoDB 层完成,不依赖应用逻辑顺序。但必须确保冲突字段已建唯一索引(主键或 UNIQUE KEY),否则语法无效。
- 适用于“存在即更新、不存在则插入”的典型场景,如用户配置同步、最后登录时间刷新、计数器累加
-
VALUES(col)引用的是本次 INSERT 子句中的值,不是原记录值,别误写成col = col + 1(那是应用层读取后计算的结果) - 避免在
UPDATE子句中写子查询或复杂函数,MySQL 不支持,且会导致执行计划无法使用索引 - 示例:
INSERT INTO user_settings (user_id, theme, updated_at) VALUES (123, 'dark', NOW()) ON DUPLICATE KEY UPDATE theme = VALUES(theme), updated_at = VALUES(updated_at);
INSERT IGNORE 和 REPLACE INTO 的隐性代价
INSERT IGNORE 看似简单,但它会静默忽略所有警告级错误,不只是唯一冲突——比如字段超长被截断、NULL 插入 NOT NULL 字段、时区转换失败等,这些错误本该报警,却被吞掉。
-
REPLACE INTO实质是DELETE + INSERT,会触发外键级联、触发器、自增 ID 跳变,还可能清空关联的二级索引缓存 - 两者都不返回影响行数的准确语义:
INSERT IGNORE对冲突行返回 0 行影响,REPLACE INTO对已存在行返回 2 行(删 1 + 插 1),业务难据此做状态判断 - 高并发下,
REPLACE INTO的 DELETE 操作可能引发间隙锁争用,尤其当唯一索引是非主键时
唯一索引设计本身的关键细节
唯一索引不是随便加个 UNIQUE 就完事。复合唯一索引的字段顺序、是否允许 NULL、是否覆盖高频查询条件,都会直接影响锁行为和性能。
- 允许 NULL 的列参与唯一索引时,MySQL 认为多个
NULL值不重复(标准 SQL 要求 NULL 不参与唯一性比较),这容易导致“看似重复实则放过”的逻辑漏洞 - 如果业务需要按邮箱去重,但又允许部分用户无邮箱(
email IS NULL),应显式拆成两个索引:一个UNIQUE(email)(email NOT NULL),另一个用其他字段兜底 - 批量插入时,若用
ON DUPLICATE KEY UPDATE,务必确认唯一索引能命中——EXPLAIN显示key列非NULL,rows接近实际冲突量,否则可能退化为全表扫描+锁等待
真正难的不是加索引,而是让每条写入 SQL 在索引约束下仍保持语义清晰、锁粒度可控、错误可区分。比如 ON DUPLICATE KEY UPDATE 中的 WHERE 条件一旦涉及函数(如 UPPER(email)),就算你建了函数索引,InnoDB 也可能无法在冲突检测阶段复用它——这时候唯一索引就形同虚设。











