Technical Note

接口幂等设计的后端检查清单

从请求唯一性、状态流转、数据库约束和异步补偿四个角度梳理幂等设计。

  • Java
  • API
  • Idempotency

幂等不是给接口加一个 token 就结束了。它更像一组约束:同一业务动作无论被触发多少次,最终都应该得到一致的业务结果。

我的判断: 幂等的核心不是“防止用户点两次”,而是让超时重试、MQ 重投、第三方重复回调和人工补偿都能落到同一个业务结果上。

请求唯一性

先定义什么叫“同一次业务动作”。常见做法包括:

  • 客户端生成请求号。
  • 服务端生成业务单号。
  • 用外部支付单号、订单号或任务号作为唯一键。

关键点是唯一标识必须进入持久化层,不能只存在于缓存。

状态机约束

幂等经常和状态流转绑定。比如订单只能从 CREATED 变成 PAID,不能从 CLOSED 重新变成 PAID。

数据库更新语句里可以显式带上当前状态:

UPDATE orders
SET status = 'PAID'
WHERE id = ? AND status = 'CREATED';

如果影响行数为 0,就要根据当前状态判断是重复请求、非法请求还是数据异常。

数据库唯一约束

唯一索引是幂等设计里最可靠的防线之一。缓存可以优化性能,但数据库约束才是最后的裁判。

例如支付回调可以对 payment_no 建唯一索引,重复回调时捕获唯一键冲突,再查询已有结果返回。

异步补偿

幂等接口通常还需要补偿任务。网络超时、MQ 重投、第三方回调重复都会让链路变得不确定。

建议把关键动作记录成可查询的业务日志或任务表,并保证补偿任务也走同一套幂等入口。

实际场景:批量创建和结果分群都怕重复提交

幂等在业务系统里最典型的场景,是用户认为自己只点了一次,系统却可能收到多次请求。比如批创平台里,运营提交一次批量创建,后端可能要生成多个项目、多个广告组、多个创意,再调用媒体接口;如果前端超时后用户再次点击,或者网关重试了一次,后端不能把同一批广告重复创建。BI 平台里也类似,用户把路径分析下钻结果保存为结果分群,如果重复提交,不能生成多个含义相同但 ID 不同的分群。

这类场景的幂等键不能随便选。批创可以用 batchId + versionId + commitNo,或者在保存模板后生成一次提交批次号;媒体侧返回的计划 ID、广告 ID、创意 ID 也要和本地草稿建立绑定关系。结果分群可以用分析任务 ID、节点切片参数、用户 ID 集合版本、保存动作批次号共同决定唯一性。关键是幂等键必须能表达“同一次业务动作”,而不是只用用户 ID 或时间戳。

状态机也非常重要。批创任务可以有 DRAFT、PREVIEWED、COMMITTING、PART_SUCCESS、SUCCESS、FAILED 等状态。重复请求进来时,如果状态是 COMMITTING,可以返回“任务处理中”;如果是 SUCCESS,返回已有结果;如果是 PART_SUCCESS,只允许重提失败项,而不是全量重跑。这样幂等不只是防重复插入,也让用户体验更清楚。

数据库唯一约束是最后防线。比如本地媒体计划表可以对 batch_id + advertiser_id + local_index 建唯一约束,素材关系表可以对 creative_id + material_id 去重,结果分群表可以对 source_type + source_key + creator_id 做唯一限制。接口层判断再完善,也挡不住并发请求同时落库;唯一索引能把竞态条件变成可处理的业务分支。

还有一个容易忽略的点:幂等结果也要可查询。只返回“重复请求”不够,最好返回第一次请求生成的业务结果,比如批创的每层成功数、失败原因、媒体返回 ID,或者结果分群的 groupId 和计算状态。这样前端能恢复页面,运营也能继续处理失败项。幂等的最终目标不是让接口“不会报错”,而是让重复提交、超时重试、任务补跑都能得到同一个业务结论。

所以幂等不是接口装饰器,而是业务流程设计。先定义动作和结果,再定义唯一键、状态机和补偿入口,最后才考虑锁或 token。