用户重复点击、网关重试和超时补发都可能把同一个业务请求送来多次。
本文使用Java 7 与支持事务、唯一约束的关系数据库,代码为设计伪代码。支付系统同时面对用户请求、外部通道和本地账务,三者不共享一个事务。网络异常下必须保留“不确定”状态,并用查询、流水和对账逐步消除差异。
1. 从现象推到机制
幂等键代表一次业务意图,不是每次网络调用都生成新键。数据库唯一约束决定哪一个请求首次占位,其他请求读取已有结果。键相同而参数不同需要拒绝,不能悄悄复用旧金额。
图中的四个节点分别表示业务意图、唯一占位、参数核对、结果复用。顺着实线检查输入如何变成输出,再沿虚线核对反馈条件。图示表达的是本文讨论的关系,不是实测曲线或现场截图。
2. 最小实现与输入条件
在同一事务内建立幂等记录并处理本地订单。远程调用不在该事务保护内,需要独立状态与恢复流程。
1
2
3
4
5
BEGIN;
INSERT INTO request_guard(biz_key, payload_digest, state)
VALUES(:key, :digest, :initial);
-- 唯一冲突后读取已有记录并核对请求摘要
COMMIT;
示例使用虚构订单和金额,不能连接真实支付通道。实现时需要替换数据库访问与验签适配,并明确事务提交点;外部接口的签名字段和响应约定以通道协议为准。
3. 用对照区分原因
下表列出需要主动改变的条件。预期行为来自机制分析,复现时还需记录运行环境和原始输出,不能将预期当作已经测得的结果。
| 改变的条件 | 需要验证的行为 |
|---|---|
| 相同键与相同参数重试 | 返回同一业务处理身份 |
| 相同键但金额变化 | 拒绝不一致请求 |
| 两个请求并发首次到达 | 仅一个占位成功 |
对照 1:相同键与相同参数重试
返回同一业务处理身份。观察时记录唯一约束冲突和已有结果。把这个条件与基准情形分开,有助于判断变化来自哪一层;若得到相反结果,应先核对前提,再修改实现。
对照 2:相同键但金额变化
拒绝不一致请求。观察时记录已保存摘要与本次参数。把这个条件与基准情形分开,有助于判断变化来自哪一层;若得到相反结果,应先核对前提,再修改实现。
对照 3:两个请求并发首次到达
仅一个占位成功。观察时记录数据库约束而非应用先查询。把这个条件与基准情形分开,有助于判断变化来自哪一层;若得到相反结果,应先核对前提,再修改实现。
4. 适用条件与取舍
幂等记录要定义保留时间和作用域。过早删除后,旧重试可能成为新业务;超时键也不能直接标为成功来消除重复。
一次正常运行只覆盖了这组输入。继续扩展前,应把上面的异常对照保留下来,尤其关注“两个请求并发首次到达”这一分支。后续方案若改变了图中任意一处依赖,原来的判断就要重新验证,不能仅凭界面相似或返回值相同沿用结论。