接口幂等实现方案
接口幂等是分布式系统的「必备能力」。
HTTP 重试、消息队列重试、服务超时重试——在分布式环境下,请求被重复发送几乎是不可避免的。网络抖动、进程崩溃、超时未响应——任何一种情况都会导致客户端重试。如果接口不做幂等处理,重复请求就会产生重复操作:扣两次钱、创建两笔订单、库存被扣两次。
这篇文档详细介绍三种主流的幂等实现方案:唯一键约束、状态机 + 乐观锁、防重 Token。每种方案都有适用场景和局限性,理解这些才能在实际项目中做出正确选择。
方案一:唯一键约束
核心思想
利用数据库的唯一索引(或唯一键)来保证幂等。当重复请求插入相同唯一键的记录时,数据库会抛出唯一键冲突异常,捕获异常后返回「已处理」状态——这就是幂等。
场景:创建订单
优点与缺点
适用场景
- 新增类操作(INSERT)
- 有明确唯一标识的业务(如订单号、支付流水号)
- 对一致性要求高的核心业务
方案二:状态机 + 乐观锁
核心思想
当业务操作涉及状态流转时,可以通过「检查当前状态 + 乐观锁」的方式实现幂等。只有在允许的状态下才能执行操作,其他状态直接返回成功(幂等)。
场景:支付回调
状态机幂等实现
优点与缺点
适用场景
- 支付、退款、取消等有状态流转的业务
- 需要严格控制状态转换顺序的场景
- 乐观锁能解决并发更新问题的场景
方案三:防重 Token
核心思想
客户端在调用接口前,先向服务端申请一个「防重 Token」,服务端将 Token 标记为「已申请但未使用」。调用接口时携带这个 Token,服务端检查 Token 状态,如果未使用则执行操作并标记为「已使用」。如果 Token 已被使用,直接返回成功(幂等)。
Token 生命周期
Redis Token 实现
优点与缺点
适用场景
- 需要对非幂等操作(POST)做幂等处理的场景
- 客户端可控制的场景(如前端页面、App)
- 需要追踪请求处理状态的场景
权衡矩阵
方案组合使用
实际项目中,三种方案可以组合使用:
- 入口层:使用防重 Token 做第一道防线,支持快速拒绝重复请求
- 业务层:使用状态机 + 乐观锁控制业务状态流转
- 存储层:使用唯一键约束做最后一道防线,确保数据不会重复插入
实践建议:不要过度设计。简单场景下,一个唯一键约束就足够了。只有在复杂业务场景下,才需要组合多种方案。
术语表
思考题
问题 1:唯一键约束方案中,如果客户端没有生成唯一键,服务端如何处理?
参考答案
服务端可以采用两种策略:
-
服务端生成唯一键:让客户端先调用获取唯一键的接口(如 POST /generate-id),服务端生成并返回唯一键,客户端再携带这个键调用业务接口。
-
拒绝请求:如果请求中没有唯一键,且业务本身需要幂等,可以直接返回错误,要求客户端带上幂等键。
实际项目中,推荐第一种方式,因为客户端可能因为网络问题在获取唯一键后仍然重试,所以服务端也需要有兜底的幂等逻辑(如防重表)。
问题 2:乐观锁在高并发场景下失败率很高,应该如何优化?
参考答案
乐观锁在高并发场景下的主要问题是「大量重试」。优化思路:
- 重试 + 随机退避:发生冲突后,等待随机时间再重试,避免集体重试。
- 分段乐观锁:将数据按某个维度分段,不同段的更新不会冲突。
- 降低冲突概率:使用「预占位」方式,如先更新状态为 PROCESSING,再执行业务,最后更新为最终状态。
- 改为悲观锁:在高并发场景下,悲观锁(如 SELECT FOR UPDATE)可能更稳定。
- 异步处理:将并发请求放入队列,串行处理。
问题 3:防重 Token 方案中,如果业务处理成功但 Token 更新失败(如 Redis 宕机),会怎样?
参考答案
会出现「重复处理」的问题:
-
Redis 宕机:Token 校验完全失效,业务接口会正常执行。但这种情况下,重复请求只有数据库层做幂等(如果有唯一键约束)或状态机层做幂等。
-
Token 更新失败:Token 仍为 PENDING 状态,后续重试会被认为「正在处理中」,业务不会重复执行。但超过 30 秒(processing 锁过期)后,可能再次处理。
解决方案:
- 业务逻辑执行完后,即使 Redis 操作失败,也将结果返回给客户端
- 配合数据库唯一键约束作为兜底
- 使用 Redis 的 AOF 持久化或主从复制提高可用性