分享是最有效的学习方式。
博客:blog.ktdaddy.com/

故事

这是一个真实事件,三年前老猫负责公司的支付资产业务。为了响应上级号召,加强国央企之间的合作,公司新谈了一个支付对接的渠道(当然这个支付渠道其实很冷门的,也是为了对接而对接,具体哪个渠道也不方便透露),由于原始支付系统的第三方支付可拓展性设计得还不错的,所以老猫对接的也是比较快的,熟悉对方的对接文档之后对着编码就好了,差不多花了三天的时间就完成联调了。一切看似很顺利地上线了。

时隔几天,收到了一个快递包裹,是一袋价值53块钱的“原皮腰果”,当时诧异,翻看了各大消费平台,都没有之前的下单记录,后来和媳妇确认了一下,她也没有下单。“难道是某个崇拜哥的小姑娘送的?不能吧”当时心里美滋滋地yy着。

不过之后的一个客诉问题,引起了老猫的重视,老猫排查下来发现一个很重大的问题,钱款的扣除和实际的订单状态对不上。说白了就是订单完结了,但是账户资产并没有完成扣除。我瞬间明白了之前那个“原皮腰果”是怎么回事儿了,当时在生产测试渠道的时候,在公司内部商城提交了订单,但是并没有付款,然而订单却成功了。

再三确认之后,确实存在这一问题。一瞬间整个人心态崩了,头皮发麻,口干舌燥,心脏“突突突”。怎么办?怎么办?生产还不知道涉及多少单子,没办法,兜不住了,先把这件事情往上抛吧(向上级领导汇报)。

具体原因是什么呢?我们来看一下对接第三方支付的大概时序流程。

一个小小的逻辑判断符,资损几万块

我们一般在对接第三方支付渠道的时候会有上面一些基本流程。

1、当我们内部生成待支付单之后会请求外部第三方支付渠道,此时第三方支付渠道内部会生成待支付单。

2、我们第三方支付单创建成功返回之后,一般内部系统会唤起收银台,然后用户确认支付。

3、第三方支付成功返回消息之后,整个支付就算已经完结了。

但是问题就出在了第三方支付渠道还有一个定时异步通知的任务,并且我们也对接了这个接口。这个异步通知的功能主要是会定时告知我们支付系统第三方支付单的状态。而且无论成功与否,都会轮询告知,例如,如果是待支付,对方会告知状态是0,如果已完成支付,对方会告知状态是1。我们拿到支付结果之后就会执行后续的订单完成流程。

收到异步通知之后的代码处理判断如下:

事故代码

    //校验交易状态
    if (!Objects.equals(notifyModel.getTradeStatus(), TradeStatus.SUCCESS.getCode())
            && amtConvertY(notifyModel.getTradeAmt()).compareTo(thirdPartyCharge.getAmount()) != 0) {
        LOGGER.error("交易状态不正确:{}", notifyModel.getOutTradeNo());
        throw new BusinessRuntimeException(Errors.PAY_NOTIFY_IS_FAIL);
    }

正确代码

    //校验交易状态
    if (!Objects.equals(notifyModel.getTradeStatus(), TradeStatus.SUCCESS.getCode())
            || amtConvertY(notifyModel.getTradeAmt()).compareTo(thirdPartyCharge.getAmount()) != 0) {
        LOGGER.error("交易状态不正确:{}", notifyModel.getOutTradeNo());
        throw new BusinessRuntimeException(Errors.PAY_NOTIFY_IS_FAIL);
    }

相信眼尖的小伙伴已经发现了,其实就是“||”和“&”的区别。第一种情况当对方告知状态为0的时候,其实并不会被拦截掉,而是直接走了往后的流程,于是悲剧就发生了。

直接说一下最终的处理,最终其实还是比较幸运的。由于,我们本身已经对接了微信以及支付宝的支付渠道,再加上这个渠道的支付使用的频率还是非常少的,很多用户不太会使用这个渠道进行支付,所以最终盘算下来整个的资损金额差不多是3w左右,另外的是其中有个不幸中的万幸。是因为老猫在这之前做了一套资金追讨系统,该系统可以定位出那些用户“空手套白狼”了。并且能给这些用户生成对应的待支付订单,用户可以通过这些待支付订单最终完成资金的补偿付款,最终完成了资金了追讨。所以事后,完全盘点之后发现一共的资损是1600左右。

最终,也算是有惊无险。但是这次的经历给了老猫上了一课。

下面总结一下我们在做支付账务系统的过程中应该如何进行资金安全相关的设计,最终做到防患于未然。

一个小小的逻辑判断符,资损几万块

资金安全设计

针对资金安全的问题,不限于通过技术手段避免资损,其实很多时候我们还需要结合数据核对、监控等措施,做到快速发现资损并且止损。下面我们来一一盘点一下资金安全设计的一些点,希望能给大家一些帮助。

资损风险分析

风险要素

在我们做支付资产系统的时候,我们其实需要好好盘点一下资损风险,这些风险可能来自于各个方面。下面涉及,

资金流:我们实际的产品业务中,尤其是支付资产的时候,其实往往会有很多类型的资产形态,可能是积分,可能是现金,当然还有可能是优惠券等等。看到这些金额的时候,我们需要确保上下游系统的一致性、金额计算的正确性、逆向金额不能大于正向金额等等。

交互:我们需要考虑客户端展示内容是否正确。尤其是小数点展示的精确位上,很容易出现客户端展现信息不全的问题,实际其实为99.99元,但是展示的时候却为99.9或者9.99。

资金规则:资金规则是为资产本身的产品形态规则,举个例子,发放优惠券这个行为,每个人发多少,发的时间点,发放的人数,发放的门槛等等。再比如某个积分资产的限额,其中又涵盖着日限额以及月限额。

异常:存在资损风险很多时候其实由于异常导致的,抛开系统本身异常之外,我们还要考虑网络抖动异常,其他服务异常。如果系统本身的事务处理不好,或者最终一致性没有做好,就很有可能造成资损。

技术风险

系统发生资损么,很大一部分就是系统没有设计好或者是编码过程中的粗心,例如上面老猫的真实案例。那么且抛开粗心这个人为因素,我们盘点一下本身技术风险,这些技术风险场景主要来源于多并发、幂等、分布式事务、上下游服务超时、数据计算精度、接口协议、校验逻辑的不严谨等等。

上面罗列的这些很多都为一致性的问题。我们一个个来看。

1、并发:多线程、同时对数据进行读写处理的时候,就有可能造成一致性的问题,例如用户资产重复支付,积分超发等等,如果在系统层面还用了缓存的话,还有可能存在缓存未刷新,导致数据库和缓存不一致的情况。

2、幂等:在支付资产系统中,接口幂等性是十分必要的,接口的幂等能够很大程度上避免(1)中提到的资产重复支付问题、下单重复问题以及网络重发问题。关于幂等详细设计,其实老猫在以前的文章中也有梳理,大家有兴趣的可以看这里【前任开发在代码里下毒,支付下单居然没加幂等

3、服务超时:系统所依赖的服务执行结果返回慢,造成上下游数据状态不一致,例如核心的支付服务调用底层的资产服务进行扣款,结果由于资产扣款逻辑返回超时,导致两边数据不一致。

4、接口规范:尤其是对接第三方接口的时候,文档上的必填字段和非必填字段如果本身文档就有出入,可能就是致命的。

5、事务:其中包含本地事务以及分布式事务,研发在开发过程中对事务理解不够透彻,使用不严谨,最终导致数据不一致。

6、数据精度:主要在金额四舍五入的场景,最终导致精度丢失。或者上下游系统精度不一致。

防止资损

如果说想要彻底的避免资损,并不是一件容易的事情,系统链路复杂,任何一个环节出现问题都有可能导致最终的资损。我们虽然是技术,但是我们的眼光其实不能够仅仅局限在技术的眼光去看待资损这个事情,除了技术侧尽量规避资损发生之外,其实还有其他方案,例如下图。

一个小小的逻辑判断符,资损几万块

上图准确来说其实是一个防止资损,或者说尽量保证企业损失最小的一系列的手段以及方案。这些步骤,其实从时间上来看是不同的时间线。以下咱们来一一看一下。

1、技术规避:

技术侧我们当然要保证我们自身代码的严谨性。这里主要提及的还是上述所说的一致性的问题。我们在系统开发的过程中要挖掘系统可能出现问题的点,其中可能包含事务的使用、接口需要做好幂等设计,系统和系统交互过程中需要考虑接口的重试机制等等。当然这些都是咱们研发在实际开发的过程中需要注意的点。

2、对账发现:

很多时候,资产支付系统上线出问题,并不是直接的日志异常。这种异常可能还好,容易被发现,因为已经卡流程了。怕就是怕在不知不觉的情况下,看似风平浪静,其实内部数据已经一团乱麻。资损已经产生了,就像老猫上面遇到的这种情况。这种情况的发生其实主要还是由于没有做好相关的对账措施。从而导致了悲剧的发生。其实如果我们能够做到每日对账,可能问题就能及时被发现。

对账方式:我们可以从上层系统一路往下游系统进行对账。例如,我们有这样几个系统,例如,上层电商业务系统,订单系统,支付核心系统,第三方支付系统。那么我们对账的方式就如下。

一个小小的逻辑判断符,资损几万块

咱进行对账的过程中,我们一般是系统之间进行两两单据对账,对账维护有两个方面,第一个是总数量,第一个是总金额。如果我们发现所有的系统能够两两账目对齐,那么系统就没有太大问题。

一个小小的逻辑判断符,资损几万块

当然很多时候单据数量可能由于业务的原因是不对等的,所以这个时候可能还需要进行一定的数据清洗,然后才能进行对账处理。做得好点的话,可以将对不齐的单子能够自动告警,告警方式可以是短信或者邮件方式,当然也可以支持相关人员能够看到每日的对账看板。

这种对账的方式可以协助我们及时发现系统上的问题。老猫之前对接的那个渠道,其实还没有来得及做对账。因为那时候我也疏忽了,认为当前第三方渠道比较冷门,所以就偷个懒没有做对账。然而最终还是逃不过“墨菲定律”。所以咱们研发在做系统上的事儿的时候还是不要抱有任何的侥幸心理。

3、应急止损:

如果真的到了这一步,其实悲剧已经发生了,这个时候其实是比较考验心态的。因为系统漏洞已经造成了资损,并且资损还在持续。这时候内心就会燥热,像老猫那样口干舌燥,着急得像热锅上的蚂蚁。这个时候如果越想早点修复问题,可能越容易出错。很多时候人在着急得时候往往会病急乱投医。为了快速解决问题,修一下bug就直接上线。这种很容易导致错上加错。

所以当我们意识到问题已经发生了,就要向上汇报了,让上级知道这个事情,然后一起看一下问题的处理。这样的话多个人一起把控修复问题肯定比当事人一个人默默修复问题来得好。所谓“当局者迷旁观者清”是有道理的,这样也至少可以降低二次错误的概率。所以出现问题后,一定不能慌了手脚。唯一要做的就是冷静,然后一步步梳理处理的步骤。

当然如果有条件的话,可以根据当前的业务模式开发一个资金追讨系统来防范未然,当然这个系统真的希望是一辈子都用不上,然而这个系统可能是最后的一道屏障了。

总结

以上就是老猫的一段经历,大家可以当做乐子看个热闹。如果真的对你有所帮助,也希望能够得到你的点赞和收藏。当然,如果你也恰好维护同样的系统,对于这样的系统维护有其他新的认知,也欢迎大家能够在评论区留言。