支付系统中异常处理的重要性及常见异常场景解析

2024-08-01
来源:网络整理

前言

本文主要针对支付领域的这三种异常场景进行探讨,并分享针对支付系统中出现以下异常的一些处理方法:

其实这些处理方式并不局限于支付系统,也可以应用到其他系统中,大家可以借鉴,应用到自己的系统中,提高自己系统的健壮性。

异常是系统运行时不可避免的问题,如果一切正常,我们的系统设计就会相当简单。

但不幸的是没有人能够做到这一点,所以为了处理异常可能带来的问题,我们不得不增加很多额外的设计来处理这些异常。

可以说在系统设计中,异常处理是需要我们集中思考的,它会占用我们大部分的精力。

异常订单取消

最常见的支付平台架构关系如下:

上图我们是站在第三方支付公司的角度,如果是自己公司内部的支付系统,那么外部的商户其实就是公司内部的一些系统,比如订单系统等,而外部的支付渠道其实就是第三方支付公司。

我们以携程为例,当我们在其上发起订单支付时,会经过三个系统:

上述流程简单如下图所示:

在这个过程中,可能会遇到用户工行卡已经扣款,但携程订单还在等待付款的情况,我们通常把这种情况叫做“掉单”。

上述丢单场景,大多都是因为“③、⑤”环节的信息丢失导致的,我们把这类丢单称为“外部丢单”。

极少数情况下,在“③、⑤”环节收到了返回信息,但内部系统在“④、⑥”环节未能更新订单状态,导致支付成功信息丢失。由于此类掉单属于内部问题,我们通常称之为“内部掉单”。

外部订单取消

外部取消订单是由于没有收到对端的返回信息导致的。

这种情况很有可能是网络问题,或者是另一端的处理逻辑太慢,导致我们的请求超时,直接断开网络请求。

解决方案 1:增加超时时间

对于这种情况,第一个也是最简单的解决办法就是“适当增加超时时间”。

不过这里要注意的是,我们把网络超时时间调大之后,可能还需要调整整个链路的超时时间,否则可能会造成整个链路内部出现问题,造成内部掉单。

画外音:连接外部频道时,必须“设置网络连接超时和阅读超时”。

解决方案 2:接收异步通知

第二种方式是从接收通道接收异步回执通知信息。

一般来说,我们现在可以向支付渠道接口发送一个异步的回调地址,当渠道端处理成功后,会将成功信息通知到这个回调地址。

这种情况下,我们只需要接收通知信息,解析,然后更新内部订单状态就可以了。

对于这种情况,我们需要注意以下两点:

解决方案三:遗失订单查询

有些渠道可能不提供异步通知功能,只提供订单查询接口,这种情况下我们只能采用第三种方案,即调度订单查询。

我们可以将这些未知超时的订单单独保存在丢单表中,然后定期从渠道端查询订单状态。

如果查询成功或者明显失败(比如订单不存在),则可以更新订单状态,并删除单表记录。

一定要特别注意订单不存在的情况,有可能你查询的时候,查询请求在对方数据入库之前就到了,导致对方查不到数据,返回订单不存在的信息,但其实订单是存在的,所以你需要意识到“订单不存在”可能是一个非常危险的场景。

如果查询仍然未知,我们需要等待下一个查询的结果。

这里需要注意的是,有些情况下,可能无法查询到返回订单的状态,所以需要设置订单查询的最大次数,防止无限查询浪费性能。

解决方案 4:帐户对帐

最后,极少数情况下,订单查询或者异步通知都无法获取支付结果,那就只剩下最后的退路了:对账。

如果第二天渠道提供的对账文件包含这个付款结果,我们可以直接根据这个记录来更新我们的内部付款记录。

为了保险起见,可以先发起查询,然后根据查询结果更新订单记录。

但在某些极端情况下,查询无法获得结果,因此可以直接更新内部记录。

如果第二天这条记录还是没有结果的话,这种情况下,我们可以认为这笔交易失败了。

如果用户被扣款了,渠道会发起退款,把支付金额退还给用户,所以这种情况不需要处理。

内部订单损失异常付款公司内部订单关系

接下来我们讲一下内部撤单异常,首先我们看一下为什么会出现内部撤单异常,这个其实跟我们的系统架构有关系。

如上图所示,第三方支付公司内表通常呈现支付订单与渠道订单1对N的关系。

支付订单中存储了外部商户系统的订单号,代表了第三方支付公司内部订单与外部商户订单的对应关系。

渠道订单代表的是第三方支付公司与外部渠道的关系,其实对于外部渠道体系来说,第三方支付公司就是外部的商户。

为什么我们需要设计这种关系?而不是使用下面的1对1关系?

如果我们采用上图中1对1的订单关系,如果第一次支付失败,外部商户可能会使用同一个订单号再次向第三方支付公司发起支付。

此时若第三方支付公司也使用相同的内部订单去请求外部渠道系统,有可能出现外部渠道系统不支持相同订单号重复请求的情况。

其实我们还有其他的方式,比如生成新的内部订单号,在原有的支付订单上更新内部记录,然后请求外部渠道系统。

但是这样会导致上次支付失败的记录丢失,不利于我们后续的统计。

事实上,第三方支付公司可能不支持用同一个订单号重新请求,但这种情况下,外部商户就需要重新生成一个新的订单号。

这样,第三方支付公司的系统就简单了,所有的复杂性都交给了外部商家。

但现实情况下,很多外部商户想要更改并生成新的订单号并不是那么容易的,所以第三方支付公司一般需要支持在支付不成功的情况下,用同一个外部商户订单号进行重复支付。

这时候我们就需要上面的1:N顺序关系图。

内部订单异常取消原因

当我们收到外部渠道系统返回的成功信息时,我们成功更新了渠道订单表中的记录。但是由于渠道订单表和支付订单表可能不在同一个数据库中,也可能不在同一个应用中,这样就可能导致支付订单表的更新失败。

由于支付订单表存储的是外部商户订单与内部订单的对应关系,如果支付订单不成功,外部商户是无法查询到支付成功结果的。

至此,渠道订单表已经创建成功,所以上述删除外部订单的方法不适用于内部订单。

解决方案 1:分布式事务

内部订单掉落异常简单来说就是因为支付订单表和渠道订单表无法使用数据库事务来确保两者同时更新成功或者同时失败。

解决方案 2:异步补偿更新

当发生内部订单掉单,即支付订单更新失败时,可以将该支付订单保存到内部订单掉单表中。

但是这里可能存在一个问题,我们无法保证保存到内部订单损失表这个步骤一定能够成功。

所以我们还需要定期查询,查出一段时间内还未支付成功的支付订单,但是渠道订单表中的支付订单记录已经支付成功,然后插入到内部订单丢失表中。

另外一个系统应用只需要定期扫描内部订单掉落表,成功支付订单,然后删除内部订单掉落记录即可。

这里需要注意的是,当支付订单表数据量较大时,定时查询可能会比较慢,为了不影响主库,可以将此类查询放到备库上进行。

订单损失汇总

上一节主要介绍了支付系统中的掉单异常,该类异常往往会导致用户实际被扣款了,但是商户的订单还在等待支付。

此异常若处理不好,将导致客户的用户体验不佳,还可能引发客户的投诉。

掉单异常通常有外部系统和内部系统两个原因。

掉单大部分是外部系统导致的,通过增加超时时间、查询掉单、接受异步通知等方式可以解决99%的问题,剩下1%的掉单只能靠第二天的对账来弥补。

由于系统内部异常导致的订单丢失是分布式环境下数据一致性的典型问题,对于该类问题我们不需要追求强一致性,只要保证最终一致性即可。

我们可以使用分布式事务来解决此类问题,也可以定期扫描状态不一致的订单,然后进行批量更新。

接下来我们来谈谈支付场景中的另一类异常。

重复付款异常

重复支付异常常见于网银支付、微信支付、支付宝等支付方式,需要跳转到支付网关页面(网银支付)或者跳转到钱包APP(支付宝、微信)才能异步完成支付的扣款。

这种支付场景下,需要通过接收异步通知才能知道支付结果,我们一般称之为异步支付。

另外我再补充一句:有了异步支付,那什么是同步支付呢?

其实同步支付就是指调用支付接口之后,可以立即返回支付结果,比如银行卡快捷支付/直付等支付都是同步支付。

当然也有一些比较奇葩的银行卡支付渠道,同步支付结果为接受成功,只能接受异步通知或者查询返回支付结果。

由于银行卡支付需要返回明确的支付结果,因此对于该类通道,其内部唯一的设计就是将异步转为同步。

好的,回到主题。

后端支付流程如下:

为什么会出现重复付款?

主要原因其实和上次内部订单异常是一样的,和业务表设计有关。

上次我们提到支付系统主表结构如下:

在此表结构下,只要支付订单不成功,商户可以复用同一个内部订单号来调用支付接口。

假设这样的场景,用户在收银台付款时,选择通过招商银行网银支付,点击支付按钮后,商户系统会调用支付公司的网银接口。

此时支付系统内部会创建一个支付订单和关联的渠道订单,并调用招商银行系统的接口。

则用户的浏览器页面就会打开一个新页面,跳转到招商银行网站。

此时,如果用户在收银台再次点击支付,则会再次调用支付系统接口。

此时由于支付订单已经存在,所以只会创建一个渠道订单记录,并调用招行系统的接口,此时用户的浏览器会再次打开一个招行网站。

如果用户在两个招行网银页面完成支付,就会出现重复支付的情况。

上述场景看似愚蠢,但在现实生活中却确实发生。

除此之外,博客园里的朋友还提到了以下情况:

重复付款 - 解决方案

解决重复支付异常问题主要有两种方案:事前解决方案和事后解决方案。

提前的主要目的是为了尽可能的防止用户重复支付,主要的解决方案是优化支付页面,提供尽可能多的提示。

第一个优化方法是从支付页面直接跳转到第三方/银行的网银页面,而不是打开新的页面进行跳转。

此方法可以避免用户意外打开两个网银支付页面,造成重复支付。

但是这里有一个问题,在银行网银页面支付成功之后,用户如何知道商户端的订单状态也是成功了呢?

其实很简单,现在网银支付接口一般都会有一个参数:

:同步跳转地址。

例如支付宝开发文档中有这个字段:

只要在接口中传递了这个地址,当支付成功的时候,页面最终会跳转到传递进来的地址,商户端就可以在该地址显示订单支付是否成功。

如上文所述,用户可能会使用浏览器的返回功能跳转至支付页面,从而造成重复支付。

这样的话,当返回到支付页面的时候,我们可以先从后台查询到这个订单的支付结果,如果支付成功的话,就直接显示成功页面。

第二个优化方法是重新打开一个页面跳转到银行的网站,我们可以在页面中添加弹窗提示,询问用户支付是否已经完成。

比如上述处理方法,当用户点击确认充值后,后台可以立即查询到订单状态。

解决办法我们以后再说。

其实解决办法很简单,发起内部退款,退还多付的款项。

支付系统内部可以有一个定时任务,定期扫描该支付订单下的多个成功渠道订单记录,然后选择对重复的支付渠道订单发起退款。

这种方式是支付公司系统内的操作,不需要商户主动发起指令。

订单无效异常

这种场景常见于电商购物、秒杀等购物场景,用户下单后页面会开始倒计时,用户需要在有效期内成功支付。

假设用户点击跳转支付宝,但是并没有立刻付款,而是停留了很久,直到下单的最后一秒才完成支付,但是此时订单已经因为时间已到而被自动取消。

这样就会出现用户的付款已经成功扣款,但是订单失败或者被关闭的情况。

还有一种情况是,用户在有效期内支付成功,但是由于网络、内部应用等问题,导致过了很长时间才收到支付结果的异步通知,这种情况下会因为时间过期而取消内部订单。

订单过期异常-解决方案第一个解决方案是将有效期发送到支付渠道。

一般支付接口都会有支付有效期字段,表示最晚可以支付的时间,如果超过时限没有支付,支付将会被关闭。

例如支付宝开发文档中有这个字段:

当然,正常情况下,如果没有上传的话,这个字段一般都会有一个默认的有效期,比如3天,这个是比较长的时间。

因此在调用支付接口时,可以将订单剩余有效期传入支付接口,这样如果用户在超时时间内没有完成支付,则支付失败。

第二种解决方案是内部发起退款。

此方案依然是事后备份方案,如果支付订单已经关闭,但是支付成功,则会发起内部退款,将款项退还给用户。

里面可以有一个计划任务,定期扫描支付订单已经关闭,但是支付成功的情况,然后发起退款指令。

终于

最后我会用思维导图的方式来帮助大家总结支付系统中可能遇到的异常情况。

分享