幂等接口设计

用户点了一下"支付",网络抖动导致请求超时,用户又点了一次;支付平台回调通知重复发了三次;上游服务重试了同一个下单请求。这些情况都会让同一次业务意图被执行多次,造成重复扣款、重复下单、重复发货。幂等设计就是让"同一请求执行多次"和"执行一次"的结果完全一致。

幂等的定义

幂等(Idempotent):对同一个操作,执行一次和执行 N 次,对系统产生的影响相同,返回的结果也相同。

这里的"影响相同"指的是业务状态相同,不是每次返回的报文一字不差。比如重复提交订单,第三次返回"订单已存在,订单号 20240101001",业务状态没有变化,这就算幂等。

形式上可以写成:

f(f(x)) = f(x)

即对同一输入连续调用,结果收敛到同一个状态。

典型场景

场景重复来源不幂等的后果
支付扣款用户重复点击、客户端重试重复扣钱
创建订单网络超时后网关重试同一商品多张订单
第三方回调对方未收到响应,按策略重发重复发货、重复入账
消息消费MQ 至少投递一次语义重复处理业务
定时任务补偿多实例同时执行、任务重跑重复生成对账单
外部接口调用调用方自身重试框架重复创建资源

只要链路中存在"超时重试"或"消息重投",就必然存在重复请求,因此幂等不是可选项,而是分布式系统的默认要求

幂等与去重的区别

两个词经常混用,但关注点不同:

对比项幂等去重
关注点同一请求多次执行结果一致同一数据不被重复写入
范围单个业务操作的语义数据层面的唯一性
实现状态机、令牌、乐观锁等唯一索引、去重表
关系去重是幂等的一种实现手段幂等是去重想达到的目标之一

简单说:去重解决"重复数据",幂等解决"重复影响"。一个接口可以做到不产生重复数据,但仍然重复返回错误码或重复发消息,那样仍然不是完整的幂等。

幂等的判断依据:业务唯一标识

实现幂等的第一步,是找到一个能唯一标识"一次业务意图"的字段。常见选择:

支付:商户订单号(out_trade_no)
下单:客户端生成的 requestId / 业务流水号
回调:第三方通知 ID(notify_id)
消息:消息唯一 ID(msgId)或业务主键

注意:不能用数据库自增主键或时间戳当幂等键,前者在第一次请求失败时还没生成,后者在并发下不唯一。

HTTP 方法语义误区

HTTP 规范中方法的幂等性常被记错,先明确结论:

方法是否幂等是否安全说明
GET只读取,重复调用不改变状态
HEAD同 GET,只取头部
PUT整体替换,重复执行结果相同
DELETE删除不存在的资源通常也返回成功
POST每次调用都可能创建新资源
PATCH语义取决于实现,默认不幂等

三个常见误区:

  • 误区一:认为 GET 天然安全,于是在 GET 里做"浏览量 +1"之类的写操作,一旦被预取或重试就多计一次;
  • 误区二:认为 PUT 幂等就等于不会出错,幂等只保证结果收敛,不保证并发安全,"整体替换"仍可能覆盖别人的修改;
  • 误区三:认为用 POST 就不能幂等,方法语义只是规范建议,业务上完全可以在 POST 接口内部用幂等键实现幂等,这也是绝大多数交易接口的做法。

实现思路总览

方案核心思路适用场景主要代价
唯一键 / 去重表唯一索引阻拦重复插入创建类接口需要额外表,需清理历史
状态机只允许特定前置状态流转订单、工单等有状态业务状态设计要完备
幂等令牌先领 token,提交时消费表单、支付入口多一次交互
乐观锁带版本号或条件更新库存、余额变更失败要重试
分布式锁同一业务键串行执行短时临界区锁粒度与超时难调
数据库唯一约束由数据库兜底所有落库场景仅限单库

选型建议:

  • 有明确状态的业务(订单、退款)优先用状态机,天然贴合业务且可读性好;
  • 纯创建类、无状态的业务用去重表
  • 面向用户的前端表单入口,加一层令牌防连点;
  • 涉及金额累加、库存扣减,必须叠加乐观锁或悲观锁,因为幂等只保证不重复,不保证并发正确。

什么时候必须做幂等

判断标准很简单:这个操作重复执行一次,会不会产生用户可感知的错误结果

必须做:扣款、扣库存、发券、发货、发短信、记账
可选做:查询、日志上报(可容忍少量重复)
不必做:纯幂等的自然操作,如 SET 某字段为固定值

幂等的验证方法

设计完成后,用这几个问题自查:

  1. 同一请求连发 3 次,数据库里会不会多出记录?
  2. 并发 100 个相同请求同时到达,会不会有多个成功?
  3. 第一次请求执行到一半崩溃,重试后状态是否一致?
  4. 幂等键的生成方是谁?客户端生成还是服务端生成?
  5. 幂等记录保留了多久?过期后重复请求会怎样?

小结:幂等的本质是"同一业务意图只生效一次",实现前先找到稳定的业务唯一标识,再按业务特点在唯一键、状态机、令牌、乐观锁、分布式锁中选一种或组合使用。特别注意 HTTP 方法语义只是规范约定,接口是否幂等取决于实现而非方法名。下一章给出可落地的表结构与代码流程。

笔记加载中…