支付回调、库存扣减与退款如何避免重复执行
关键交易不能只依赖前端按钮或分布式锁;需要用数据库条件更新、事务、乐观锁和真实并发验证共同守住订单、库存与资金状态。

支付平台可能重复发送回调,买家可能连续点击提交,网络超时也可能让同一个请求被重试。关键交易 如果只依赖前端禁用按钮或短期分布式锁,进程崩溃、锁过期和消息重投仍可能造成重复处理。
CusMall 对支付、SKU 库存和退款结果使用数据库中的前置状态与条件更新认领业务,再把后续状态 变化放入事务中;同时在与交付环境一致的 MySQL 上运行并发竞争验证。
三个关键不变量
| 场景 | 服务端约束 | 并发验证目标 |
|---|---|---|
| 支付回调 | 只有未支付订单可以认领支付结果 | 两个回调竞争时只处理一次,佣金不重复累计 |
| SKU 扣减 | 使用库存条件与乐观锁版本更新 | 两个订单竞争最后库存时只有一个成功 |
| 退款结果 | 只有未处理结果可以认领补偿流程 | 两个退款通知竞争时库存和积分只归还一次 |
“接口返回成功”不是最终不变量。真正需要保护的是订单、库存、会员资产、佣金和支付记录之间的 一致状态,以及重复请求到达时能够得到可解释的幂等结果。
为什么条件更新要和事务一起使用
条件更新负责决定谁获得处理权,事务负责让认领后的多项业务变化一起成功或一起失败。例如支付 结果被一个请求认领后,订单状态、营销消费和佣金记录需要在同一业务事务中推进;中途失败时不能 留下“订单已支付但资产没有处理”的半完成状态。
SKU 使用乐观锁时,后提交的事务发现版本已经变化便整体失败,再由业务层返回库存不足或重新报价, 而不是覆盖前一个事务的库存结果。
分布式锁为什么不是最终事实
分布式锁适合减少同时处理,但不能替代数据库不变量。服务可能在写库后、释放锁前崩溃,锁租约也 可能在长事务中到期;第三方消息还会在锁结束后再次投递。因此最终状态必须由数据库前置条件、唯一 约束、版本字段和持久化状态机共同决定。
Redis 可以承担快速触发和短期协调,但截止时间、处理结果和可补偿任务需要落库。即使缓存事件 丢失,定时任务也应能从数据库重新发现并推进未完成业务。
并发测试应覆盖什么
- 允许的前置状态与禁止的逆向状态;
- 两个请求同时认领同一订单或退款结果;
- 最后一件库存被不同事务竞争;
- 重复回调、超时重试和延迟通知的返回语义;
- 事务中途失败后的回滚、补偿与对账入口;
- 测试数据库版本、字符集和最终表结构是否与交付环境一致。
隔离并发测试只能证明当前声明场景,不代表所有业务状态天然安全。新增订单状态、支付渠道、营销 资产或佣金规则时,需要同步增加原子认领条件、失败补偿、真实数据库竞争用例和观测指标。