幂等接口设计
用户点了一下"支付",网络抖动导致请求超时,用户又点了一次;支付平台回调通知重复发了三次;上游服务重试了同一个下单请求。这些情况都会让同一次业务意图被执行多次,造成重复扣款、重复下单、重复发货。幂等设计就是让"同一请求执行多次"和"执行一次"的结果完全一致。
幂等的定义
幂等(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 某字段为固定值
幂等的验证方法
设计完成后,用这几个问题自查:
- 同一请求连发 3 次,数据库里会不会多出记录?
- 并发 100 个相同请求同时到达,会不会有多个成功?
- 第一次请求执行到一半崩溃,重试后状态是否一致?
- 幂等键的生成方是谁?客户端生成还是服务端生成?
- 幂等记录保留了多久?过期后重复请求会怎样?
小结:幂等的本质是"同一业务意图只生效一次",实现前先找到稳定的业务唯一标识,再按业务特点在唯一键、状态机、令牌、乐观锁、分布式锁中选一种或组合使用。特别注意 HTTP 方法语义只是规范约定,接口是否幂等取决于实现而非方法名。下一章给出可落地的表结构与代码流程。