支付状态与分布式一致性

在处理支付问题时,最难以处理的的是“支付状态未确认”。如果发生了支付失败(因为支付限额不够、用户在银行该签的东西没有签等等)并不会构成任何实质问题。用户在数秒种内看到支付失败的原因后就可以采取措施解决问题。但是支付状态未确认是真的会令人抓狂。

支付结果未确认

小明在赶火车前在候车大厅的小超市找到老板小强用微信支付买了20元泡面,然后看到微信提示

交易请求已经提交,请留意微信支付公众号下发的消息通知。支付状态未明确前,请勿重复支付。

这时,小明拿不到不到泡面。因为小强不知道小明是不是真的支付成功了。如果支付没成功就给了,小强可能会拿不到钱;反过来,小明也不能离开。因为一旦支付成功,但没拿到货,就白白付了钱。火车就要发车了,小明很郁闷。

也许小明可以直接忽略20块钱,不拿泡面就走人(我表示干过类似的事情)。但是在投资理财的场景下就会真的会骂娘。

比如,小明看到今天基金的价格不错,希望下单买10万。小明早早的就准备了10万块在银行卡里。基金交易是每个交易日15:00截止,所以他必须在当天15:00钱完成下单并保证支付成功,然而基金的实际成交价格要到晚上20:00左右才能最终确定。为了买的尽量合适,他在14:30时大概估算了一下成交价格然后赶在15:00钱下单支付。他用了理财公司的下单支付,但再一次看到了

您的支付状态未知,请您稍后再试。

的提示。这时离15:00很近了,他面临两难的抉择:

  • 小明可以选择什么都不做,但也许到了15:00也不知道支付是否成功。一旦15:00后发现支付失败(资金对账会收低,下文提),他就会会与当前这个交易日失之交臂,也就白白的放过了一个抄底的机会;
  • 小明可以选择撤单,15:00前撤单可以保证它的交易一定是无效的,所以他最终能拿到返款。问题是他没法立刻拿到钱。因为能拿到钱的前提是商户已经确认了得到了钱。如果都无法确认钱到了账上,也就无法返还。那么小明为了做成这笔交易就必须在撤单之后,在数十分钟内再凑10万,另下一单。这也许并不容易。谁会常备数万元现金预备这种情况呢?

为何会发生支付未确认

这是因为一般电商,理财等交易系统要通过互联网接入支付通道,再由支付通道接入各大银行。中间层层转发,每个阶段都有可能超时。即某个消息丢掉了,无法确认到底是请求没发出去还是返回没收到。

此外,整个调用链条的任何一环都可能有bug。在最差的情况下,我遇到过支付通道中断数个小时的情况。此时,貌似除了降级整个系统停止银行卡交易,没有任何别的办法。

-------------            -------------           -------------          -------------
|    用户    | --------->|   交易系统   | -------->|  支付通道   | ------->|   银行系统  |
-------------            -------------           -------------          -------------

其实所有的系统,只要是分布式系统,就无法规避分布式一致性问题。只不过支付因为涉及到钱,所以显得比较敏感和重要。

一个简单的结论是:支付状态未确认是普遍存在的,是不可消除的

对此,我们能做到的只有三件事

  • 尽量减少支付未确认发生的可能性
  • 在发生支付未确认时,尽快拿到明确的支付结果
  • 通过业务设计规避支付未确认

减少支付未确认发生的可能性

国内有很多支付通道,每个支付通道又有多种支付接口和交互方式。因为单一支付通道总是会不定时抽疯,一般应该总是考虑接入2~3个通道,然后做两件事情:

  • 对支付通道的成功率做出比较实时的统计
  • 实现“支付路由“。即可以通过动态配置的形式,根据统计解结果决定当前用哪家支付。

支付路由的粒度可以做的很细致,这要看开发资源是否足够。比如可以定制出”什么银行“的“购买什么商品”,“支付金额满足什么条件”时,应该用哪家通道。这样就能极大的避开因为某个特定通道临时挂带来的损失。当然,支付路由的开发代价也很高。稍一不慎就会带来某笔支付找不到合适的支付通道造成支付直接失败的问题。

在做支付路由时,要特别留意支付信息认证。部分支付通道要求一定要与用户直接完成一次手机验证码认证的过程(支付绑卡短信验证)才会允许用户使用该通道。所以在做路由时要特别留意避免让用户因为底层用的通道不同而反复进行短信验卡。对于业务来说,这简直就是转化率的大杀器。

尽快拿到支付结果

不同支付通道的接口返回支付结果的形式多样。但都支持一件事情——资金对账。即会存在一个批量接口,把一段时间内(一般是一天)的支付记录汇总后让交易系统比对。大致的比对方法如下所示。

交易系统的结果

支付通道的结果

最终resolve的结果

支付成功

支付成功

支付成功

支付成功

支付失败

WTF?系统有bug吧。赶紧找找用户的钱去哪了。或者给个补偿?

支付失败

支付成功

把钱还给用户吧,订单估计已经作废了。

支付失败

支付失败

支付失败

支付未确认

支付成功

支付成功

支付未确认

支付失败

支付失败

对于支付状态不一致的情况下,可以case by case的处理。极端情况下,支付通道在支付时给的结果和资金对账结果不一样(先报成功,再报失败;或者反过来)。这时要以对账结果为准。当然,如果发生就可能会涉及到赔款,所以要留足证据到时候扯皮:)。

通过资金对账,所有的不一致都可以最终解决

但是“资金对账”要数小时或者1天才会发生一次。如果希望用户不骂娘,还是得想办法把支付状态的获取弄快点。

还记得上面小明买基金的故事吗?他也许只有几分钟可以等。

为此,就需要对那些支付状态不确认的交易记录不断的尝试从支付通道的获取结果。这种模式一般就三种:

  • 推拉结合

所谓是指支付通道将支付结果推送给交易系统方。但是有的支付通道只会推一次,不管收没收到就不推了。被miss掉的消息只能靠资金对账补齐。有些支付通道会通知多次,直到支付发起方确认“我收到了”。

所谓是指支付发起方不断的去反复读取支付状态,直到确切的知道了支付结果。这种方案的有效性也取决于支付通道自己是否能拿到支付结果。例如,如果是银行系统出现了问题,支付通道自己也没有支付状态的话,就无能为力。银行系统往往更多考虑的是是否符合国内相关标准,而非让系统更好用,所以往往只保证资金对账时一定能给个结果;至于实时性,出问题时只能呵呵。

推拉结合就是指上面两种方案同时执行。这种方式效果往往最好,但也最难实现。开发者要小心翼翼的处理支付状态的状态机,在推或者拉的任一方动作的到支付结果后就终止另外一方的再次尝试。如果用了多进程/多线程/分布式服务的技术来实现这个功能,就要更加小心。一定要避免类似于用户充值支付了100元,结果在账上却给了200元的情况。这时,根据交易订单ID(或者等价的dedup key)做幂等实现处理的"at most once"语义极为关键。配合DB的unique key+支付状态改动的乐观锁,可以得到不错的效果。对于分布式的场景,可以考虑基于分布式锁的实现(BTW,DB的锁实际上也可以当分布式锁使用)。

其实,如果能交易通道能够提供一个pay-or-get-pay-result的语义的支付接口是最好的。这种接口的特性是:

  • 如果这个支付还没做,就支付,并返回支付结果;
  • 如果只个支付已经做了,就返回其支付结果

这样用户/交易系统方就可以反复重试调用支付接口来拿到支付的最终结果,但是又不用惧怕多次支付。不过很可惜,因为支付的敏感性和各家支付机构完全没有动力去大改这么核心的系统。基本上找不到支付接口实现了pay-or-get-pay-result语义,至少我从未见过。

当然,由交易系统方自己实现一个支付流水,再配合unique payment key,是可以自己拼凑出来一个pay-or-get-pay-result的。只不过其意义也仅限于让上层业务代码更好写(不需要一堆的if,有的要pay,有的不要pay,有的取已有支付结果等等;而是无脑调用pay-or-get-pay-result即可)。毕竟这个做法无法触及到真正知道支付状态的银行系统。

用业务手段避开支付未确认

因为支付是发生在银行系统(含用户的账户和交易系统账户)、支付通道和业务系统三方的交互过程。链条越多,越容易出问题。为此可以做两种尝试,将这个问题简化:

  • 拆分下单和支付,将支付视为一个独立的过程

即,下单是下单,支付是支付。订单只要下完,可以给一个非常宽泛的时间范围来(比如几天)来让用户反复重试获取支付结果,如果已经明确失败了就可以重新支付,直到成功。这种做法适合对支付没有硬时间要求的场景。目前大的电商如京东、淘宝也都是这么做的。但对于大部分需要考虑15:00交易日必须给出明确支付结果的理财交易系统就不适用。所以,另外一个做法是

  • 做一个交易系统的资金账户,让用户提前充值

所以你会发现基本上所有的理财类APP都会有一个可以让用户充值的地方,比如XX宝,比如资管账户等。充值一旦完毕,所有的数据,包括用户有多少钱可以用,都会记录在交易系统这边。这时再去“支付”买东西就完全与支付通道没有任何关系。此时交易系统可以在单DB下用一个transaction完成“扣减账户金额”、“扣件库存”、“增加用户持有资产”等一系列动作。一个分布式问题被简化为了一个transaction的问题。

但是,这样做的坏处是,用户本来一步可以执行的操作要做两个步骤(充值-买入)。所以为了提高转化率,这种方式一般都会搞如“充值打折”,“充值返券”一类的诱导性的产品设计,引导用户舒舒服服的走完流程。

结论

只要是个分布式的问题,就不可能用单一技术来解决,支付也不例外

对于支付状态的问题,一定要靠资金对账来兜底。在此基础上,能做多快的数据补偿,让支付结果明确的显示在用户界面上,要完全看

  • 支付的场景是什么,有什么业务上的歪招可以绕开部分问题
  • 接入的支付通道的接口是怎样的,能否支撑足够好的同步+异步的信息补偿的代码
  • 开发资源是不是足够去处理那些边界的问题

可怜的小明,不管开发者用什么语言,用户用什么框架,用什么技术栈,都没有办法根绝这个问题。非常抱歉。

其实不仅仅是支付,业务系统充斥着一致性与可用性的矛盾。我有另外一篇文章 如何避免下重复订单,可以对照一起看看。


本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏金融民工小曾

【支付系统设计从0到1】支付业务调用方式有哪些?为什么微信公众号支付采用JSAPI方式?

对于大多数做支付系统设计的同学来说,对于支付渠道提供的调用方式都不陌生,相信大家对这些支付渠道的调用方式也了如指掌。

1522
来自专栏安恒信息

威胁告警:大量ubnt设备被植入后门

近期,安恒安全研究团队监控到大量利用弱口令对22端口进行暴力破解的攻击。经过安全团队详细分析,我们发现网络上大量的ubnt设备的存在弱口令,并且已经被黑客使用自...

5016
来自专栏西枫里博客

关于ICP备案你所不了解的那些事

原打算这篇文章是写成正常的网站备案指导步骤的,在写的过程中,我发现其实各大IDC厂商的的帮助信息都已经非常明确具体了,甚至细分到每个省区有不同的细则都标识的很清...

5763
来自专栏安恒信息

【连载】2016年中国网络空间安全年报(四)

2016年中国网络空间安全年报 1.5. 网站安全管理问题深度分析案例 本章节重点选取站点管理中其中较为典型的三类问题,如基础备案信息、钓鱼站点、僵尸站点等...

3257
来自专栏SAP最佳业务实践

从SAP最佳业务实践看企业管理(188)-FI-160现金管理

image.png FI160现金管理 现金状态概览提供有关银行帐户当前财务状态的信息。这是现金集中的起点,其中将不同银行帐户的余额集中到一个目标帐户,考虑最小...

3505
来自专栏魏艾斯博客www.vpsss.net

Bluehost 美国站和 Bluehost 中国站的区别

1623
来自专栏腾讯云安全的专栏

腾讯云发布一键封堵工具,完美规避 NSA 黑客工具影响

2307
来自专栏安恒信息

《公安机关信息安全等级保护检查工作规范(试行)》解析

《公安机关信息安全等级保护检查工作规范(试行)》是2017年9月份发布的依据《信息安全等级保护管理办法》为规范公安机关公共信息网络安全监察部门开展信息安全等级保...

1853
来自专栏SAP最佳业务实践

SAP最佳业务实践:FI–应付账款(158)-4 FB60过帐供应商发票

4.4 FB60过帐供应商发票 您从供应商处收到发票并在系统中进行过帐。供应商的物料发票被过帐到物料模块中,有关详细信息,请参见文档。您还可以为除物料(如下文中...

37210
来自专栏域名资讯

“伦理”lunli.com双拼域名85000元结拍

拼音域名是指用汉字的拼音做域名的前缀,这样的好处是用户一看就明白这个域名的意思从而知道网站的内容,中文搜索引擎也对拼音域名很友好,拼音域名在关键字排...

22010

扫码关注云+社区

领取腾讯云代金券