4. 防重核心原则

重复付款检测不是靠“查一下有没有”就能解决的问题。真正可靠的支付防重体系,必须建立在四条核心原则之上:幂等性保证同一请求多次执行结果一致,唯一性约束在数据层兜底拦截,去重键为每笔支付提供可判定的身份标识,状态一致性确保支付状态在并发与重试中不发生错乱。四者缺一不可,任何一条失守,都可能让重复付款从缝隙中穿透。

一、核心原理:四原则的职责边界

很多团队把防重简单理解为“加个唯一索引”,结果上线后仍出现重复扣款。原因在于没有分清四条原则各自解决什么问题。

  • 幂等性解决“同一操作重复执行”的问题。它要求无论请求被重试多少次,最终业务结果与第一次执行相同。幂等是接口契约层面的承诺,通常通过幂等令牌、请求指纹或业务单号实现。
  • 唯一性约束解决“并发写入”的问题。它由数据库唯一索引、分布式锁或去重表承担,是最后一道物理防线。即使应用层判断失误,唯一约束也会让第二条写入失败。
  • 去重键解决“如何判定两笔支付是同一笔”的问题。去重键是幂等判断和唯一约束的共同输入,设计质量直接决定防重精度。
  • 状态一致性解决“支付状态在多次操作后是否可信”的问题。它要求状态迁移遵循确定规则,避免出现“已支付又被置为待支付”这类回退。
四原则的关系可以概括为:去重键是判据,幂等性是契约,唯一性约束是底线,状态一致性是结果保障。判据错了,后面全错;底线缺失,并发必穿;状态错乱,对账必崩。

二、方法流程:从请求进入到落库的防重链路

一个可落地的防重链路通常包含以下步骤:

  1. 提取去重键:从请求中提取业务单号、商户号、金额、币种、支付渠道等字段,组合成唯一键,例如 merchantId + outTradeNo。
  2. 幂等前置校验:以去重键查询幂等表或支付单表,若已存在且状态为成功,直接返回原结果,不再发起支付。
  3. 获取幂等令牌:若不存在,尝试写入幂等记录,利用唯一索引保证只有一个请求能成功插入。
  4. 执行支付:持有令牌的请求调用渠道支付,其余请求等待或返回处理中。
  5. 状态落库与更新:支付成功后更新支付单状态,并保证状态迁移单向不可逆。
  6. 异步对账兜底:通过渠道对账文件核对本地状态,发现不一致时触发补偿。

其中第 3 步是并发防重的关键。下面是一段典型的幂等插入 SQL:

INSERT INTO payment_idempotent
  (idempotent_key, biz_no, status, create_time)
VALUES
  ('M1001_ORDER20240520001', 'ORDER20240520001', 'PROCESSING', NOW())
ON DUPLICATE KEY UPDATE
  update_time = NOW();

如果返回受影响行数为 1,说明当前请求抢到了令牌;如果为 0,说明已有请求在处理,应查询原状态而非重复发起支付。

三、案例场景:一次重复付款是如何被拦截的

假设商户在 1 秒内对同一订单发起了 3 次支付请求,去重键为 M1001_ORDER20240520001。

请求 幂等表插入结果 系统动作 最终结果
请求 A 插入成功 调用渠道支付 支付成功,状态置为 SUCCESS
请求 B 唯一键冲突,插入失败 查询原支付单状态 返回“处理中”,不重复扣款
请求 C 唯一键冲突,插入失败 查询原支付单状态 返回“已成功”,复用原结果

这个案例说明:唯一性约束把并发请求收敛为一条执行路径,幂等性让 B、C 请求获得与 A 一致的结果,状态一致性保证最终只有一笔真实支付。

去重键设计建议:优先使用业务方可控且全局唯一的字段,如商户订单号;避免使用时间戳、随机数等不可复现字段,否则重试时无法命中同一条幂等记录。

四、误区与复盘:四原则最容易踩的坑

误区一:只用应用层查询做防重。“先查再插”在并发下存在竞态窗口,两个请求可能同时查到“不存在”,然后都发起支付。必须依赖唯一索引或分布式锁兜底。

误区二:去重键包含可变字段。若去重键包含支付金额、渠道返回流水号等可能变化的字段,重试时键值改变,幂等记录无法命中,防重失效。

误区三:状态可回退。支付成功后又因超时补偿把状态改回“待支付”,会导致后续请求再次发起支付。状态迁移必须单向,补偿应通过新增状态或冲正记录表达。

误区四:忽略渠道侧幂等。本地防重只能拦住自己的重复请求,若渠道侧因网络重试收到两次扣款指令,仍需依赖渠道幂等参数,如 out_trade_no 或 request_id。

复盘要点:每次重复付款事故都应回答四个问题——去重键是否唯一且稳定?幂等记录是否在支付前写入?唯一约束是否覆盖所有写入路径?状态迁移是否单向且可审计?四问中任一为否,事故就可能重演。
防重四原则:从请求到落库的收敛链路 去重键 merchantId + outTradeNo 幂等性 重复请求返回同一结果 唯一性约束 唯一索引 / 分布式锁 状态一致性 单向迁移,不可回退 请求 A 首次到达 请求 B 并发重试 请求 C 超时重发 幂等表 唯一键:idempotent_key 仅允许一条写入成功 其余请求读取原状态 支付单状态 PROCESSING → SUCCESS 单向迁移,禁止回退 对账兜底校验 去重键判定身份 → 幂等表收敛并发 → 唯一约束兜底 → 状态单向落库

把四原则落到工程上,就是一条清晰的收敛链路:去重键先判定“这是不是同一笔”,幂等表用唯一约束把并发请求收敛为一条执行路径,状态机保证最终结果唯一且可审计。任何一环松动,重复付款就会从缝隙中穿透。真正稳定的支付防重,不依赖某一次查询的准确性,而依赖这四层原则共同构成的确定性结构。