先说结论
同一个操作执行一次或执行多次,系统最终产生的业务效果相同,就称为幂等。HTTP GET、PUT、DELETE 在协议语义上通常被设计为幂等,但真实业务是否幂等仍取决于服务实现。
网络超时无法证明服务端没有执行。客户端、网关、消息队列都可能重试,因此订单创建、支付、库存扣减等关键写操作必须主动设计幂等。
先识别业务唯一性
最可靠的幂等依据通常是业务唯一键,例如支付请求号、订单号或消息 ID。键必须代表“同一次业务意图”,不能每次重试都生成新值,也不能把不同操作错误地复用同一个键。
数据库唯一约束
通过唯一索引让重复插入在数据库层原子失败,是最直接的方案:
CREATE UNIQUE INDEX uk_payment_request
ON payment(request_id);
收到唯一键冲突后,应查询并返回第一次请求的结果,而不是统一报系统错误。“先查询再插入”存在并发窗口,唯一索引才是最终保障。
幂等记录表
通用接口可以维护 idempotency_key、request_hash、status、result。第一次请求创建处理中记录,完成后保存响应;重复请求校验参数摘要,若参数不同应拒绝,若已完成则返回原结果。
处理中记录需要超时与恢复机制,否则服务崩溃会留下永久处理中状态。响应过大或含敏感信息时,不应无期限保存完整内容。
状态机条件更新
对订单、任务等有明确生命周期的数据,可使用条件更新:
UPDATE orders
SET status = 'PAID', version = version + 1
WHERE id = ? AND status = 'PENDING' AND version = ?;
受影响行数为 0 时,需要区分已经完成、版本冲突和对象不存在。状态机还必须定义合法迁移,避免状态倒退。
Redis 的使用边界
SET key value NX EX seconds 可以快速拦截短时间重复请求,但 Redis 故障、过期时间和数据库事务之间仍存在一致性窗口。对于资金等核心数据,Redis 适合削峰,数据库约束仍应作为最终防线。
消息消费幂等
消费者可将消息唯一 ID 与业务写入放在同一数据库事务中。若消息记录插入发生唯一键冲突,说明已处理过。仅在内存中记录或先写缓存再写数据库,都无法可靠覆盖进程重启和部分失败。
设计检查表
- 幂等键由谁生成,生命周期和作用域是什么?
- 相同键但不同参数如何处理?
- 第一次请求仍在执行时,重复请求等待、拒绝还是返回处理中?
- 服务在业务成功、记录结果前崩溃如何恢复?
- 幂等记录保存多久,过期后重试是否仍安全?
- 是否用并发请求和故障注入验证过,而不只是串行测试?
常见问题
追问:Redis SET NX 能完全保证幂等吗?
不能。锁过期、Redis 故障以及数据库事务之间仍有窗口。Redis 可快速拦截重复请求,核心数据仍需数据库唯一约束或条件更新兜底。
追问:幂等和防重复提交一样吗?
防重复提交偏向入口拦截;幂等要求重复执行后业务最终效果一致,必须覆盖网络重试、消息重复和服务崩溃等更广场景。