点击关注公众号👆,探索更多Shopee技术实践
目录
1. 背景
2. 模块设计
2.1 规则决策模块
2.2 控量决策模块
3. 应用与监控
4. 总结与展望
随着 Shopee 在海外市场的发展,其在信贷场景的应用也有所增长。在电商分期、现金贷等信贷金融场景中,出于进一步扩大规模、满足当地金融监管、控制业务风险等原因,须以联合贷款、资产证券化(ABS)等方式接入外部的资金方。
资金对接平台作为连接外部金融机构和内部信贷金融产品的桥梁,扮演着连接器的角色,而出于风险、成本等方面的考虑,不同的外部资金方不仅对贷款用户、贷款单等信息有一定的要求,而且对不同维度的放款规模有一定的限制。在此背景下,资金路由决策引擎在资金对接平台中起到最核心的作用:为贷款单匹配一个当前合适的资金方。
设计扩展性强、使用优雅的规则决策模块以及高性能、计算精准且不超卖的交易控量决策模块是本文的关键所在。
资金路由决策引擎作为在资金对接系统中独立的单元,其具体功能是根据贷款单、用户等相关信息基于资金方的规则做决策,以及基于资金方的放贷规模来限制或准入,最后输出一个当前匹配的资金方。概要流程如下图所示。
资金路由决策模块供服务内交易模块调用,有着明确的输入输出信息,输入项为 loan 相关的信息,输出项为决策出的资金方。
其主要包括规则决策和控量决策,围绕这两大核心模块,我们合理设计开发了相关的功能:
在资金方接入过程中,每个资金方对贷款资产往往有一定的要求,比如期望贷款人职业范畴不能属于某些行业、单笔贷款金额在固定金额区间范围内、年龄大于多少岁、授信额度大于多少。
此外,对于资金平台来说,希望分配给资金方的单的利率要小于用户的利率才能盈利,对带某些信息的单必须匹配给特定的资金方。
针对资金方、资金平台的各种决策逻辑校验需求,如果单纯靠硬编码的方式来实现,可扩展性会比较差,这不符合优秀软件开发架构的定义。因此有必要来设计一个可灵活配置的规则决策引擎。
一个规则校验往往包含几部分:一是运算符,常用的有大于、小于、等于,以及范围校验、特殊逻辑校验等;二是数据源,比如用户年龄、贷款金额等等;三是阈值信息,比如年龄范围、职业范围、可贷省市区等。这些信息都需要配置在数据库中做到灵活配置使用。
首先,常用运算表达式归纳如下所示,可以建一张规则基础定义表来维护相关的运算。
对于比较复杂、特殊的逻辑可用自定义的函数来配置使用;而对于源数据,同样也需要一张表来定义数据名、数据源、数据类型、加载方式等相关信息,某些源数据不是所有的资金方都需要校验,可定义成懒加载的方式使用。
资方规则的 ER 图如下所示,以此来维护资方相关的规则,并且按需开放一些简单的规则供运营人员修改。其中:
rule_define_tab
存储基本的规则信息,包含操作码、前置处理、描述等信息;rule_item_tab
存储基本的属性元数据信息,包含属性名、数据来源等;funder_rule_info_tab
记录不同资方的校验规则,通过这个记录关联基本规则、属性信息、阈值信息,供规则引擎使用。大多数金融系统都用 Java 技术栈来打造,Java 领域的规则引擎有 Drools、Esper、Activiti、Flowable 等,由于我们以 Go 语言来构建相关系统,因此对 Go 领域的规则引擎做了预研。
由于当前系统中只需要规则引擎动态加载,执行简单的规则,输出 true/false 即可,后续的决策由系统模块处理,从可用性、性能等方面考虑选取 govaluate 来执行配置的规则表达式。一条规则在 govaluate 的使用如下:
func Compare(left interface{}, operator string, right interface{}) (bool, error) {
var params = make(map[string]interface{})
params["left"] = left
params["right"] = right
var expr *govaluate.EvaluableExpression
expr, _ = govaluate.NewEvaluableExpression(fmt.Sprintf("left %s right", operator))
eval, err := expr.Evaluate(params)
log.Infof("expr=%v,params=%+v,result=%+v", expr.String(), params, eval)
if err != nil {
return false, err
}
if result, ok := eval.(bool); ok {
return result, nil
}
return false, errors.New("convert error")
}
代码中 left 和 right 为属性值和阈值,operator 为操作比较符,这些参数通过配置化的方式配置在 DB,执行函数最终输出一个校验的结果,以此做到灵活配置使用,可扩展性强。
系统通常需要在某些维度控制接入资金方的放贷规模,比如:总贷款余额不能超过多少、某期数在一天内不能超过多少等等。
在转账等动帐支付类场景中,往往需要对某一特定的账户进行扣款、收款等操作,如果并发量较高,假设我们数据库对数据进行一次更新需要使用 10 毫秒的时间,业务量小的话还能处理,对于一秒钟要处理几千甚至上万次的话,数据库就会来不及响应。
对于此类热点账户的处理,业界常用的做法是通过汇总记账的方式来处理,即通过先 insert db
再汇总入账,这样就把多次 db update
操作通过多次 insert + 一次 update
来替换,数据库的 insert 操作能支持的并发量是很大的,以此手段来解决热点账户的问题,虽然会导致延迟入账,但这在多数场景是可以容忍的。
在我们的场景中,对于放款类正向交易,往往需要校验并扣减某个相应的额度,而对于还款等负向交易则不需要校验,直接入账便可,对此类需求,有不同的处理方式。
最直接的实现方式是对于所有交易基于 MySQL 数据库同步计数更新数据,抛开数据库可能造成锁等待、锁超时等问题不说,下面我们先基于 MySQL 的悲观锁方式计数,验证在不同并发情况下的吞吐量如何。
此次测试在特定配置的机器上,基于 MySQL 5.6 版本执行。
由测试结果可以得知,加锁更新 MySQL 某条记录的 TPS 在 200 左右,每次 MySQL 的 update 操作大概耗时 5ms,这个速度显然不能处理海量金融交易的场景。
如上图所示,对交易进行缓存校验扣减成功后,用消息队列存储,用消息队列多副本、落盘机制来保证交易可靠性,异步入账任务批量消费消息计算并更新账户余额信息。
这种方式虽然能支持高并发流量,但是也有一定的弊端:
因此,这种方式也不可取。
数据库插入数据的并发支持是非常大的,一般不会造成数据库性能问题,只是更新账户余额需要进行锁处理,会造成性能问题,所以解决账户的关键是解决账户余额更新问题。
因为需要对正向类交易进行校验并扣减用额,如果校验额度不够则需要拒绝交易,因此把不同维度的用额同步到缓存中,对交易进行实时校验扣减,校验扣减成功则往 DB 流水表插入一条记录,然后由入账任务异步汇总入账到用额表。
对于放款类正向交易需进行缓存校验扣减,由于涉及到多个维度的操作,采取 Redis 单线程工作模式 + Lua 脚本的方式来保证校验扣减操作的原子性。
而对于还款类的负向类交易,由于不需要校验直接入账,所以无需通过缓存校验扣减,直接写流水异步汇总入账便可,因此会出现缓存可用额比 DB 实际可用额少的情况,当缓存中可用额不足的时候,异步去刷新 DB 可用额覆盖到缓存便可。容忍对于少量放款交易,某个资方在 DB 实际有额,但是没及时同步到缓存而拒绝这些交易,但不允许超卖发生。
在此方案下,从系统高可用性考虑,当 Redis 宕机发生时,还款类负向交易仍能正常插入 DB 并异步入账,放款类正向交易会由于 Redis 故障没法校验扣减而拒绝,系统会退而求其次地选择兜底的资金方放款,不阻塞业务进行,待 Redis 恢复后,系统根据流量来赖加载刷新缓存便可。
在灾备环境下,只需要所有的 DB 数据同步到了灾备环境就可以正常使用,缓存数据由于懒加载刷新而不需要同步,而且系统可以以资方维度来检查勾销资方的数据是否同步,从而决定接入流量,更加细粒度来管控。
在多活以及单元化架构下,一般按用户把各自数据拆进行了划分,对于资方额度相关的全局数据则没法拆分使用。因此,可以把这个模块做成一个单独的服务,在存储层面针对一致性敏感数据做到强同步,供各数据中心使用,当然也需要做好数据正确性的控制,以及保证系统高性能、高可用、高可靠。
Redis 缓存热点资方的相关信息,用一个 khash 的数据结构来存储计算相关的数据,key 为 funder_hotspot_limit_khash_{funderId}
。
缓存刷新时的计算逻辑如下图所示。假设配置的阈值为 100,DB 已用额 60,DB 流水表中有 20 未入账,因此实际用额是 80,这个时候刷新到缓存中的可用额度为 100-80=20,当然刷新的时候还需要先校验是否有正在插入的流水、以及锁住缓存,才能把可用额 20 刷新到缓存中生效使用。
控量决策逻辑如下所示,深色代表应用的逻辑,浅色代表 Redis Lua 脚本的执行逻辑。
在尽量保证缓存与 DB 一致,不允许超卖的前提下,需采取合适的缓存更新策略:
同样,对此方案进行测试分析,测试一个资源在缓存校验扣减 + insert DB 在不同并发场景下的性能耗时,同样在特定配置的机器上,基于 Redis 6.2.2、MySQL 5.6 版本执行。
由测试数据分析可知,在高并发场景下,TPS 能达到万级,因此本方案能应对高并发交易的处理。
此方案设计开发后经过功能测试、性能测试,达到使用要求,上线以来对规则配置的频繁修改扩展性强,对放款额度能够精准控制。
为防止规则的错配影响业务,需对规则的拒绝量、单的匹配量等统计与告警。经调研,我们选择采取开源的 Promethus 来做时序数据存储、监控、告警。同时,运营人员可基于这些数据来统计决策,反向调节规则参数,形成一个闭环,更好地支撑业务。
该方案在上线以来,对不同市场地区的贷款单进行路由处理,能满足规则的灵活配置,可扩展性强,在高并发场景下对交易的处理以及入账性能高,对平台要求的单量、金额等不同维度的管控能做到精准控制,从无超卖现象。
规则决策方案在风控决策、以及各类互联网应用场景比如某些特殊场景发短信、通知等决策可应用,热点账户解决方案则可在账户支付转账、互联网营销管控等场景可供使用。
目前,规则的上线需要开发配合运营人员处理,后续可把规则做成规则包的方式供业务下发生效使用。
本文作者
Haiyan,后端开发工程师,来自 Shopee Financial Products 团队。
技术编辑
Yang,Shopee 技术委员会后端通道委员;
Liping、Chuanliang,来自 Shopee Financial Products 团队。
团队简介
基于 Shopee 服务的市场,Financial Products 致力于打造信贷、保险和投资理财等金融服务。信贷为客户和商户提供消费贷和现金贷服务。在诸多消费场景,提供各种类型的保险购买服务;以及提供即时的基金购买和赎回服务,满足用户的投资理财需求。
我们围绕零售金融持续打造资金、资产、核算、交易、风控、用户、承保、理赔等金融核心服务;另外,在金融方面的资金账务严要求情形下,我们的业务还呈现出场景多、金额小、交易并发高的特点,我们的团队需解决业务扩展性、数据一致性、高并发等多维度的挑战。