支付掉单排查:从用户反馈到对账闭环

Cold 发布于 2023-08-19 10 次阅读 766 字 预计阅读时间: 3 分钟 后端开发


客服转来一个用户反馈:"钱扣了,订单显示未支付"。支付掉单是交易系统最经典的问题之一,这篇记录这次排查的完整链路,以及事后补上的长效机制。

掉单是怎么发生的

先看链路:用户下单 → 跳转支付 → 第三方支付扣款成功 → 回调通知我们的服务器 → 我们更新订单状态。掉单,多半断在回调这一环。查这笔订单:

  • 支付平台侧:查单确认用户确实付款成功,有支付流水号
  • 我们这边:订单状态一直是"待支付",回调日志里没有这笔单的记录——回调根本没到达业务层
  • Nginx 访问日志里找到了那次回调请求:504,超时

对时间点:回调发生时数据库正在执行一个大事务(历史数据迁移),回调接口里更新订单要等行锁,等超过 Nginx 的 60 秒超时,被掐断了。第三方一般会重试回调,但重试次数有限(这家是 5 次,间隔递增),几次都撞上迁移窗口,最后一次重试也失败了,这单就永远掉在那了。

应急处理

用户侧:通过支付流水号人工核对后补单,先让用户正常使用,态度诚恳送了张券。技术上:没有立刻写"全量补偿任务",而是先回答一个问题——还有多少单掉了?用支付平台提供的对账单和我们的订单表做了比对,当天一共掉了 3 单,全部补齐。范围可控,没有更大的坑。

长效机制(这次真正的收获)

  • 回调幂等:回调接口按支付流水号幂等,重复回调直接返回成功。重试不可怕,重复处理才可怕
  • 回调处理异步化:回调只做"验签 + 落库到账消息 + 立即应答",订单状态更新由消费者处理。回调接口的耗时从最长 60s 变成 200ms 内,再也不会被下游拖死
  • 主动查单兜底:对超过 N 分钟仍"待支付"的订单,定时任务拿单号去支付平台主动查一次状态。回调是"推",查单是"拉",推拉双保险,掉单率理论归零
  • 每日对账:每天拉支付平台对账单和本地流水比对,差异单自动挂起人工处理。这是最后的防线,前面所有环节都失效时靠它兜底

复盘

这次问题的根因不是代码 bug,而是对回调链路的脆弱性没有设计:一个同步接口把状态更新的成败绑在了数据库的实时状态上。支付这类和钱相关的链路,设计时永远多问一句:这一环失败会发生什么?用户看到什么?钱去了哪里?把这三个问题回答清楚,系统才算合格。

和喜欢的一切在一起~
最后更新于 2023-08-19