一、什么是幂等
幂等指同一个操作执行一次和执行多次,最终产生的业务结果一致。
例如查询接口天然是幂等的:
1 | GET /api/orders/1001 |
多查几次不会改变订单状态。
但创建订单、支付回调、库存扣减、消息消费就不一定幂等:
1 | POST /api/orders |
如果用户连点两次、客户端超时重试、网关重复转发、消息队列重复投递,就可能产生重复订单、重复扣款或重复发券。
二、哪些场景必须考虑幂等
常见高风险场景:
- 创建订单。
- 支付回调。
- 退款申请。
- 优惠券领取。
- 库存扣减。
- 表单重复提交。
- 消息队列消费。
- 定时任务补偿。
- 第三方接口回调。
判断标准很简单:如果重复执行会改变钱、库存、权益、状态,就必须设计幂等。
三、唯一请求号
最常见的方式是客户端或服务端生成唯一请求号,也叫 requestId、idempotencyKey 或业务流水号。
请求示例:
1 | POST /api/orders |
服务端处理流程:
- 校验幂等 key 是否存在。
- 不存在则创建处理记录。
- 执行业务操作。
- 保存业务结果。
- 重复请求直接返回第一次处理结果。
表结构示例:
1 | create table api_idempotency ( |
关键点是唯一索引。没有唯一约束,只靠先查再插,在并发下仍然可能重复。
四、利用数据库唯一约束
有些业务可以直接用业务唯一键保证幂等。
例如用户领取优惠券,一个用户对同一张券只能领取一次:
1 | create table user_coupon ( |
接口逻辑:
1 | public void receiveCoupon(Long userId, Long couponId) { |
这种方式简单可靠,但只适合能明确找到业务唯一键的场景。
五、状态机防重
订单、退款、工单这类业务通常有状态流转。状态机本身也可以防止重复执行。
例如订单支付:
1 | update orders |
只有待支付状态才能更新为已支付。重复回调时,订单已经是 PAID,更新行数为 0,业务可以查询当前订单后直接返回成功。
这种写法比“先查状态再更新”更抗并发,因为判断和修改在同一条 SQL 中完成。
六、Redis 锁的作用和边界
Redis 分布式锁常用于拦截短时间内的重复请求:
1 | SET lock:order:create:{requestId} 1 NX EX 30 |
拿到锁才执行业务,拿不到锁说明同一个请求正在处理。
但要注意:
- 锁只能减少并发进入,不能替代数据库唯一约束。
- 锁过期时间太短,业务没执行完就可能被第二个请求拿到锁。
- 服务异常退出时,要有最终一致的兜底机制。
可靠做法通常是“Redis 锁 + 数据库唯一约束 + 状态机”组合使用。
七、消息消费幂等
消息队列通常只保证至少投递一次,消费者必须能处理重复消息。
可以建立消费记录表:
1 | create table mq_consume_record ( |
消费流程:
- 插入消费记录。
- 插入成功,执行业务。
- 业务成功,更新记录为成功。
- 插入失败,说明已经消费过或正在消费。
如果消息本身没有全局唯一 ID,可以使用业务单号、事件类型和版本号组合出唯一键。
八、接口返回策略
幂等接口遇到重复请求时,不建议直接返回“重复提交”错误。更好的体验是返回第一次请求的业务结果。
例如创建订单:
- 第一次请求创建订单成功,返回订单号。
- 第二次使用同一个幂等 key 请求,仍然返回同一个订单号。
这样客户端即使因为超时重试,也能拿到确定结果。
如果第一次请求还在处理中,可以返回明确状态:
1 | { |
九、小结
幂等设计不是单个工具能解决的问题,而是一组约束:
- 请求层用幂等 key 识别重复请求。
- 数据层用唯一索引兜底。
- 状态流转用条件更新防止重复变更。
- 消息消费用消费记录防重。
- 返回结果尽量保持可重试。
涉及资金、库存、权益和外部回调的接口,都应该在设计阶段把幂等方案写清楚。
- 本文链接: https://blog.hansong.icu/2026/06/22/Backend_API_Idempotency/
- 版权声明: 本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。