资金服务架构设计
背景介绍
作为互金公司来说,需要从银行、信托等三方机构获取资金,来支持其分期、贷款等消金业务。为了能快速接入资方,满足业务对资金的需求,我们抽象了资金服务,资金服务作为对接外部厂商的端口,要协调业务与资方的各种差异,将不同机构提供的接口通过统一的方式呈现给业务方,是业务系统的重要支撑。且由于对接的各银行和第三方系统的稳定性参差不齐,通道故障时有发生,作为承接上下游的核心系统,要在一系列不稳定的系统之上建立一个可以给上游提供稳定服务的系统。
架构设计
资金服务架构资方服务拆分为资金服务、代理服务、以及资方路由服务。资金服务进行参数组装和流程串联,资金代理负责和外部资方机构进行网络通信,资方路由服务提供规则配置,及给用户推荐合适的资金方。
请求流程
请求流程时序图流程
- 请求单幂等,并落库
每个请求会携带一个requestId(客户端通过分布式id生成器生成),服务收到请求后,会拿着此requestId从请求流水表(requestId为流水表的主键,流水表会记录请求的入参,及最终的返回结果,还有异常信息)中查询记录,如果已经存在,则直接返回流水表记录的结果信息。如果不存在直接插入,并捕获重复主键异常,确保并发下的幂等,然后,插入一条记录到task表(在同一个事务中),关联id为requestId,并记录任务类型,然后直接返回"进行中"(中间状态)。 - 任务推送
定时任务扫描task表,服务请求资方接口,得到同步结果,并更新请求流水表的状态,删除task表,将task记录移到history_task中(同一事务)。若发生异常,则增加task表的retry次数字段,等待下次重推,查看该接口的配置中是否支持幂等,若支持幂等,则重推,否则调用资方的结果查询接口,若未在资方系统落单,则重试,否则直接将流水表置位终态。成功后会进行相关单据的创建和更新。当异常后,三次重推失败后,会停止重推,触发报警机制,人工介入。 - 结果消息推送
当资金服务获取得到最终结果后,会通过kafka将结果异步推送到到业务系统。也提供了结果查询接口,让业务系统定时调用,防止结果消息丢失。
资金路由流程
路由流程解决的问题
-
屏蔽不同资方接口的参数及流程差异
我们在抽出资金服务之前,不同的资方的流程和接口参数耦合到了业务系统之中,使得每次新接入资方,业务系统的流程就要进行重构,效率很低及不稳定。
解决方案:通用接口 + 统一入参 + 参数聚合
我们抽象了通用的接口,和通用的流程,业务系统只需按照固定的流程调用我们的接口即可,且参数只需要传输通用的必须的参数,如订单号,资方编码等,由资金服务通过数据服务根据资方接口的要求获取所需的数据并进行参数的拼接,和对资方的接口进行组合。
屏蔽流程差异 -
接口的响应时间依赖于外部资方接口,资方接口的响应时间不可控,导致超时。这时候可能出现业务方调用失败,但是资方成功的数据不一致的情况。且同步接口失败在系统内不好进行重试
解决方案:预落单 + 异步 + 任务推送。异常都资金服务自行进行重试及处理,不会因任何交互异常而影响上游获取结果的准确性,能让上游感知的,都是明确的、资金服务不能处理的异常。
请求异步化 -
部分资方的接口超时,占用线程,导致其他资方的请求阻塞
解决方案:线程池隔离,不同资方的不同接口的异步请求线程池相互独立,防止相互影响。其实当不同接口的请求量不同,即不同的业务模块相互影响时,也可以采用业务隔离的方式,例如授信部署在一个节点,用信部署在另外的节点。 -
线程池的监控与动态配置
-
对系统中不稳定的因素进行熔断降级
1)使用sentinel对资方的接口进行保护,当异常数和响应时间(不同接口的设置响应时间也不同,有的1s就异常,有的会3s)超过阈值时,这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口(10分钟)之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
2)降级策略:异常属性上报,进行报警;失败的请求task记录状态置为降级状态;会被并将该资方接口置为不可用状态,作为资方路由路由功能的前置判断依据。 -
对外提供的业务接口也接入sentinel进行限流
1)根据请求的qps和并发线程数进行流控
代码结构设计
- 模板策略:经典设计模式的合理利用
-
面向接口:以Node接口为基础,可以按需继承AsyncQuery、CallBack、TaskExecutor等单一职责接口
代码聚合:Node接口通过继承其他接口,把一个节点的主要流程封装进一个Node,可读性高,入手easy
声明式身份:用FundCode、NodeCode接口为Node实例声明身份,避免修改beanId
多方向调度:统一的Task模板聚合TaskExecutorMap进行调度;统一的回调入口聚合CallBackMap进行调度。
image.png
通过fundCode(资方编码)+ nodeCode(功能编码)可以唯一确定一个nodeService,所以我们新接入资方的时候就可以新增nodeService实现即可,不会对原来资方的功能代码产生影响。且nodeService还可以按需实现callback,taskExecutor、asyncQuery等接口实现回调、定时任务、异步查询等能力。
表设计
- 请求流水表
- 授信单:存储授信记录,每个月300多万单数据增长,当授信记录达到6千万的时候,对授信单进行了分表,在老库中分成8张表,分表id为用户id,采用基因分表法,授信单号(时间戳+机器码+分表id+随机数)。
- 放款单:存储放款记录
- 还款单:存储相关的还款记录
- 代偿记录
- 任务表/历史任务表
- 账户表
- 账户的动账流水表
-
影像文件表
image.png
联调问题
- 业务方联调时,资方系统未达到联调要求,如何不影响联调进度?
解决方案:增加AOP处理,拦截特定的请求进行Mock,工程配置mock.json文件,启动解析mock配置,运行时通过参数匹配来判断是否执行mock。
mock.json整体是一个map,第一层是类名,第二层是方法名,然后就是jsonobj数组,每一个 jsonobj就是一个匹配,数组里的jsonobj。主要有一下几个字段
matchKey:用于匹配特定的字段
matchValue:如果matchKey不为空则判断指定的成员变量是否为此值,如果matchKey为空则忽略判断参数里是否有此值。
syncResult:同步返回值,jsonObj
asyncResult:异步返回值,jsonStr
{
"com.xxx.xxx.xxxService": {
"creditApply": [
{
"matchKey": "mobile",
"matchValue": "1398888xxxx",
"syncResult": {
"code": "0",
"msg": "success",
"requestNo": "110"
},
"asyncResult": "ok"
}
]
}
}
- 与第三方接口的联调工作要与开发并行。开发人员经常会天然的把“技术含量最高”的开发工作作为最优先的部分,迫不及待的投入到编码中,等到开发完成或者完成的差不多了,才开始进行联调这是不对。因为系统之间的风险天然的是大于系统内部的风险的。因为有以下几种情况:
1) 对方提供了技术文档,但是有错或者版本太旧,误导了你,最后测试才发现;
2) 对方技术文档没问题,但是你的业务需求,恰好触发了对方系统的某个问题,对方需要很久才能解决;
3)对方公司对你这个项目的优先级设得很低的,指派的对接人员很不好说话甚至是个 sb,完全不顾你的死活。
解决这个问题的方法很简单,就是“尽量早,尽量深入的和对方进行沟通”。通过沟通,尽早识别对方的人员水平和态度,识别对方的文档和产品质量,有针对性的及早采取对策,风险前置。
早期进行 demo 联调,不但可以尽早验证对方的产品,还可以尽早建立和对方人员的联系,判断对方人员的水平和态度。
其他技术点
- 分布式锁 (setnx复合命令+lua脚本实现),目前转为redisson(可重入,可阻塞,支持锁续期)
- sentinel机构超时接口的快速失败,防止阻塞任务线程池
- traceId链路追踪
- 拆解依赖关系,并行处理(从数据服务拼接数据)
- 使用freemarker模板引擎来进行资方请求报文的参数化映射,freemarker模板文件中key对应的具体的值采用占位符的形式,从前置的实体对象中取值,将变化点使用占位符表达式来解决。
- 接入对账平台,t+1进行对账,每天凌晨会对白天的交易进行对账,这样可以及时发现不一致的数据。
接入的银行渠道
1.工行
2.光大
3.渤海
4.新网
5.众邦
6.微众
7.中关村银行
8.渤海
9.湖南信托
10.蓝海银行
11.民生
12.交通
13.招商