返回博客
返回主页

2026-01-10 · Java 后端

记一次线上支付问题的排查过程

线上支付系统出问题,最怕的不是报错,而是"看起来没报错但结果不对"。这次要讲的就是这样一个案例:退款操作表面上成功了,但实际退款并没有真正到达用户账户。

现象

运营反馈某天开始,用户投诉退款没到账的数量明显增加。我们拉了数据看,退款失败率从正常的 0.07%(约 7/10000)上升到了 0.3% 左右。比例看起来不高,但在日均几十万笔退款的基数下,每天多了上千笔异常。

更麻烦的是,这些退款在我们的系统里显示的是"处理中"或者"退款成功",但用户实际没收到钱。这说明问题不是在我们系统的入口处,而是在和外部支付渠道交互的某个环节。

排查过程

第一步:看日志

最先做的是找到具体的失败案例,然后看完整链路日志。我们的退款流程是:退款请求 → 渠道网关 → 外部支付渠道 → 回调通知 → 更新状态。

看了几十条异常日志后,发现一个规律:这些退款在调用外部渠道时返回了"处理中"的状态码,但之后的回调通知要么没来,要么来了但内容和我们预期的不一致。

这把问题定位到了两个可能的方向:外部渠道的回调机制有问题,或者我们的回调处理逻辑有 bug。

第二步:反查

为了区分这两个方向,我们对"处理中"状态的退款做了反查——主动去外部渠道查询退款状态。反查结果证实:大部分"处理中"的退款在外部渠道那里其实已经退款成功了,只是回调通知没有正确到达我们系统。

问题确认在回调环节。

第三步:分析回调数据

拿到回调数据后,发现了真正的原因:外部渠道最近做了一次接口升级,回调通知的格式发生了变化。具体来说,退款状态字段的值从字符串 "SUCCESS" 变成了枚举值 2,但文档没有更新,我们的解析逻辑还是按字符串匹配的。

结果就是:回调通知到了,但被我们的代码当成了"未知状态"丢弃了。退款状态一直停留在"处理中"。

第四步:修复

修复本身很简单——兼容新旧两种状态格式。但更重要的是善后:

  1. 对所有"处理中"超过一定时间的退款发起反查,更新实际状态
  2. 对已经确认退款成功但状态未更新的记录,触发补偿逻辑

从这次事件学到的几件事

反查能力是救命稻草

在支付系统里,不能完全依赖回调通知。回调可能丢失、延迟、格式变化,任何时候都要有主动反查的能力。我们在重构退款模块时加上的反查机制,这次直接救了场。

错误处理要分层

这次的问题之所以没被及时发现,是因为我们的错误处理把"未知状态"和"明确失败"混在了一起。"未知状态"应该触发告警和人工介入,而不是静默丢弃。后来我们把错误分成了三层:

强制终态很重要

"处理中"是一个危险的状态。如果退款长时间停留在"处理中",对用户来说就是"钱没了"。后来我们加了强制终态机制:如果退款超过一定时间(比如 48 小时)仍然是"处理中",系统自动发起反查,如果反查结果是已成功就更新状态,如果反查也查不到就标记为"需人工处理"。

日志要串链路

这次排查之所以还算顺利,是因为日志能把一笔退款从头到尾的链路串起来。如果日志是散的,光是找到"这笔退款的回调到底来没来"就要花很长时间。关键日志点包括:退款发起时、渠道调用后、回调收到时、状态更新时,每个环节都要带上退款单号作为关联字段。

结果

修复上线后,退款失败率回到了 0.07% 的正常水平。后续我们加了回调格式的自动化测试,确保外部渠道的接口变更能被及时发现。运维工作量从之前的每周 5 小时人工核对,降到了每周 0.5 小时巡检。

总结

线上支付问题的排查,核心思路就是:先看日志缩小范围,再通过反查确认实际状态,最后分析根因。但比排查方法更重要的,是系统设计时就为排查留好余地——反查能力、分层错误处理、强制终态、完整链路日志,这些都不是事后能轻松补上的。