业务设计原则--防重设计


一 引言

《亿级流量网站架构核心技术》一书中关于业务设计的原则中第一个提到的就是防重设计,那我们就来好好的学习对应的防重设计是如何体现在复杂的支付系统中。
声明:本博客是个人的实际学习中,涉及到了网络中一些优秀的博客(优秀的博客阅读之后会让你豁然开朗,感谢大佬),附件如下:
作者:无敌码农
文章:移动端支付系统如何设计有效地防重失效机制
作者:歪脖贰点零公众号
文章:互联网金融系统——交易防重设计实战

一 正题

针对一个简单的外卖支付场景:
用户在外卖网站或App上购买了点了一份外卖,并通过微信支付进行付款,系统在收到用户支付完成的消息后,提示用户付款成功并派单给餐馆?
针对上述的需求场景,会有什么样的异常场景呢?与支付系统防重失效机制的设计有什么关系呢?
先具体看一下以上场景设计到的系统主要流程:(来源:移动端支付系统如何设计有效地防重失效机制
在这里插入图片描述
没想到,一个小小的支付流程竟然涉及到这么庞大复杂的处理过程,那么我们就从具体的操作流程一点一点的分析支付过程中需要解决的异常场景。
在上面的流程中,虽然从用户角度看可能只是几秒钟的事情,但实际上整个系统链是经历了一个比较长的调用过程。具体如下:

  • 用户在点外卖的过程中选择微信支付后,App会将支付请求发送给外卖后台系统,如果在整个外卖平台中,支付系统是一个独立的系统,则外卖业务后台服务会在生成业务订单后将支付请求发送给独立的“支付系统”进行处理;(这个就是后台系统和订单系统的拆分)
  • 此时支付系统作为独立的中间系统会处理外卖平台业务后台发送过来的支付请求,记录其业务订单号并生成对应支付系统自身的支付流水号,并对支付流水进行状态初始化(这里涉及一个业务订单号&支付订单号如何匹配问题,会在后面的讨论中阐述);
  • 支付系统调用微信统一下单接口进行预支付(这里的操作方式就是类网银式的支付方式,先进行预支付然后由用户跳到站外进行支付),并同步得到微信支付返回的预支付订单信息,支付系统此时需要更新支付订单为pending状态表示处于预支付状态;
  • 然后支付系统将预支付信息同步给调用方—外卖业务后台,外卖业务后台再同步给外卖App;
  • 外卖App会根据预支付订单信息通过客户端支付SDK唤起微信支付客户端,由用户操作微信支付客户端直接向微信支付发起付款动作,需要注意的是,此时调用链已经转移到了站外,实际上此时用户是否支付或是否支付成功,无论是支付系统还是外卖系统及App本身都是无法直接感知到支付结果的,需要逐层回调;
  • 微信支付会通过循环调用的方式主动将支付结果回调给支付系统,再由支付系统回调给外卖业务系统,最后在用户直接感知前由App主动查询外卖业务系统订单支付状态,同时提示用户支付成功或支付处理中这样的信息;
支付场景下遇到的问题

在这一系列的支付流程中,可能是会遇到这样的问题:
在点外卖后付款,微信端页提示支付成功了,但是外卖APP却始终不显示点餐成功(这个就是支付系统在异步回调的时候,订单回盘处理的工程中出现了问题,或者是没有及时的将订单支付结果通知到后台服务
,即使选择重新支付,页提示支付中,不允许重复支付,或者选择重新支付以后外卖App显示也显示点餐成功了,但是之前支付的钱却不见了

解决支付遇到的问题
异常补偿机制

具体来说,就是支付流程按照正常的流程走,通过采用旁挂定时的方式扫描系统中一定时间策略范围内的pengding状态的订单,通过微信提供的订单查询接口主动轮询,一旦支付状态查询到终态即刻触发系统回调,完成支付订单及业务逻辑的补偿;

另一方面,如果pengding状态订单通过轮询方式没有查询到最终状态则需要设置一定的重复轮询策略,例如5分钟、10分钟、20分钟、1小时、3小时、8小时、24小时这样,并在超过策略规定的时间及轮询次数后将支付流水更新为失效终态,并提供订单查询接口供业务平台完成自身业务订单逻辑的更新。

系统示意图如下:
在这里插入图片描述
通过旁挂式的方式,支付主流程会变的相对简单,只需要考虑正常的收单场景,对于很多业务实时性不太高的支付场景,这种方式也够用。但对于业务实时性要求非常高,并且对用户体验有极致要求的场景来说,这种方式显然也是存在明显问题的。

我们还是拿点外卖这件事来说,外卖后台在接收到用户通过App发送的点餐支付请求后生成外卖订单并将支付请求发送给平台支付系统,支付系统一般来说会首先进行订单防重判断(如何设计好这个防重判断),即已经发起过的成功支付/支付中的请求不被允许发起第二次,支付成功的交易不允许重复发起。是能够进行继续支付的操作

但是等待支付或支付失败的交易很多公司内部支付系统都会被要求允许发起二次付款,(某团和和某了么都是这么设计的)在外卖点餐环节,如果用户点餐了但是并没有立刻进行支付或者支付由于某种原因失败了,是可以重新发起付款的,在这种情况下,支付系统就会面临一个问题,由于不知道在进行预支付后用户是否完成了支付(这个感知??),防重逻辑就会变的迟钝,如果允许用户支付则可能出现重复扣款的问题,不允许则会影响用户体验,为了让整个机制变得合理,所以需要依赖于上述系统的补偿机制来进行回盘或失效处理。

遇到的问题:

  • 1,用户最终未支付,则系统安装一定的轮询机制进行后续的订单失效处理即可
  • 2、用户完成了支付,支付系统迟迟收不到微信的回调,通过逐步轮询的方式系统也会进行后续的订单回调补偿;但这里的问题是,如果异步补偿系统对订单的轮询不够及时(在支付订单量比较大的情况下,通过定时轮询的方式在时效性上较差),那么就会导致一个比较尴尬的情况,用户付完了钱,但是外卖订单很长时间显示未支付,无法进行派单,在轮询补偿系统完成回调后触发派单操作,但往往很可能已经过了饭点,并且很可能用户已经触发了申诉流程,外卖平台需要进行退款操作(增加客服工作量);
  • 3、则是用户当时并未及时支付,在订单失效前的某个时间,用户可能会选择重新付款,因为此时支付系统订单并未失效,会处于支付中状态,触发防重机制,无法再次发起付款;

实时支付流程优化设计
在这里插入图片描述
如上图所示,支付系统接收到前端发起的支付请求,系统首先需要进行防重判断,这里为了有效地防止并发请求,采用Codis锁的方式,即一笔业务支付订单请求发送到支付系统后首先获取Codis全局锁,如果存在锁则说明订单正常被处理/未被正常处理(所以当前该笔业务支付订单已经持有了这个锁),此时我们需要进行锁更新时间判断,如果锁的更新时间与系统当前时间差<=10s(可根据业务场景进行动态调整,即10s内同一笔商户订单号的支付请求不允许被多次发起),则很有可能此时系统正在处理这笔支付请求,应该正常进行防重处理;

相反,如果获取的Codis锁的更新时间与系统当前时间差>10s,则此时会存在两种情况,一种就是这笔支付订单没有被正常支付,是应该被允许重新发起支付的;另一种可能则是用户可能支付成功了,只是渠道在支付结果回调的过程中出了问题导致系统掉单。这两种情况混在一起,系统并不能立刻识别出到底属于何种情况。
这个问题是一个非常普遍和典型的问题,几乎很多公司都会遇到。
此时,支付系统有两种选择,一种选择是执行严格的防重策略,即要求所有对接支付平台的业务系统每次调用支付请求都必须生成不同的商户订单号支付系统对于同一个订单号无论支付成功与否都不允许将此商户支付单号重复发送给支付平台,这种方案与第三方支付公司的接口约定一致。

这种防重策略粗暴简单,本质上是将逻辑的复杂性传到给了业务系统,也会让业务变的难受,如果支付平台在后期经历过重建,需要推动业务线切换的话,也往往会招致业务系统的反对。

那么如何让支付平台本身来屏蔽这种复杂的细节,让业务尽可能无感知?
两个订单号

  • 商户订单号:业务系统发起的向支付系统发起支付请求是生成的在商户系统中唯一标识一笔订单的标记。
  • 支付订单号:业务系统向支付系统发起支付请求后,支付平台本身生成的系统唯一标识一笔支付流水的标记,并且是支付系统与第三方支付渠道交互的唯一流水标识。

为了达到以上目标需要在支付系统内部采用1:3(举例)的订单模型,即1笔业务订单号可以对应支付系统3笔支付订单流水,并且每笔支付流水允许被发起的条件是上笔支付流水数据库订单状态是未支付成功,并且需要在当前这笔支付流水重新生成后将上笔支付流水放入订单动态实效队列,进行快速失效处理。

之所以采用以上方式,原因在于超过3次时间间隔超过30s(策略可以根据业务实践进行动态调整)还未完成支付的情况,系统基本可以认定属于恶意点击行为,可以直接拒绝此笔业务订单重新发起支付了。

需要动态将上笔支付订单快速置为失效的原因在于,我们需要在内部设定一个逻辑:“如果支付订单处于实效状态并在后面接收到了第三方支付成功的回调,则需要系统自动发起该笔支付订单的原路退款逻辑,并确保该笔订单不会被通知到商户侧”。这种现象之所以出现,在于我们为了提高系统的实时性允许了少量重复扣款的情况发生,并进行了自动冲正逻辑。

当然,在细节的处理上我们是在当前流水发起前对上笔流水已经进行了一轮订单实时查询,如果结果为支付成功,则此次请求会直接返回支付成功(或者,也可以提示已经支付成功,App主动查询支付系统的订单状态来完成回盘)。

如果当前订单再次预支付成功,在同步返回预支付结果前需要更新Codis中订单锁的时间及发起次数。同时,在接受到第三方正常的支付成功回调后完成订单状态更新及商户通知后消除Codis锁。

上述策略,为解决防重&二次支付问题提供了一种方案,当然还有很多细节的代码逻辑是需要考虑完善的,例如,实时查询超时的策略、退款的触发时间、用户提示等。
此外,如果用户不再选择再次发起付款,系统中的存量订单也需要通过文中早些时候介绍过的异步补偿机制逐步将进行失效处理(具体策略机制可参考图示及之前的概述),只是如果在异步补偿机制过程中发现掉单的订单,是否正常回盘或自动给用户退款,就需要具体情况具体分析了。

防重设计(通用简单性的设计思路)

外卖后台在接收到用户通过App发送的点餐支付请求后生成外卖订单并将支付请求发送给平台支付系统,支付系统一般来说会首先进行订单防重判断,即已经发起过的成功支付/支付中的请求不被允许发起第二次,支付成功的交易不允许重复发起。
为什么需要防重设计?
比如你点外卖,付款后页面出现卡顿,但是此时,你的系统是已经在处理你当前订单发起的支付操作,然后在之后你又发起了一次订单支付操作,如果后端不进行重复订单的检验的话,实际上你是支付了两分钱,买到了一个东西。
由上图的4,支付系统进行接收请求进行防重&订单逻辑判断 ,在一般的后端处理中,是对于订单生成一定的token,并在真正进入支付之前,进行token验证,同时支付成功之后,可以进行token销毁
在这里插入图片描述

  • 1、在进入到需要防重交易的表单页面之前,请求后端生成token的服务,生成token并存储在后端,与该用户的请求绑定,便于后期在交易验证时与之比对,token返回到交易页面。
  • 2、携带token提交表单,在进入真正交易之前,做token验证(比如使用AOP),如果存在,则token正常,比对成功后销毁进入正常的交易功能。如果不存在,则证明token已经被销毁,为重复提交请求。

一般的防重设计的解决方案是在前端由JS控制提交表单按钮,提交后置灰(无法点击),但此方法也只针对小白用户有效,防范机制也不是很彻底,比如直接调用请求而非通过页面表单进行,比如JS校验代码清除等,可以绕过JS的置灰功能进行二次提交。

采用前端JS置灰防止重复提交请求,再加上后端token验证,可以更有效的防止关键交易的重复提交。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值