接口幂等设计:从重复提交到可恢复业务状态机

网络请求可能超时,但超时只说明客户端没有及时收到结果,不代表服务端没有执行。用户重复点击、网关重试、消息重复投递和客户端断线重连都会制造重复操作。幂等设计的目标,是让同一业务意图执行多次仍只产生一次有效结果。

先区分读取与写入

GET、PUT、DELETE 在 HTTP 语义上通常应幂等,但业务实现仍可能破坏这一点,例如每次 GET 都增加余额。POST 默认不幂等,创建订单、支付、提交审核等操作需要显式业务键。

客户端为一次业务意图生成 Idempotency-Key,并在重试时复用:

Idempotency-Key: 6a1d3a0e-...

服务端保存 Key、请求摘要、处理状态和响应结果。相同 Key 且请求内容一致时返回第一次结果;相同 Key 但参数不同,应拒绝,避免错误复用。

数据库唯一约束是最后防线

CREATE TABLE idempotency_record (
  scope        VARCHAR(64) NOT NULL,
  request_key  VARCHAR(128) NOT NULL,
  request_hash CHAR(64) NOT NULL,
  state        VARCHAR(16) NOT NULL,
  response     JSON NULL,
  create_time  DATETIME NOT NULL,
  PRIMARY KEY (scope, request_key)
);

首次请求插入 processing,业务结果与记录状态在同一事务中提交。唯一键冲突后读取已有记录:completed 返回保存结果;processing 返回处理中或短暂等待;failed 是否允许重试要由业务规则决定。

仅用 Redis SETNX 加短 TTL 存锁有风险:业务仍执行时 Key 过期,第二次请求会再次进入;Redis 故障也可能丢保护。Redis 可以加速判断,但重要写操作仍应由数据库唯一约束或领域唯一键兜底。

幂等键应该绑定什么

  • 绑定用户或租户,防止不同用户 Key 碰撞。
  • 绑定接口与业务动作,不能跨操作复用。
  • 保存请求摘要,检测同 Key 不同参数。
  • TTL 至少覆盖客户端最大重试窗口和消息最长重投周期。
  • 对永久业务对象,直接使用业务唯一键可能比单独幂等表更自然。

例如“用户领取某活动奖励”可直接建立 (activity_id, user_id) 唯一键;无论请求 Key 如何变化,都只能成功一次。

状态机比布尔成功更可靠

跨服务流程可能经历 created -> processing -> succeeded/failed。客户端查询同一业务 ID 获取状态,后台任务从中间状态继续。不要在超时后直接创建一个新业务对象,否则无法判断上一次是否已成功。

测试这些故障点

  1. 请求到达前断网。
  2. 事务提交后、响应返回前断网。
  3. 两个相同请求同时到达。
  4. 同 Key 使用不同 Payload。
  5. 服务重启后再次请求。
  6. 下游成功、本地记录暂时失败。

幂等不是一个中间件注解,而是业务唯一性、事务边界和恢复状态共同构成的协议。

参考资料