从无到有,支付路由系统升级打怪之路

2021-01-06 11:08:16 浏览数 (1)

今天分享一下支付路由系统实现方式

其实这个话题去年也写过,不过当时没啥阅读量,这次就再次翻出来加工一下,炒下冷饭。

支付路由系统

提到路由,不免会想到网络通信过程中起到数据包转发的路由器。

图片来自网络

而我们今天讲到支付路由系统,也是起到类似的作用。路由系统本身并不处理具体业务,它的作用就是将支付请求转发底层支付通道。

支付路由系统

如上图所示,支付系统接入层,接收到支付请求之后,经过内部运算,最后将会通过路由系统转发给具体底层的支付通道。

另外,除了路由转发功能以外,路由器一般还会有一些额外的功能,比如防火墙等。

那我们其实也可以在支付路由系统加入额外功能,比如实时计算底层支付渠道的成功率,若低于一定的阈值,进行报警并且将该渠道下线。

这里需要说明一点,这里的路由系统可以是一个应用中子模块,也可以是一个单独子系统。

为什么需要路由系统

看到这里,可能会有一些小伙伴会思考,一定需要这个路由系统吗?直接将请求发给支付通道不好吗?

答案当然是可以的,如果当前只对接「一两个支付渠道」,这么做没问题,并且也推荐这么做。

这个阶段由于业务量不大,支付系统可能只是一个单体应用,或者也可能是其他应用内一个子模块而已。

那这个时候,业务很简单,系统也很简单,那我们不需要额外的路由系统,增加系统复杂度。

不过这样的系统弊端也很明显,如果后期再新增一个支付通道,我们需要再开发对接。

另外由于所有实现都在一个系统,假设系统应用发生问题,那么就是大家一起「死」,这其实也是单体系统最大弊端。

所以如果底层对接支付通道很多,像一般第三支付公司的支付系统,同一家银行的可能会对接很多支付通道,比对银联无跳转支付,网联支付,也有可能是 XX 行自己提供的接口。

那么这种情况就非常需要单独维护一个路由系统。

PS:630 政策之前,支付公司对接支付通道那真是一个多,同一家银行,可能会对接四五个通道。 但是 630政策之后, 不允许支付机构直接对接银行。 所以现在支付机构对接通道可能会比之前少很多。

实现方式

路由系统实现方式有很多,下面主要分享一下我所经历过实现方案。

我们的路由系统经历过三个阶段的迭代,才有了现在的实现方案。

第一个阶段-混沌初开

这个阶段就跟上面讲的场景一样,业务需求较简单,仅仅只需要对接一两个支付通道。

为了快速上线,设计方案就简单粗暴,所有业务都处于一个应用。

支付子模块对内暴露暴露支付服务接口,由业务系统发起直接调用。

系统设计图如下:

这个阶段由于只有一个支付渠道,所以也不需要有路由系统,直接由业务系统调用支付服务接口发起支付。

不过随着业务量增大之后,这个设计方案就暴露很多问题:

  1. 业务系统与支付系统位于同一个系统,系统任何一次变更都会影响整个系统。
  2. 扩展性问题。每接入一个新的支付通道如微信,就需要增加一个新的实现类。另外,业务系统的代码同时也需要改动,需要调用新的实现类。

针对以上问题,我们进行第一版的改造。

首先我们将整个支付模块从原来应用中拆分出来,成为一个独立的子系统,专门运行支付业务。

这个系统对外提供一组支付接口,业务系统只需要调用这个接口,传入必要的参数,无需关心支付系统到底是如何实现的。

如果业务系统想指定某个支付通道,比如支付宝,那么可以在接口传入这个渠道标识,支付系统将会根据这个渠道标识调用相应的支付通道。

其次梳理渠道接口文档,抽象出共性接口,每个支付通道实现都需要继承这个接口。

这组通用渠道接口,其中 「channelName」 方法,代表这个实现类具体代表哪个通道。比如说这个方法返回 「aliPay」,那么就代表这个实现类将会调用支付宝通道。

支付系统子应用中将会维护一个类似路由表,这里简单使用 Map 存储映射关系,「key」 为上文提到的渠道唯一应用标识,而 「value」 为具体的实现类。

应用初始化之后,将会调用 「Spring ApplicationContext getBeansOfType」 方法,获取同一个接口的所有实现类 ,最后将其放入 Map 缓存中。

每次支付调用都会根据渠道唯一标识从路由表获取具体实现类,然后由具体的子类实现支付逻辑。

学过设计模式的同学,这里应该不会陌生,这其实是使用设计模式中的策略模式。

这个阶段,由于业务还不是很复杂,系统还是挺简单,路由系统还只是系统中的一个子模块。

第二个阶段-神功初成

经历过一段时间,公司的业务量变的越来越大,这个阶段我们开始追求系统的稳定性。

但是在第一阶段设计方案中,支付系统所有模块位于同一工程。有些模块可能需要频繁发布,这样一旦发布就会影响所有系统功能。

第二点,系统功能全都耦合在一起,团队开发也变的困难,分支冲突,代码丢失也是经常的事。

第三点,一旦某些改动发布发生问题,整个系统都受到影响,真的是「要死一起死」。

所以这个阶段针对以上的问题,我们进行了相应改造,开始将支付系统进行拆分。

首先按照功能,将支付系统拆分几个独立的子系统,路由系统,渠道系统,成为独立系统,独立部署维护。

每个支付通道单独维护部署,成为一个单独的子应用。

系统之间的调用关系,就从同一进程内调用,变成使用 「RPC」 进行跨进程调用。

这个时候就会有个问题,渠道系统可能会因为发布而下线/上线,这时路由系统必须动态维护这种关系,在渠道系统某一节点下线时,自动删除调用关系,而当应用上线时,新增调用关系。

「说白了,路由系统需要实现渠道服务动态发现。」

看到这里不要怕,其实 Dubbo 框架已经自带这个功能,我们没必要自己再去实现了。

Dubbo 配置中有一个属性-「group」,这个属性可以用于服务分组。

当同一个接口有多个实现,我们就可以根据这个来区分不同渠道系统的实现。

因为渠道系统实现同一组接口之后,提供出 Dubbo 服务需要加上相应的 group 属性,值为相应的渠道唯一标识。

如下所示:

路由系统只要引入这个 Dubbo 服务,设置相应的 「group」 属性 ,路由系统引用渠道系统的服务:

此时路由系统就跟第一阶段一样,内部维护一个路由表就好了。

这里采用了 XML 配置存储渠道标识与 Dubbo 引用服务的映射关系,如下所示:

服务启动之后解析这个 XML 文件,然后将其维护在 Map 中。每次支付调用都会根据渠道唯一标识从路由表获取服务名,然后借助 「Spring ApplicationContext#getBean」 获取具体的 Dubbo 引用服务。

后续如果再新增渠道系统,路由系统不需要再修改任何代码,只要在配置文件中新增 Dubbo 服务引用以及增加路由表引用关系即可。

第三阶段-登峰造极

第二个阶段路由系统基本上已经满足现有阶段业务实用,不过还是存在个问题,渠道应用新增时,还需要新增配置「重启应用」

之前有一次新增渠道,忘记了在路由系统新增配置,从而导致新的渠道应用无法被调用,找了很久的问题,才发现是这个问题。

所以第三阶段,主要是优化路由系统,去掉上述配置文件,到达新增渠道应用,而不用重启路由系统。

这个阶段的改造,我们不再使用 XML 配置引用服务,而是借助 「Dubbo API」 ,动态引用 Dubbo 服务。

查看 Dubbo 文档 ,可以直接使用 ReferenceConfig 直接查找服务提供者。

官方文档建议:

ReferenceConfig 实例很重,封装了与注册中心的连接以及与提供者的连接,需要缓存。否则重复生成 ReferenceConfig 可能造成性能问题并且会有内存和连接泄漏。在 API 方式编程时,容易忽略此问题。

这里使用ReferenceConfigCache,用于缓存 「ReferenceConfig」 实例。

改造之后,去除之前所有引用服务配置文件以及缓存注册代码,不用再使用 Map 存储路由的映射关系。改造如下:

总结

回顾上文,可以看到初期没有路由系统,整个系统可以运行下去。

但是随着业务量不断变大变复杂,最开始的系统架构已经不能适应当前的环境,所以我们才开始系统拆分,进行微服务改造,一步步改进系统。

改进的过程中,不断发现方案不足处,然后一步步迭代演进。这个过程中,要善于利用现有框架的功能,加速功能的开发。

最后,本文给出了几种不同阶段路由系统实现方式,适合不同阶段、不同类型的系统。

如果各位同学刚好也有类似需要,可以根据自己系统的情况借鉴参考。

好了,下周见~

0 人点赞