0 前言
日常生活中,从线下超市购物到网上外卖订餐、电商网购等,支付时时发生,无论是通过现金、POS机刷卡还是微信支付宝等第三方支付。 在线支付拥有及时、便捷、无缝的极致体验。 当然,也有少数时候体验不够流畅。 例如,早期我们在PC版上购买火车票时,当支付完成后,订单的支付状态往往无法及时更新。 会有一段时间。 延误,有时甚至长时间处于未付款状态。
支付过程中,由于各种原因(如外部通道处理问题、异步回调延迟),导致流程中途停止。 当用户看到订单仍未付款时,他们会感到困惑。 这时候就需要有一个机制来推动交易的完成。 本文以第三方支付系统中的订单补单机制为例,介绍一种更为通用的单据补偿模型。
1、三方支付系统简介 1.1 什么是三方支付
所谓第三方支付,是指与各大银行签订合同,独立于商户和银行,具有一定实力和信誉保证,为商户和消费者提供支付结算服务的第三方独立机构。 它是买方和卖方之间的可信第三方,扮演担保人和资金托管人的角色。 三方支付也可以称为虚拟账户支付。 消费者在第三方支付机构开立虚拟账户,并使用虚拟账户中的资金进行支付。 业内常见的第三方支付包括支付宝、微信支付、美团支付、京东支付等。
1.2 三方支付中的交易和支付系统
交易最直观的描述就是“一手付钱,一手发货”。 交易将使买卖双方形成债权债务关系。 交易的存在是支付发生的前提,用户通过某种支付方式完成交易。 交易是支付过程的驱动力。 根据具体场景组合不同的支付指令,完成交易资金的转移。
支付是一种交易处理资金流的工具,目的是偿还债权和债务关系; 支持多种支付方式(如银行卡支付、余额支付、优惠券组合支付、类似花呗的信用支付等),负责对接账户、记账、计费系统接收支付指令等资金处理能力推动资金互换完成。 将实际支付行为(实际资金)与内部核算(虚拟资金)结合起来,保证虚实一致。
第三方支付整体业务结构如图1所示,交易核心和支付核心从业务分工上看都属于“收单支付域”,具有收单、支付、退款充值、转账等共同功能。以及普通交易的撤回。 它还包括支持电子商务业务的组合订单支付、担保和账户共享的能力。 其中,交易和支付核心有异常检查和补偿模块,涵盖了所有业务的补偿流程,也是本文的主体部分。
图1 三方支付业务结构
2. 什么是补单?为什么需要补单?
支付过程中因各种链路异常导致交易中断。 此时,交易处于中间状态。 这种情况俗称“卡顺序”,即牌卡在那里,不向前移动。 进步。 还有一种情况是支付核心向通道发起扣款。 通道接受付款后,银行卡扣款成功。 但由于各种原因,并未向支付核心发起回调。 从而导致支付未完成,用户不享有相应权利。 然而,银行卡上的钱已经被扣除了。 这种情况称为“掉单”。
无论是滞单还是掉单,都是处于中间状态的订单。 补偿订单是指对中间状态的订单进行补偿,直至推进到最终状态(成功或失败)。 补单一般有两个要点。 一是补偿的有效性。 极端情况下,多次补偿也可能不成功。 你现在不能放弃。 你需要有一个后备机制。 另一个是补偿的及时性,因为交易正在等待中。 时间越长,用户体验越差。
交易核心和支付核心的补单功能相辅相成,在设计和实现上有一定程度的相似性。 我们以支付核心的订单补货为例,介绍异常订单补货机制。
3. 如何执行补充订单
本章首先看一下业务流程,讲解了实现订单补货所需的前提和基础,然后介绍了订单补货机制的演进路线、各个版本存在的问题以及下一版本如何解决。
3.1 资金操作的有限状态机及有限状态机幂等辨识
我们先以用户发起余额提现为例来说明一下业务流程,简化后如图2所示。
图2. 余额提取流程
首先生成支付订单,然后请求记账系统扣除用户账户下的余额,然后向外部渠道发起支付操作。 资金运作完成后,统一结果并更新单据信息。 最后还有一些对上游和下游的异步通知。 正式包括消息和 RPC 回调。
我们在数据库中记录每个关键资金操作的状态,如下表所示。 提款是指钱从哪里来,存款是指钱去向哪里,逆转是指回滚交易。 在提现场景中,资金从用户的支付账户中提取并转入用户的银行卡。 其他场景如充值(银行卡->用户支付账户)会有不同的资金流向。 表中最后两行粗体的场景还没有达到最终状态,所以我们需要进行补偿。
为了便于理解,我们这里省略了反转的相关操作。 余额提取流程的状态机转换如图3所示。
图 3. 状态机转换的余额提取流程
可重入性和幂等性保证
发起支付会涉及多次系统间调用,因网络原因导致通信超时是常见问题。 同时上游系统也可能会重新发起请求,这就需要我们的系统保证操作结果的幂等性。 少数用户还可能存在多个终端同时操作引起的并发请求,这就需要我们保证接口的可重入性。 除了服务本身之外,我们下游的依赖也需要保证它们的接口具有相同的能力。
我们先来说一下什么是可重入和幂等。
具体到业务上,幂等性是针对已经达到最终状态的支付而言的。 对于第一次未能获取最终业务结果的请求,再次调用的结果可能会不同(正在处理->处理成功或失败)。 那么我们如何保证业务流程的可重入性和幂等性呢? 让我们分别分解每个步骤:
生成付款单据:首先,付款单据可以使用业务方传递的外部订单号作为唯一索引,保证唯一性。 如果插入数据库时出现唯一索引冲突,则会查询现有数据进行幂等参数验证。 如果与当前两次请求的参数完全一致,则说明是重复请求。 您可以使用DB中的付款单继续后续流程; 如果不一致,将返回错误。 资金处理流程:账户和通道系统均保证其接口的幂等性。 我们还维护每个下游操作的状态,根据状态机决定是否继续,尽量不向下游输出重复的流量。 例如,如果支付订单已完成所有资金处理,并且状态机处于最终状态,则接口可以直接返回相应的结果。 更新支付订单信息时,先给支付订单加行级排它锁,然后再更新,保证多个并发请求只有一个会成功。 支付订单推进到最终状态后执行异步通知。
通过可重入性和幂等性的保证,我们可以广泛地重用前向过程来实现订单填写接口。
3.2 初始版本
一般来说,最常见的订单补货形式是设置定时任务定期扫表,完成业务补偿。 实现比较简单,但时效性不够,对于收款、转账等交易用户体验不好。 我们采用通过消息队列即时补偿的方法,如图4所示。补偿消费者不做补偿工作,而是解析消息并通过RPC调用支付核心暴露的补偿接口。 为什么不在消费者之间直接进行补偿呢? 这样做的主要原因是为了将逻辑整合到一个地方以便于维护。
图4 余额提现异常时订单补货流程
当然,补偿命令仍然可能失败,我们可以再次发送补偿消息。 但这样循环不能继续下去,所以需要设置最大重试次数,超过后不再发送。 当补偿多次失败时,通常是下游系统出现问题。 这时候我们就需要减慢补偿的频率。 随着重试次数的增加,每次补偿之间的时间间隔也会逐渐增大。 延迟消息传了过去。
以下是三个容易想到的问题:
如果异常消息发送失败,且上游没有重试机制,订单可能会永远挂在这里,如图5所示。补偿消费者请求支付核心补货订单不成功。 可能已经超时但实际补偿成功,或者请求根本没有通过,如图6所示。如果达到最大重试次数仍然失败,订单该怎么办?
图5 补偿消息发送失败
图6. 补偿消息消费失败
3.3 改进版本
针对问题1,如果重试后发送失败,我们通过引入异常消息表,将发送失败的消息丢入库来解决。 表中记录了订单号、当前重试次数、异常分类、记录状态、消息正文等字段。 如果图6中步骤4中的消息发送失败,订单将被放入DB中的异常表中,并设置计划任务来处理它。 就目前的可用性而言,异常数据很少发生。 如图7所示。
图 7. 消息生产/消费异常的改进版本 - 1
对于第二个问题,如果补偿消费者调用支付核心失败,补偿消费者会将其抛给上层。 利用梯度重试机制,当消费重试次数达到一定上限时,就会进入死信队列。 如图8所示,这种情况通常是由业务或网络问题引起的。 恢复后,可以将这些消息从死信队列中拉出并进行统一处理。
图 8. 消息生产/消费异常的改进版本 - 2
当然,还有更极端的情况。 向MQ和DB请求失败怎么办? 基于目前MQ和DB的可用性,同时发生的故障基本可以忽略,可以通过报警的方式进行人工干预。
关于问题3,如果重试次数超过最大次数,仍然补偿不成功,通常是下游依赖有问题。 在本例中我们也将其放入异常表中。
对于这两类漏网之鱼,需要支持单/批量支付单补偿的操作能力进行人工干预; 最好有一个自下而上的任务,在业务低峰时期运行,扫描业务单据表,完成一段时间内没有完成的任务。 责令赔偿。
另外,备份任务可能会造成消息的短期堆积,影响在线实时补偿流程的推进。 这可以通过使用独立队列来隔离。
3.4 最终版本
事实上,如果仅在异步通知操作中发生异常,则无需每次都重新走一遍完整的业务流程。 缺什么就补什么吧。 因此,我们将异常分为多种类型,将一些异步操作和业务处理分开,进行精细化处理:
图9以这些异常为例,说明了每种补偿类型的消息参数。
图 9. 分类补偿
我们最终的订单补偿系统如图10所示。它不仅通过即时消息保证了补偿的及时性,更是占据主动的最佳方式; 它还使用延迟消息重试、着陆失败消息异常表和掩盖任务来确保补偿的有效性。 性是万无一失的盾牌。 它不仅可以用于支付单据的补偿,还可以通过保证流程的可重入性作为通用解决方案,但不适合无状态和不可重入的业务形态。
图10. 异常补偿系统
4. 总结
本文首先介绍了什么是补单,然后基于三方支付系统的实现,全面阐述了补单机制的演变。 它最终演变成一种相对常见的异常处理模型,该模型基于消息队列、有限状态机和多任务。 提供业务层最终一致性保证机制供大家参考和修正。