支付系统的要求:安全、高效。安全是基本,高效是追求。

要达成两个目标,难免会遇到各种坑,下面挑几个典型的问题来讲述,并附上简单的应对方案。

请求超时问题

网络的可靠性要依赖硬件,所以只要是网络调用,必然要考虑超时问题,另外因为支付系统一般内部验证操作多,请求处理时间长,比一般系统超时的概率更大。

支付系统内的每一个请求都应该谨慎处理,而对于无法确定结果的超时请求更不能轻易确定终态,绝对不能像一个简单的网页请求一样重试一次。

一般采取保守策略,将交易状态保持在一个无害的默认状态(处理中或未支付),等待下次触发处理。

请求超时本身易处理,但它导致的后续问题会很多,下面会提到。

终态判断处理问题

返回码映射

终态的判断应该是支付系统内最重要也是最容易踩坑的地方,这个处理的复杂程度真的太依赖三方系统的状态码设置了。由于成功和处理中的状态只有一种,而错误则会有各种各样的原因,有的错误可以重试,有的错误是系统错误。分清交易失败的原因,关系到系统如何下一步处理交易,所以错误明细码的设计十分重要。

对于一个返回码设计良好的系统,如微信、支付宝,有业务结果码和明细错误码之分,我们进行终态判断和返回码映射时,可以首先以业务结果码为准,在业务结果为失败时,再去检查明细错误码。

而一个设计不那么好的系统,将业务结果码和明细错误码混淆在一起,判断结果就比较坑,要么将错误码列出对比,要么用很危险的else

此问题无法真正避免,只能给出谨慎映射,多向三方系统求证的建议。

查询无交易记录

无交易记录应该是最危险的返回码了,偏偏因为偶发的网络波动,交易请求受理超时,这个码还不可避免。 正常逻辑的情况下,无交易记录是没发到三方系统,当然是失败,可是如果在代付交易中,三方系统告诉你,别轻易置失败,万一你参数传错了呢,钱付出去我们可不赔哟~你还那么有自信么。。。

解决方案中最保守的方式当然是作为处理中来处理,然后人工介入处理,这个只能用在交易量不是太大,网络偏稳定的情况下,目前我们只在代付交易中使用此策略。

另外一种方式是搭配请求时的响应信息来判断,如果三方系统响应信息为成功时,查询为无此交易,那自然是参数或系统逻辑等问题,迅速报警通知处理。如果请求受理时为超时,那么便可以认为是网络问题没有发送成功了,有时候还是要对自己的代码有一些信心的。

交易及时性问题

交易及时性不是一个很严重的问题,甚至在支付系统中,太有及时性的交易还会使用户不太放心。但作为一个程序员,追求效率是天性嘛,我们还是希望尽早获取到交易结果,但这也可能导致踩坑。

太早的查询

查询太早导致问题会出现在两种场景:请求超时、三方系统设计问题。

  • 请求超时:请求超时时,系统在过了超时时间后断开连接不再阻塞,立刻发起查询请求的话,三方系统可能刚接收到请求,正在进行参数验证,数据还未落地,此时会收到无此交易的响应,我们将交易作为失败处理后,交易可能在之后成功。所以查询一定要有延迟,一定要给三方系统足够的时间来处理交易。
  • 三方系统设计问题:如xx,在受理交易时使用了中间件,中间件挂掉后,我们查询无此交易,但他们重启中间件后又处理交易,竟然又成功了。这个最好在之前能问清三方系统的处理方式,并针对性地设置查询延迟。

频繁的查询

太多频繁的查询是无意义的,交易正在三方系统中处理,查询不会使交易被迅速处理,还会造成网络资源和系统资源的浪费,如果你还记得与三方系统的每一次交互都要重视,那么查询日志也没法看了。

解决此问题,要:

  • 避免“过早”的查询,这要考虑三方系统的处理速度;
  • 合理设置查询时间间隔,一些交易需要更长的处理时间,可以设置梯度时间间隔;
  • 处理无意义的查询,如查询“无此交易”,那么进行多少次结果都不会变,再进行查询就是无意义的;

隔日账问题

隔日账问题在对账过程中不可避免,由于服务器时间有差异,交易处理也需要时间,在凌晨附近发生的交易可能会遭遇此问题,这会给对账造成一定的困扰,但合理的处理方式不会有太大的问题:

自己系统与三方系统对账文件不一致时调用查询接口在缺失交易的系统内查询,先确认交易的存在,再分析交易时间。如在隔日附近,则暂不处理,待次日对账文件的对比。

如某一系统内交易不存在,或交易不太可能会发生隔日账问题,这便需要系统之间人工来处理了,不过这不也是对账的意义所在么。

并发效率问题

并发锁

并发问题在所有系统内都会存在,只是支付系统内处理不好后果会很严重,处理方式一般是事务、互斥锁。

支付系统单系统内使用这些方式也没有问题,只是锁的粒度会略大,至少需要保证一个模块内交易处理的原子性。 分布式系统内就要考虑分布式锁了,这些业内也都有很多解决方案了。

只是加锁就意味着效率损耗,合理拆分出交易核心模块,并对这些模块添加锁。另外使用合理的“进程-数据”分配方式,也会减少锁冲突。

幂等

保持交易中的幂等很重要,它是避免重复支付的基石。 即使系统设计完全,我们还是要追求业务逻辑上的幂等,这也就意味着更多的查询确认,同时意味着效率下降。

效率下降不可避免,我们可以使用缓存来降低效率下降的幅度,在缓存中设置交易状态标识,对交易状态标识异常的交易再去数据库查询。

异步拆分尴尬

异步可以抗并发,提高效率,放之四海皆准。支付系统对异步的依赖更强是因为支付系统由于其处理流程冗长更易达到效率瓶颈。

面对异步我们首先要解决的问题是异步拆分的粒度问题,粗粒度的拆分效率能提升的效率有限,细粒度的拆分调控起来不易,处理异步拆分的粒度,看交易量吧,不做过度设计。

进行异步拆分时,每一步都需要一个触发进程,此进程可以是常进程轮询,也可以是cron进程,事件机制来触发自然是更好的,但它对消息队列的要求很高,设计也较复杂。

除此之外还需要一个确认进程,确保下一步的进程顺利接收处理了,在后续进程受理失败时,能够及时重试处理。

测试坑

测试是开发中必不可少的步骤,自己测和测试来测,总要完全走一遍流程才敢放心上线。

支付测试略坑了: 首先测试环境的布置,支付系统牵涉到多个三方系统的交互,靠谱的系统都会提供测试系统,可是难免有些系统不提供测试环境,或者测试限制颇多,限支付行,限金额等,还要提防其测试系统忽然就挂了。然后是线上测试,更要小心翼翼,一个不慎就是资金损失。

为了提供良好的测试环境,我们引入“MOCK”功能,mock 中文意思为“模仿”,即通过模拟三方系统的返回值来测试本系统的稳定性。

但 mock 的代码侵入性略强,完整的 mock 模块必然有if else语句的存在,由于支付相关的系统较多,要搭建完整的 mock 系统不容易,单点 mock 需要各处埋点。整体 mock 的又不便于测试特定功能。

小结

支付的坑包括但不限于本文介绍的这些,可能还会有其他奇怪的问题,文章没有介绍到。

若想尽量避免支付系统的坑,那么一定要保持着保守的态度,将状态或交易保持无害。有些需要事务操作,但无法使用典型事务的场景,将次要的一开始执行,即使出了问题,有重试、回滚等操作,也不会造成影响。

支付总结暂时到此为止。