mysql无“自幂等性”,真正起作用的是唯一约束+原子语句+显式事务组合;漏建唯一索引、update含非幂等表达式、字段允许null或事务配置不当均会导致失效。

MySQL 本身没有“自幂等性”这个概念,所谓“利用自幂等性优化锁定”是常见误解——真正起作用的是唯一约束 + 原子语句 + 显式事务的组合,不是数据库自动具备某种幂等能力。
为什么不能依赖 MySQL 的“自动幂等”
很多人看到 INSERT ON DUPLICATE KEY UPDATE 成功执行就以为“MySQL 自带幂等”,但本质是:它靠唯一索引触发冲突检测,再由单条 SQL 原子完成“插入 or 更新”。这不叫“自幂等”,而是你用对了语法和约束。一旦漏掉以下任一条件,就会失效:
-
request_id字段没建UNIQUE或PRIMARY KEY索引,ON DUPLICATE KEY直接被忽略 -
UPDATE子句里写了counter = counter + 1这类非幂等表达式,重复请求导致值累加 - 字段允许
NULL,而唯一索引对NULL不校验,相同非空值 + 不同NULL组合可绕过约束 - 事务中混了其他 DML(如先扣库存再插入日志),唯一键冲突时前面操作不会自动回滚
INSERT ON DUPLICATE KEY UPDATE 是最简可靠方案
它不引入锁竞争,也不依赖事务隔离级别,适合写入即幂等的场景(如日志记录、token 刷新、状态快照)。关键点在于“原子性”和“约束前置”:
- 必须确保冲突字段(如
request_id)有UNIQUE索引,否则整条语句退化为普通INSERT -
UPDATE部分只能赋固定值或函数(如NOW()),禁止任何读-改-写逻辑 - 若需区分首次插入与重复更新,可加
is_repeated TINYINT DEFAULT 0字段:ON DUPLICATE KEY UPDATE is_repeated = 1, updated_at = NOW() - 示例:
INSERT INTO idempotent_log (request_id, payload) VALUES ('req-789', '{"status":"ok"}') ON DUPLICATE KEY UPDATE updated_at = NOW();
SELECT FOR UPDATE 只适用于状态流转类更新
它不是为“防重插入”设计的,而是为“查到后改状态”服务的,比如支付单从 created → paid。但它极易因配置不当引发性能问题:
-
SELECT ... FOR UPDATE必须走主键或唯一索引,否则会升级为间隙锁甚至表锁,高并发下直接拖垮整张表 - 必须包在显式事务中(
START TRANSACTION或 Spring@Transactional),且SELECT和后续UPDATE的WHERE条件完全一致(含状态判断) - 执行完
UPDATE后,必须检查影响行数是否为 1;为 0 说明已被抢先更新,此时应直接返回,而非重试或抛错 - 错误写法:
SELECT * FROM orders WHERE order_id = 'ORD123' FOR UPDATE;→ 没带状态条件,锁住后别人仍可能并发更新成其他状态
容易被忽略的 NULL 和事务传播细节
这两个点在压测或上线后才暴露,但修复成本极高:
- 唯一索引字段必须声明
NOT NULL,否则INSERT INTO t(req_id) VALUES (NULL)多次执行不会报错——因为所有NULL在唯一约束中互不冲突 - Spring 中
@Transactional默认只对未检查异常(RuntimeException)回滚,而MySQLIntegrityConstraintViolationException虽属运行时异常,但若你在 catch 块里吞掉了它,事务就不会回滚 - 如果用
SELECT FOR UPDATE做幂等,却把事务范围设在 service 层外(比如 controller 加了@Transactional),那锁在响应返回后才释放,极大延长锁持有时间











