唯一索引不自动拦截重复请求,仅在insert/update时抛出sqlstate 23000错误,需应用层捕获处理;insert on duplicate key update是原子化幂等方案,要求冲突字段有唯一索引且update子句幂等;select for update仅适用于状态流转类更新,需显式事务、精确索引和影响行数校验;null值在唯一索引中不参与校验,易导致绕过约束,应设not null或用函数索引规避。

唯一索引本身不自动拦截重复请求,只在INSERT/UPDATE时抛错
很多人以为加了UNIQUE KEY就能“自动幂等”,但实际是:MySQL只在执行INSERT或UPDATE语句时校验约束,一旦语句触发Duplicate entry错误(如ERROR 1062 (23000): Duplicate entry 'xxx' for key 'uq_request_id'),应用层必须自己捕获并决定下一步——不是“自动跳过”,而是“报错后你得处理”。
- 错误类型固定为
SQLState: 23000,对应MySQLIntegrityConstraintViolationException(Java)或IntegrityError(Python) - 仅靠
INSERT INTO t(id, data) VALUES(?, ?)+ 唯一索引,无法区分“首次成功”和“重复忽略”,业务逻辑可能已部分执行(比如发消息、扣库存) - 如果事务里混了其他DML操作(如先
UPDATE account SET balance = balance - 100再INSERT),唯一索引报错时前面的操作不会自动回滚——除非整个事务被正确声明和传播
INSERT ON DUPLICATE KEY UPDATE 是最简可靠方案
它把“插入 or 更新”原子化,避免应用层判断分支,且全程在单条语句内完成约束校验与写入,天然规避竞态窗口。
- 必须确保冲突字段(如
request_id)上有UNIQUE或PRIMARY KEY索引,否则ON DUPLICATE KEY不生效 -
UPDATE子句里禁止写非幂等表达式,例如counter = counter + 1——重复请求会累加,不是幂等 - 想标记是否重复,可设
is_repeated TINYINT DEFAULT 0字段:ON DUPLICATE KEY UPDATE is_repeated = 1, updated_at = NOW() - 示例:
INSERT INTO idempotent_log (request_id, payload) VALUES ('req-abc', '{"user":123}') ON DUPLICATE KEY UPDATE updated_at = NOW();
SELECT FOR UPDATE + 事务只适用于状态流转类更新
它不适合“防重插入”,只适合“查到记录后再改状态”的场景,且极易因索引缺失或事务范围不当失效。
-
SELECT ... FOR UPDATE必须走主键或唯一索引,否则升级为间隙锁甚至表锁,拖垮并发性能 - 必须包在显式事务中(
START TRANSACTION或@Transactional),且SELECT和后续UPDATE的WHERE条件完全一致(包括状态判断) - 执行完
UPDATE后,务必检查影响行数——为0说明已被抢先更新,此时应直接返回而非重试 - 错误示范:
SELECT * FROM orders WHERE order_id = 'ORD123' FOR UPDATE;→INSERT ...(中间无记录,锁没生效,照样并发插入)
唯一索引字段允许NULL,但多个NULL不违反唯一性
这是常被忽略的陷阱:InnoDB认为NULL != NULL,所以UNIQUE INDEX (user_id)字段可以存多条user_id IS NULL的记录。若业务用NULL表示“未分配用户”,那这些记录就绕过了唯一性校验。
- 解决方案:用
DEFAULT ''替代NULL,或建函数索引(MySQL 8.0+):CREATE UNIQUE INDEX uq_nonnull ON t ((COALESCE(user_id, ''))); - 更稳妥做法:业务层禁止传
NULL,数据库字段设NOT NULL,配合默认值或空字符串 - 复合唯一索引(如
(ref_type, ref_id))中任一字段为NULL,整行都不参与唯一性比对——这点比单列更隐蔽
唯一索引和事务配合能实现强幂等,但关键不在“加了索引”,而在“怎么用那条SQL”以及“事务边界划在哪”。漏掉ON DUPLICATE KEY的UPDATE子句细节,或让FOR UPDATE查不到记录,结果就是看着像幂等,实际照常脏写。











