业务背景
1. 借款流程:投资和借款期限错配(如,投资365天,借款只有90天),投资生成资金,p2p维护资金池,借款从资金池捞资金生成有效债权,随后调用银行接口进行转账,至此借款流程结束。
2. 还款流程/代偿流程:用户根据规定的借款利率,返回本金和利息。p2p会根据该笔还款对应借款的所有有效债权(这里分本金债权和利息债权)进行分账,预生成转账记录,然后发起还款,生成批次调用银行的接口。等银行处理完批次,回调p2p系统。至此还款流程结束。
3. 先息后本,最后一期一次性本金还款:每一次还款还利息,入利息池,最后一期还本金和利息,入利息池和资金池。
4. 资金池:用户投资成功后,不会马上找借款匹配,而是维护一个本金资金池,供借款来按照一定的规则匹配顺序来生成有效债权(如优先新的预约资金、还款复投资金、代偿复投资金)
5. 利息复投:核心交易系统借款人还款分账时,属于出借人部分收益会实时转到其收益账户,但该笔资金需等到投资赎回日方能解锁取出。p2p为了使收益最大化,将出借人的利息组成一个利息池,使用合适的及时,将其利息转化为可以匹配的资金放入资金池(将capital_property_type字段区分本金资金和利息资金)。
6. 还款复投:当一个借款(期限是90天)和一个资金(期限是365天)生成有效债权,由于导致期限错配并且还款人提前还款,所以p2p将还款的资金再放入资金池,以便遍匹配其他借款。
7. 代偿复投:由于借款人未能及时还款,由平台替借款人还款。走后续的分账流程,放入资金池。
8. 赎回债转:投资A到期赎回时候,由于借款人的钱未还(期限错配),所以会找资金池中的资金来承接该借款人债权,然后将这笔资金转入投资A来完成赎回流程。
核心业务资金流程图
关键技术点
1. 利息复投的时机:还款、代偿、本金赎回回调。
2. 赎回流程,先拉回本金资金、然后拉回利息资金,拉回利息资金进入利息池。
3. 为了后续能够对账已经问题排查,将利息池做成两个表,一个是用户的利息表,一个是该用户的所有涉及利息池交易的流水,方便对账。
4. 赎回还会触发利息复投,场景是,用户所有利息池的钱大于这笔资金的预期利息,而且剩余的利息池金额大于100块,就会触发利息复投。
5. 还款或者代偿资金流向:
1)利息债权本金:进资金池
2)利息债权利息:进入公司收益账户
3)本金债权本金:进资金池
4)本金债权利息:进利息池
6. **还款和赎回需互斥**:当某一个出借投资A开始赎回,对应A所有的有效债权,每一笔都要需要债转。如果此时,其中一笔债权对应的借款人还款,如果不进行互斥,那么会有该笔债权会有两笔资金进入投资人虚户当中(银行接口不会做还款批次和赎回批次互斥)。
关键表以及字段含义
###### 一、资金表
1. status:0=异常|1=正常|2=完全匹配|3=已拉回
2. use_status:使用状态,借款时候CAS标志,增加借款的匹配的速度
3. principal、match_principal、pre_match_principal:
principal:初始资金,表示这笔投资生成的原始资金金额
match_principal:已匹配的资金,表示有一部分资金还在投资人银行账户
pre_match_principal:预匹配的资金,表示正在使用
###### 二、利息池
1. 利息复投开关recast_switch:旨在避免业务冲突
这里`recast_switch`开关的默认开启,关闭时机在**赎回**时候,开启时机在赎回完成。原因:用户赎回投资时候,需要将利息债权的钱债转到用户的利息池中,如果此时该用户的利息复投还开着,该用户的其他投资还款的时候会入利息池,并且触发复投生成可匹配的资金。我们赎回的目的就是将该用户所有的利息债权和未匹配的利息资金放入或者债转到利息池当中,以便赎回使用。
2. 当前投资人利息池剩余金额current_interest_pool_remain:记录投资人当前在投的利息金额总和,利息复投的时候可以通过100元这个阈值来判断是否可以复投。
3. flow_type:资金流类型(11=还款|12=代偿|13=利息资金拉回|14=利息债权拉回|21=利息复投|22=赎回使用)
4. 其他比如金额、用户ID、交易订单号等
###### 三、债权表
1. 债权订单号order_no、投资订单号invest_order_no、借款订单号loan_order_no、资金订单号capital_order_no、借款人ID、投资人ID、匹配的金额amt、匹配的天数days
2. 债权类型:property_type 资金属性(1=本金|2=利息)
3. invest_repay_date投资回款日、loan_repay_date借款还款日:可以用来进行统计
4. 是否已经发生债转is_transfer_credit:记录债权是否债转
5. 状态status:-1=预处理|0=匹配中|1=匹配成功|2=匹配失败|3=债权已完结
在请求银行放款接口后,状态置为0,在银行返回放款结果后,来决定这份债权是否匹配成功。
债权完结几种类型:还款完结、代偿完结、赎回完结
6. last_order_no上一个订单号:第一种订单号是债转之前的旧的债权订单号。第二种是还款或者代偿生成新的债权旧债权订单号。结合is_transfer_credit来判断是哪一种。这样做的好处是可以记录每一笔债权的动态变化,以便问题跟踪和统计。
###### 四、钱包表
用户余额是一个非常重要的指标,这个指标影响着整个P2P金融系统的业务运行。如果因为业务或者程序问题导致余额不准(比如用户余额多,就会被提现走),业务运行异常。最后导致公司亏损。
由于银行的余额接口不稳定(会出现接口访问不通或者网络波动),如果线上业务依赖银行接口的话是非常危险的。所以,在本地会维持一个余额钱包表记录改用户的所有的交易记录的金额。在每个不同场景业务触发的时候,会相应的调用余额服务,来保持数据准确性。
1. balance 余额
2. recharge 总充值金额
3. withdrawal 总提现金额
4. invest 投资总金额
5. interest 赎回、退回总收益(本金+利息-手续费)
8. version 版本号:当多个更新操作同时更新时会导致数据不准,故采用乐观锁来解决。
总结起来就是 `balance` = recharge + invest + interest - withdrawal
项目的难点以及解决
###### 一、用户余额问题balance
1. 在分布式系统中,为了保证数据一致性,p2p借鉴TCC分布式事务,本地状态表来解决
2. 用户余额,p2p将其拆分成一个独立的服务,在每个场景调用余额服务时候,会在本地事务中生成一个调用中间表状态,并且会开一个定时器去扫描该表的状态是否是RPC调用成功的,失败会去重试。
###### 二、项目中的分布式事务解决
1. **采用TCC思想和最大努力通知方案解决**
核心业务,如涉及银行资金转账,资金严格的按照锁定,资金冻结等等操作【不写回滚逻辑】,然后执行完本地事务后,采用发送MQ或者插入本地消息表的方法,调用另外一个服务。如果调用失败,就会更新数据状态,然后利用定时器扫描继续去调用,这里需要设置一个最大的重试次数。
2. **利用MQ来解决**
利用阿里的rocketMq 的事务消息的功能,目前还在研究,但是不支持本业务需求【要有延迟消息的功能,而且延迟时间可以指定】,故还是采用 activmq方案。
###### 三、幂等性方案设计
在P2P业务中需要做幂等流程操作,比如重复借款、重复还款,重复发布新标、重复投资、重复计息贴息等等,为了保证业务正常,采用以下方案
1. 唯一索引
2. 悲观锁:比如在借款进来时候,债权匹配,会将资金池中的一部分资金锁住,防止其他的借款使用该笔资金,导致数据问题。
3. 乐观锁:版本号或者条件来做,注意如果有条件限制,最好是用主键或者唯一索引更新。
4. 分布式锁:不管是借款还款还是投资赎回都会加上分布式锁,来保证三方借贷方或者本地赎回调用接口时候,来保证这种长流程的业务能够正常走通。
5. 状态流转机制:比如订单状态,在订单流转的时候判断状态是否正常来保证幂等。
###### 四、高并发方案设计
1. 凡是写请求,而且不需要马上返回业务结果的都可以利用消息队列来接受,利用多线程进行消费,例如每一个服务开启一个专门线程池Bean,利用AOP写在方法上,就可以多线程消费了,比如Spring的@Async。