一个小需求引发的思考
此文已由作者肖凡授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
最近接了一个看上去很小,很容易实现的需求,然而在做的过程中发现有些问题如果思考不全就很容易导致问题,这里给大家分享下。
需求背景
考拉的订单风控主要是对接杭研的反垃圾服务,订单支付完成后会异步抄送给反垃圾那边。如果订单的支付方式是网易宝支付,则网易宝也会做风控检测。那么就需要一个地方来聚合反垃圾和网易宝的风控结果,当两方都认为通过时才通知主站订单放行,否则要等待另一方的通知。
需求描述
聚合反垃圾和网易宝风控结果,将最终结果通知主站。
这个需求看上去是非常简单的,不就是聚合下两边的通知么,找个地方存下来不就好了嘛,so easy!然后开始码代码了。首先想到要在当前的风控系统的通知接口上增加两个字段,一个用来表示调用方,1表示反垃圾,2表示网易宝;一个是用来表示支付方式,1表示网易宝支付,2表示非网易宝支付,然后就是要需要将这个通知存下来,缓存肯定是不行的,数据可能丢掉,那就放在rds里面,新建一张表,包括订单id、反垃圾通知状态、网易宝通知状态、是否已通知主站等字段,然后风控通知来了之后就去表里面看看是否另一方的通知已经来过,如果没有就新加一条记录,最后根据逻辑决定通知主站订单放心/拦截/关单。整个代码逻辑大概就是这样了,但是这里面会有很多问题,下面我深入这个需求给大家分享下会碰到的问题。
问题1:如果两个通知同时到来怎么办?
初始的代码逻辑是这样:来了一个通知之后,根据订单id去表里查数据,如果不存在,则增加一条;如果存在,则判断另一方的通知是否已经到达,已到达则更新数据,标记该订单已通知主站。这里很明显有一个竞态条件:如果两个通知并发的到达,都会发现数据库里面没有数据,都会去插入数据,而订单id是唯一索引,那么这里就会报错了。如何解决呢?常见的方式有分布式锁、额外的引入队列,简单点的可以使用nkv的原子操作。考虑到这里并发的可能性不大,直接catch了重复插入的exception,然后catch块里面进行更新操作。
问题2:如果有一个通知一直不来怎么办?或者两个通知都不来呢?
任何第三方系统对于我们来说都是不可靠的,设计代码逻辑的时候是一定需要考虑第三方系统异常的情况的。针对这种情况,需要根据订单支付时的落库信息去定时反查反垃圾的订单状态然后通知主站。这里引入的定时任务也是带来额外的风险,比如如何保证定时任务一定执行?目前是使用考拉定时任务中间件kschedule来保证的。
问题3:如果通知重复了怎么办?
比如同一个订单,反垃圾风控结果是要关单,如果并发的通知了两次,程序会不会出问题?这里就需要考虑接口本身的幂等性。如果一个订单已经关单,再次调用关单时,必须返回关单成功,返回的结果和首次关单时的结果严格一致,否则可能就会导致各种数据不一致情况出现
问题4:如果消息乱序了怎么办?
比如正常情况是,一笔订单先通知拦截,订单进入审核状态,再通知订单审核通过,订单放行。假如现在收到的通知顺序是先收到审核通过,再收到拦截,那怎么处理呢?这里可以使用状态机来保证状态的流转,像前面提到的这种情况,预先定义好已审核通过的订单,再次收到拦截请求时不予处理。
问题5:数据库的操作和dubbo接口的调用怎么保证事务完整性?
当考拉风控系统收到反垃圾通知时,需要通知主站,同时要更新数据库,表示该订单已通知,那如何保证通知主站和数据库的更新在一个事务里面呢?这里可以使用考拉的分布式事务中间件DTS,不过有点太重了。考虑到第三方的通知是会有重试的,并且我们的接口都是幂等的,可以先通知主站,再更新数据库,如果都成功则返回反垃圾一个正确的code,否则有任何异常就返回一个error值。反垃圾或网易宝那边发现是error的就重试,这样就可以达到最终一致状态。
问题6:要是反垃圾一直挂了咋办?要是反垃圾疯狂的调用你的接口怎么办?怎么快速发现第三方系统的问题?
代码逻辑是写完了,但是如果反垃圾由于自己的bug挂了,导致有风险的订单一直无法关闭,如何快速发现呢?那就要加监控了。除了监控异常的情况以外,正常的调用量也需要监控。什么意思呢?我们一般监控会覆盖到一些程序中的异常,比如调用主站关单接口失败了,会在catch里面捕获到之后给哨兵发一个监控信息,但是这样会漏掉一些情况,比如反垃圾挂了,一整天一个通知都没有,或者反垃圾逻辑出错,疯狂的关闭大量的订单,这些调用量的异常也需要及时的发现。进一步的,接口要做好保护,防止被第三方调挂了。
问题7:如何平滑的上线?
这是一个兼容性问题,也是比较容易忽视的一个问题。举个例子:关单接口是之前就存在的,一个http接口,提供给反垃圾使用,接口的参数包括订单id和关单原因等。现在该接口加了一个参数payType用来表示支付类型,Integer类型。根据约定,这个参数是必传的,所以代码里面会判断,如果该字段为空,则直接返回错误码。这么做是没有什么问题的,也是一种保护接口的方式,但是这里就有一个兼容性问题了:当你的代码先上线后,该字段肯定是空的,这会导致所有的关单全部异常,这一点不能忽视了。
一个小需求,看似很简单,但是想要做到没啥问题还是需要深入的思考的,业务逻辑代码只是一部分。特别是和第三方有交互的时候,一定要记住:所有的第三方都是不可控的,它们有可能挂掉、有可能不停的调用、有可能不按照约定来,任何时候你都要保证自己的系统处于一个安全的环境中。这些逻辑有些是可以在统一网关中解决掉,有些只能靠自己的接口解决。
3天后。。。
说了这么多,然而并没有什么卵用,还是出了问题。。。
什么问题呢?反垃圾调用关单接口失败。如何导致的呢?不是思考了这么多么,为何毛用没有?那只能说明我思考的还不够多。。
这个问题导致的原因后来也查清楚了,是反垃圾在获取payType这个参数的时候报了空指针异常,导致请求根本就发到我这边。考拉这边的商户类型有新增,而反垃圾那边没有同步最新的商户类型。这次做payType映射的时候,碰到新的商户类型就报错了。之前没问题是因为没有使用到这个payType。那说好的监控呢?为毛没监控到?额,这是因为这次上线是反垃圾先上线,我们还没有上线。。。
那么最后再加一个思考:对于接口修改的内容,双方一定要多沟通确定清楚,任何一个小问题都可能会导致线上事故。联调的时候需要覆盖尽可能多的场景,然后这始终是会有遗漏的,所以单元测试赶紧跟上吧。
网易云免费体验馆,0成本体验20+款云产品!
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 知物由学 | 广告欺诈:如何应对数字广告里分羹者?
【推荐】 从DevOps到CloudNative,应用上云姿势全解锁
【推荐】 一个内部增长案例的分享