互联网金融是最近几年的长期风口,它经历了野蛮生长期,目前处于强监管期,2016 年 8 月 24 日出台的《网络借贷信息中介机构业务活动管理暂行办法》中明确要求“网络借贷信息中介机构应当实行自身资金与出借人和借款人资金的隔离管理,并选择符合条件的银行业金融机构作为出借人与借款人的资金存管机构。”
截至 2017 年 5 月 17 日,正常运营平台共有 396 家正常运营平台宣布与银行签订直接存管协议,约占同期 P2P 网贷行业正常运营平台总数量的 17.89%,其中有 209 家正常运营平台与银行完成直接存管系统对接并上线,占 P2P 网贷行业正常运营平台总数量的 9.44%。这就意味着 2017 年 8 月份之后,可能 80% 多的 p2p 平台(约 2000 家平台)都要面临淘汰或者整改。因为,除了对平台的体量规模有要求,还对平台的技术实力也是一次大考验。
首先人人聚财平台采取的是直接存管形式,直接存管才是真正符合《暂行办法》要求的银行资金存管的方式,在这种模式下,客户的资金不在平台流转,能够有效隔离平台与投资人的资金,平台全程无法触碰资金,杜绝了资金池的产生。
人人聚财于 2016 年 10 月底上线银行存管系统,已稳定运行 8 个多月,每天处理异步消息 200 万条,峰值 400 万条,人人聚财的技术架构经过了 1.0 版本的所谓的巨石(Monolith)系统,发展到了 2.0 版的分布式版本,整体技术架构如下图所示:
在此架构下,我们来看看,实现银行存管的过程中有哪些关键点。
数据一致性如何解决 订单化机制平台每天都与银行有着非常频繁的交互,如何保证平台与银行之间的数据一致呢?这个问题本质上是属于分布式事务的问题,业界一般的解思想方案有:2PC(两阶段提交)、TCC(Try/Confirm/Cancel)、事务消息等等,这些方案优缺点本文不作涉及,我们来看看人人聚财是如何应对这一问题的。
我们知道,分布式应用具有 CAP 特性,一般来讲,不论电商,类电商,还是社交应用,都会适当牺牲 C,保证最终一致 (Eventually Consistency)和 AP,我们的做法是平台与银行的所有接口进行交互均采用订单化机制。
我们把每一个与银行的操作交互抽象为一个订单,订单化机制就是指交互开始的时候平台插入 N(N>=1)个订单, 这 N 个订单根据业务的不同可能是一个父的订单,也可能是父子模式的订单,每当接口或业务完成的时候就更新对应的订单的业务状态。
当交互期间因网络或其他原因导致掉单时,平台的补单机制可以及时根据当前订单的业务状态来判断接下来应该从哪里开始补单。比如说在给用户账户进行返现时由于网络原因导致超时了,那么补单机制会先去银行查询该订单的状态,根据业务处理状态来进行下一步操作;若由于银行系统异常处理失败了,那么补单机制会自动更换流水号重新发起请求,整个交互过程如下图所示:
平台每天晚上固定时间点会主动核对平台用户账户的可用余额、冻结金额等数据,并与银行的用户账户数据进行比对,对于有异常数据会及时通知到相关责任人进行排查。
对账对于涉及到资金交互的系统,对账是必不可少的。平台每天固定时间点会主动去银行获取前一天的对账文件,与平台数据进行核对。
流水号如何生成?在与银行的交互过程中,必须使用一个唯一的流水号来标识一次请求操作,不然业务操作会乱作一团,在对比了一些优秀的唯一 ID 生成算法后,最终选择使用 Twitter 的 Snowflake 算法来为我们提供银行存管中的流水号服务。此算法的好处有两点:一是纯数字,二是整体上来说是按时间顺序的。
原理不作过多介绍,网络上的文章也很多,算法原版是 Scala 写的,网上也有 Java 版本,但是有 bug,会产生重复的 ID,无法用于生产环境的集群部署,我对其进行了修改,修改后,可用于生产环境,已稳定运行 8 个月,下面是完整核心代码:
https://gist.github.com/xishuixixia/f0f8684805d0504289b7a40f3b327dd6
需要注意的是,集群机器需关闭 NTP 的时间同步功能,至于原因,留给读者,看上面代码便知。
任务调度平台如何落地?在金融行业中,一定会存在大量定时任务,即在一个特定的时间点,系统执行指定的一个或多个业务逻辑,诸如跑批,对账等等。
为什么要用任务调度平台业界优秀的调度框架当属 Quartz,比较成熟,但是它有一个不足,就是任务的执行和调度严重耦合,代码里面有很多硬编码,很容易出 bug,因此构建一个任务调度平台就显得很有必要。
怎样搭建任务调度平台指导原则是轻量化,不需要太重,我们在 Quartz 的基础上,将任务调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”按照调度配置发出调度请求。“调度”应用和“任务执行”应用之间用 HTTP 协议进行通讯,因此,“调度”和“任务执行”两部分相互解耦,提高系统整体稳定性和扩展性;即使某个任务执行模块宕机也不会影响其它业务。整体架构如下图所示:
我们业务模块只需要提供“任务执行”接口,任意部署到一台机器上,然后调度任务平台配置调用业务接口的基本 Meta 信息(如调用地址端口,bean 名字,监控报警邮件地址)即可。需要注意的是,我们在开发“任务执行”接口必须保证四个特性:
-
可重复执行:即幂等性, 数学表示为:f(f(x))=f(x), 在此处是指调度模块执行多次,不会影响业务本身逻辑。
-
可延迟执行:如果调度任务在指定的时间没有调度。可以通过手动执行来弥补。业务执行结果和指定时间执行的结果一致。
-
可并发执行:如果调度任务同时调用 A B 两台业务服务(处理相同业务)。这个时候需要我们业务需要保证事务一致性。
-
可暂停、可恢复执行:随时可以暂停正在执行的任务,也可以恢复继续执行。
当然业界也有一些比较优秀的任务调度系统,在此不作对比。
如何提升性能下面以定投宝(人人聚财平台专有的一款智能投顾产品,以下简称定投宝)债权转让业务为例来具体说明。
平台每天都有一部分定投宝产品到期,需要将本金和利息返还给用户。在用户购买了平台的定投宝产品且定投宝复审通过后,用户的资金就会匹配并投资到对应的借款项目中去。而由于定投宝的期限相对于借款项目来说较短,以及两者的到期时间不一样,因此在定投宝到期的时候借款项目并未到期,因此定投宝只有将用户持有的债权转让出去之后才可以退出,才能将资金返还给用户。
在匹配之初,基于为用户的投资风险考虑,用户投资定投宝的资金被打散成很小的粒度分散投资到不同的借款项目中去了,因此在定投宝到期需要退出的时候会有大量的债权需要转让出去。在接入银行之前,平台只需要处理自身的数据即可。而在接入银行之后,平台严重依赖银行的操作结果。接入之初,定投宝债权转让需要经历两个阶段,一个是出让,用于业务校验,另一个是转让,进行转让动作。其中出让是同步接口,转让是异步接口。
经过压力测试和数据观察发现大部分时间都消耗在出让阶段,这种模式下无法满足平台业务峰值下的业务要求。于是,在与银行多次沟通讨论的情况下,银行取消了出让接口。同时平台自身也优化了定投宝债权转让模块,通过 MQ 来接收异步回调消息、代码重构、优化相关的 SQL 语句以及由原本的单线程模式转为线程池模式,大大的提高了业务处理效率。
通过上面这个例子,我们知道平台主要通过以下几个方面来提高业务处理性能:
-
数据库层面:优化 SQL 语句,提高执行效率;
-
代码层面:优化代码结构,使用线程池来提高并行处理能力,使用 MQ 来对系统进行解耦并提高异步处理能力;
-
业务层面:在业务的合理性与效率方面进行权衡,对不合理的接口 say no。
马宏亮,2016 年 5 月份加入人人聚财金服,曾在阿里巴巴任职 8 年,多次负责双十一购物节的技术保障。目前负责人人聚财金服的技术架构、中间件、大数据平台架构的建设。