赞
踩
《亿级流量网站架构核心技术》一书中关于业务设计的原则中第一个提到的就是防重设计,那我们就来好好的学习对应的防重设计是如何体现在复杂的支付系统中。
声明:本博客是个人的实际学习中,涉及到了网络中一些优秀的博客(优秀的博客阅读之后会让你豁然开朗,感谢大佬),附件如下:
作者:无敌码农
文章:移动端支付系统如何设计有效地防重失效机制
作者:歪脖贰点零公众号
文章:互联网金融系统——交易防重设计实战
针对一个简单的外卖支付场景:
用户在外卖网站或App上购买了点了一份外卖,并通过微信支付进行付款,系统在收到用户支付完成的消息后,提示用户付款成功并派单给餐馆?
针对上述的需求场景,会有什么样的异常场景呢?与支付系统防重失效机制的设计有什么关系呢?
先具体看一下以上场景设计到的系统主要流程:(来源:移动端支付系统如何设计有效地防重失效机制)
没想到,一个小小的支付流程竟然涉及到这么庞大复杂的处理过程,那么我们就从具体的操作流程一点一点的分析支付过程中需要解决的异常场景。
在上面的流程中,虽然从用户角度看可能只是几秒钟的事情,但实际上整个系统链是经历了一个比较长的调用过程。具体如下:
在这一系列的支付流程中,可能是会遇到这样的问题:
在点外卖后付款,微信端页提示支付成功了,但是外卖APP却始终不显示点餐成功(这个就是支付系统在异步回调的时候,订单回盘处理的工程中出现了问题,或者是没有及时的将订单支付结果通知到后台服务)
,即使选择重新支付,页提示支付中,不允许重复支付,或者选择重新支付以后外卖App显示也显示点餐成功了,但是之前支付的钱却不见了
具体来说,就是支付流程按照正常的流程走,通过采用旁挂定时的方式扫描系统中一定时间策略范围内的pengding状态的订单,通过微信提供的订单查询接口主动轮询,一旦支付状态查询到终态即刻触发系统回调,完成支付订单及业务逻辑的补偿;
另一方面,如果pengding状态订单通过轮询方式没有查询到最终状态则需要设置一定的重复轮询策略,例如5分钟、10分钟、20分钟、1小时、3小时、8小时、24小时这样,并在超过策略规定的时间及轮询次数后将支付流水更新为失效终态,并提供订单查询接口供业务平台完成自身业务订单逻辑的更新。
系统示意图如下:
通过旁挂式的方式,支付主流程会变的相对简单,只需要考虑正常的收单场景,对于很多业务实时性不太高的支付场景,这种方式也够用。但对于业务实时性要求非常高,并且对用户体验有极致要求的场景来说,这种方式显然也是存在明显问题的。
我们还是拿点外卖这件事来说,外卖后台在接收到用户通过App发送的点餐支付请求后生成外卖订单并将支付请求发送给平台支付系统,支付系统一般来说会首先进行订单防重判断(如何设计好这个防重判断),即已经发起过的成功支付/支付中的请求不被允许发起第二次,支付成功的交易不允许重复发起。是能够进行继续支付的操作
但是等待支付或支付失败的交易很多公司内部支付系统都会被要求允许发起二次付款,(某团和和某了么都是这么设计的)在外卖点餐环节,如果用户点餐了但是并没有立刻进行支付或者支付由于某种原因失败了,是可以重新发起付款的,在这种情况下,支付系统就会面临一个问题,由于不知道在进行预支付后用户是否完成了支付(这个感知??),防重逻辑就会变的迟钝,如果允许用户支付则可能出现重复扣款的问题,不允许则会影响用户体验,为了让整个机制变得合理,所以需要依赖于上述系统的补偿机制来进行回盘或失效处理。
遇到的问题:
实时支付流程优化设计
如上图所示,支付系统接收到前端发起的支付请求,系统首先需要进行防重判断,这里为了有效地防止并发请求,采用Codis锁的方式,即一笔业务支付订单请求发送到支付系统后首先获取Codis全局锁,如果存在锁则说明订单正常被处理/未被正常处理(所以当前该笔业务支付订单已经持有了这个锁),此时我们需要进行锁更新时间判断,如果锁的更新时间与系统当前时间差<=10s(可根据业务场景进行动态调整,即10s内同一笔商户订单号的支付请求不允许被多次发起),则很有可能此时系统正在处理这笔支付请求,应该正常进行防重处理;
相反,如果获取的Codis锁的更新时间与系统当前时间差>10s,则此时会存在两种情况,一种就是这笔支付订单没有被正常支付,是应该被允许重新发起支付的;另一种可能则是用户可能支付成功了,只是渠道在支付结果回调的过程中出了问题导致系统掉单。这两种情况混在一起,系统并不能立刻识别出到底属于何种情况。
这个问题是一个非常普遍和典型的问题,几乎很多公司都会遇到。
此时,支付系统有两种选择,一种选择是执行严格的防重策略,即要求所有对接支付平台的业务系统每次调用支付请求都必须生成不同的商户订单号,支付系统对于同一个订单号无论支付成功与否都不允许将此商户支付单号重复发送给支付平台,这种方案与第三方支付公司的接口约定一致。
这种防重策略粗暴简单,本质上是将逻辑的复杂性传到给了业务系统,也会让业务变的难受,如果支付平台在后期经历过重建,需要推动业务线切换的话,也往往会招致业务系统的反对。
那么如何让支付平台本身来屏蔽这种复杂的细节,让业务尽可能无感知?
两个订单号
为了达到以上目标需要在支付系统内部采用1:3(举例)的订单模型,即1笔业务订单号可以对应支付系统3笔支付订单流水,并且每笔支付流水允许被发起的条件是上笔支付流水数据库订单状态是未支付成功,并且需要在当前这笔支付流水重新生成后将上笔支付流水放入订单动态实效队列,进行快速失效处理。
之所以采用以上方式,原因在于超过3次时间间隔超过30s(策略可以根据业务实践进行动态调整)还未完成支付的情况,系统基本可以认定属于恶意点击行为,可以直接拒绝此笔业务订单重新发起支付了。
需要动态将上笔支付订单快速置为失效的原因在于,我们需要在内部设定一个逻辑:“如果支付订单处于实效状态并在后面接收到了第三方支付成功的回调,则需要系统自动发起该笔支付订单的原路退款逻辑,并确保该笔订单不会被通知到商户侧”。这种现象之所以出现,在于我们为了提高系统的实时性允许了少量重复扣款的情况发生,并进行了自动冲正逻辑。
当然,在细节的处理上我们是在当前流水发起前对上笔流水已经进行了一轮订单实时查询,如果结果为支付成功,则此次请求会直接返回支付成功(或者,也可以提示已经支付成功,App主动查询支付系统的订单状态来完成回盘)。
如果当前订单再次预支付成功,在同步返回预支付结果前需要更新Codis中订单锁的时间及发起次数。同时,在接受到第三方正常的支付成功回调后完成订单状态更新及商户通知后消除Codis锁。
上述策略,为解决防重&二次支付问题提供了一种方案,当然还有很多细节的代码逻辑是需要考虑完善的,例如,实时查询超时的策略、退款的触发时间、用户提示等。
此外,如果用户不再选择再次发起付款,系统中的存量订单也需要通过文中早些时候介绍过的异步补偿机制逐步将进行失效处理(具体策略机制可参考图示及之前的概述),只是如果在异步补偿机制过程中发现掉单的订单,是否正常回盘或自动给用户退款,就需要具体情况具体分析了。
外卖后台在接收到用户通过App发送的点餐支付请求后生成外卖订单并将支付请求发送给平台支付系统,支付系统一般来说会首先进行订单防重判断,即已经发起过的成功支付/支付中的请求不被允许发起第二次,支付成功的交易不允许重复发起。
为什么需要防重设计?
比如你点外卖,付款后页面出现卡顿,但是此时,你的系统是已经在处理你当前订单发起的支付操作,然后在之后你又发起了一次订单支付操作,如果后端不进行重复订单的检验的话,实际上你是支付了两分钱,买到了一个东西。
由上图的4,支付系统进行接收请求进行防重&订单逻辑判断 ,在一般的后端处理中,是对于订单生成一定的token,并在真正进入支付之前,进行token验证,同时支付成功之后,可以进行token销毁
一般的防重设计的解决方案是在前端由JS控制提交表单按钮,提交后置灰(无法点击),但此方法也只针对小白用户有效,防范机制也不是很彻底,比如直接调用请求而非通过页面表单进行,比如JS校验代码清除等,可以绕过JS的置灰功能进行二次提交。
采用前端JS置灰防止重复提交请求,再加上后端token验证,可以更有效的防止关键交易的重复提交。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。