一、前言
原先携程内部的各账务系统都是随着自身的业务发展而建立起来的,其中有些共同的东西,但也存在着不少差异。但其对底层业务的抽象是统一的,都可以抽象为:账户开立、记账、稽核。为了系统开发、运维的简便性,也为了更好的为前台业务提供支持,我们计划实施账务中台系统,从而做到账务系统的:
1)敏捷:快速的适应业务需求的变化,满足外部快速变化的需求,实现业务的敏捷。
2)解耦:建立功能独立的系统,避免因为一个功能的修改而影响很多方面,降低功能的耦合度。
3)复用:对一些公共组件的复用,提高开发效率,避免重复建设,使得数据和流程都可以得到管控。
二、账务中台之路-基础篇
2.1 系统概述
2.1.1 背景
账务系统从 2014 年开始一期建设,先后经历了两次大的技术架构升级。
一期:
账务组刚成立的时候,携程的 JAVA 技术栈尚未完善。
技术上我们是首批试点使用 JAVA 的小组,使用的分布式 RPC 框架是 Zeroc ICE,它可以支持多语言,通过 slice 文件生成代码,性能也比较好,所以当初选型用了这个。消息队列用的 RABBITMQ,缓存用的 REDIS。这些集群都是自运维的。
业务上,一期的业务只支持单用户单账户的模式,交易支持充值、支付、退款、预授权类(预授权冻结,预授权撤销,预授权完成,预授权完成撤销)、提现、转账,接口都是基于业务接口独自开发的。
系统业务架构图如下:
二期:
随着携程 JAVA 技术栈的完善,二期主要针对 JAVA 技术栈进行了升级,放弃了自运维的集群,使用了携程 JAVA 体系,包括 SOA,qconfig,qmq,qshecdule 等技术。业务上增加了会计系统和日终的实现。
系统业务架构图如下:
原有的账务核心系统存在以下问题:
1)抽象不足:对业务规则抽象不够,如果新增业务需要编码实现
2)隔离不够:系统不同业务的系统流量、数据没有隔离,存在某一业务有问题,影响全局的风险
3)降级策略:不支持
4)扩容困难
基于以上问题,我们设计并实现了新的统一账务平台。
2.1.2 目标
针对旧系统的不足,我们确定统一账务平台的目标:
1)抽象
2)隔离
3)易扩容
4)配置化
5)支持多机构多币种
2.2 系统架构与简介
统一账务系统旨在建立一套立足于携程集团之下的高可用,易扩展,业务可定制的账务系统。系统包括场景码系统,账务前置系统,账务核心系统,账户管理系统,会计系统,异步系统,job 系统,日志系统。各个系统之间通过 dubbo 进行服务拆分解耦。
系统业务架构图如下:
前置系统:账务的业务处理系统,主要负责对上游业务系统的对接,完整账户的拆分等工作。
账务核心系统(原子系统):主要负责账户记账,记录对商户、用户、内部户等客户账的动账及明细。
管理系统:对外提供商户、用户、内部户的管理服务,包括创建、查询、状态冻结、状态解冻等服务。
会计系统:采用复式记账法根据分录规则对发生的交易进行记录,来表示资金的流转。
基础服务系统:对外提供科目、分录、交易码等基础配置的查询服务。
日终系统:对记账原子和会计系统数据进行稽核,完成数据校验工作。
2.3 系统设计
2.3.1 基础组件设计
2.3.1.1 日志组件
我们日志进行 logger 输出,会碰到以下的痛点:
1)我们经常会对方法的入参,出参及异常进行日志打印,还要把 tag 写入 clog 和 ES,手写的话工作量巨大。
2)有些日志需要脱敏处理,比如手机号、身份证号、卡号,不能把明文输出到日志。
3)logger 目前只支持抛公司的 logger 日志平台,部门想自定义日志查询分析工具比较困难。
所以在设计统一账务中台化的工程中,进行了日志组件的设计:
1)统一使用高性能的 log4j2 替代 logback;
2)通过 spring aop 和 annotation,支持方法入参、出参、异常日志的自动打印;
3)支持 clog 和 es 的 tag 的配置,可以从参数中获取,并通过 log4j2 的 ThreadContext 打入本地线程,线程使用过程中 tag 共享,代码如下所示:
4)支持配置脱敏规则,进行敏感信息的脱敏处理;
5)同步抛公司的 clog 和 cat,异步抛 kafka,异步接受程序进行 ETL 处理,抛部门自己的日志系统(比如鹰眼系统,hive 日志分析系统);
6)抛 kafka 时,对于原始报文进行 apache arvo 压缩处理,减少传输带宽;
7)支持原生 API,可以手工打 tag 和脱敏处理。
流程图如下:
2.3.1.2 分库分表组件
分库组件,我们调研过公司现有的和开源的组件,最终选用了开源的 sharding-jdbc。
1)携程的 dal 组件
dal 使用 dal cluster 通过服务端配置分库分表信息,但是全套使用 dal 太过笨重,它是一个完全的 ORM 框架,生成 sql 的工具不支持生成特殊自定义的 sql。
2)去哪儿的 qdb 组件
通过配置表达式或算法的方式进行分库分表配置,缺点是文档较少,容易踩坑。
3)MYCAT
Mycat 是一个中间件,它拦截了用户发送过来的 SQL 语句,首先对 SQL 语句做了一些特定的分析:如分片分析、路由分析、读写分离分析、缓存分析等,然后将此 SQL 发往后端的真实数据库,并将返回的结果做适当的处理,最终再返回给用户。Mycat 的缺点就是需要搭建一套中间件做拦截者,而且需要自运维,成本比较高。
4)sharding-jdbc
当当网首先推出的开源分库组件,现在变成 apache 项目,分为 sharding-jdbc、sharding-proxy。开源社区活跃。我们使用轻量级的 sharding-jdbc,可以编写算法,支持精确分片、范围分片、复合分片和自定义 hint 分片,配置方式支持 xml、yml 和 java api 方式。基本能解决我们所有的分库分表需求。我们把分库算法包成 jar 包,方便使用。配置我们使用 yml。在使用过程中,需要结合 dal cluster 的 key,代码示例如下:
2.3.2 前置系统设计
账务前置位于整个账务体系的最上游,提供标准的交易接口,包括入款、入款返还、出款、出款返还、预授权类、转账以及批量接口。
2.3.2.1 标准的交易接口
整合之前的老系统,都是业务导向的接口,随着业务的不断迭代,接口越来越多,职责不清晰,代码重复,给维护带来很大的工作量。比如:光提现接口,就分为个人提现、返现提现、商户提现和定向提现。另外,原先的子账户的交易顺序是硬编码的,如果发生子账户的增加或交易顺序的变化,带来的复杂度就成倍增加。
我们经过研究,发现账务处理是有共性的,对于交易顺序、原子交易类型都是可以提取出属性的。所以我们建立了场景码模型。
首先,我们定义子账户 id,按账户类型+币种+业务类型唯一定义一个子账户。
其次,按产品代码+交易类型来定义一个交易顺序,交易顺序关联子账户 id,该顺序设置为默认的场景码。接口只要传入产品代码和交易类型就能能走默认的场景码。
第三,支持商户自定义场景码,我们维护了一个后台管理系统,允许商户自定义场景码,审核通过后,接口传入该场景码编号就可以走自己定义的场景码。
2.3.2.2 异步化
我们接口都是同步接口,为了减少同步响应时间,把次要的工作通过 mq 进行异步化处理。比如:转账的转入方入款,抛送会计等。
2.3.2.3 数据库策略
除了支持自己的支付业务,也支持把账务系统输出到其他 BU。为了数据权限及互不影响,我们做了数据隔离。需要特别说明的是,只做数据隔离,系统还用同一套,不做隔离,方便发布和运维。数据隔离分两层,第一层是 domain,区分自有/BU。第二层是具体分库。
sharding 库也分两套,Mapping 库和交易库。Mapping 库存放请求流水号和前置流水号的关系。交易库存放所有的交易信息。特别地,我们把逆向交易和原交易落在同一 DB 中,这样有利于控制逆交易和原交易在一个事物内。
首先,我们使用请求流水号做 hash 算法,分散到 mapping db。Mapping db 只保存请求流水号和前置流水号的关系,Mapping db 也是分库的,分库数量初始是固定的,以后扩展可以用一致性 hash 算法进行扩容。
我们的交易表是通过权重配置来分库,通过权重可以进行数据的自由分配。DB 支持友好的扩容,下线和故障切换。我们有一套故障切换的方案,如果某个分片出现 dbconnect 异常,我们会抛送支付的监控系统。监控系统会有套智能算法会实时监控,达到阀值后自动触发该分片的 markdown/markup 机制。
2.3.2.4 异常处理
高并发场景下的异常处理一直都是各方研究的课题。为了适应高并发场景,前置做了如下的异常处理:
1)幂等机制:所有接口都支持幂等操作。
2)重试机制:接口内部调用的重试,job 补偿重试,上游也可以发起相同请求报文的失败重试。
3)查询机制:所有接口都写了一套查询接口,上游可通过查询接口查该交易的最终状态。
4)通知机制:支持成功/失败的结果主动通知上游的机制。
2.3.3 原子系统设计
原子系统流程处理中,主要有以下几步:订单参数预处理,分单,同步执行器,异步执行器,后处理,最后封装参数返回。为方便业务扩展,系统维护,在分单和执行部分,系统架构采用责任链模式的分单器;代理进行分单,产生 drivers,再由系统自动注册的同步执行器和异步执行器进行执行。目前,只有订单登记簿采用异步方式完成,后续 job 系统中会对任务进行相应的补偿。
下图为原子系统的系统架构图:
PreBizProcess 是参数预处理部分根据调用域不同进行了定制化校验与信息构造。
DispatcerControl 是分单部分,因为原子系统负责账户余额的管理,不存在任何业务逻辑,所以可以将记账模型进行抽象以适应不同的业务需求。记账模型包括账户入账与出账(Accnt)、活动入账与出账(Activity)、资金冻结与解冻(Fund)、日登记簿入账与出账(DaliyBook)、订单登记入账与出账(OrderBook)等记账模型。针对有效期概念的账户,增加了登记簿管理,日登记簿对账户同一有效期的资金进行汇总,订单登记簿是有效期的资金的订单维度的记录。
SynExecutor 是同步执行器主要负责出账入账,资金冻结解冻,日登记簿处理;AsynExecutor 是异步执行器负责订单登记簿的记账操作。日登记簿采用同步执行的方式,而日登记簿记账成功可保证订单登记簿记账成功,故订单登记簿采用异步记账既可以保证记账成功又能减少系统同步处理的时间。
后处理部分会发送动账消息,给关心账户余额变动的系统,比如风控。
2.3.4 会计系统设计
会计系统采用复式记账方式完成,可以清晰的表述资金是从哪里来并流向了哪里,针对不同的业务涉及不同的清分规则。在清分规则中可以配置记账的不同策略,比如单条、汇总记账等不同策略。
针对同一业务多科目的场景,添加扩展配置,实现清分规则的科目动态化。
2.3.5 日终系统设计
2.3.5.1 为什么需要日终系统
1)提供账务系统支撑
要保证账务系统能正常运转,账务的余额要 100%准确。影响账务系统稳定性的主要因素有一下几个方面:
缓冲记账,问题表现:分录有数据,明细无数据
会计异步记账,问题表现:明细有数据,分录无数据
分录规则配置,问题表现:同一笔分录中,借贷方金额不一致
2)为企业大财务提供汇总记账凭证
账务系统记录的是业务账,这些数据是整个企业财务数据的一部分,需要合并到公司的大财务系统中去。
日终系统对会计分录进行加工映射为大财务的分录,然后汇总,直接对接企业 ERP 总账。
2.3.5.2 日终系统都做了什么
1)生成快照
每日凌晨统计截至上一日的所有账户的快照。
2)生成分户账
根据快照生成分户账。
3)生成总账
根据分录流水生成科目总账,科目发生额和余额从末级科目逐级汇总到一级科目。
4)总账平衡检查
余额平衡检查:一级借方科目余额=一级贷方科目余额;
发生额平衡检查:一级科目借方发生额=一级科目贷方发生额
5)总分核对
总账科目余额等于分户账科目余额。业务 24 小时不间断运行,账户中余额在不断变化,无法准确取到期末的账户余额进行核对,采用余额快照与总账科目余额进行核对。
6)稽核明细
检查明细账与分录流水是否一致。对于当日发生过余额变动的账户,昨日余额与分录流水中的发生额进行轧差,检查计算出的余额与快照余额是否一致。
2.3.5.3 日终系统遇到的挑战
1)24 小时记账
在银行账务系统中,对于 24 小时运行,有很多种方案,例如切换余额、记不同分户账、日切后补流水等,但无论哪种方案,都不能实现完全 24 小时运行。
其问题,主要是因为日终要进行总分核对,而分户账余额是在不断变化的,所以要想办法把期末的分户账余额取出来进行核对。
本日终系统解决方案,采用余额快照与总账进行核对,这样即使分户账余额进行变化,也不影响总分核对。
2)生成账户快照
生成快照的方式有两种:
从账户余额中获取
交易明细按账户汇总发生额更新快照
相较于数亿账户而言,每日发生交易的则要少得多。采用动账汇总的方式,对于数据库的操作更少,处理时间更快。
3)流程复杂、测试困难
对日终任务模型进行抽象,按照业务边界划分为:快照生成、分户账生成、总账生成等多个子任务,自动注册到任务工厂中,以便编排调用。
2.4 总结
此系统业务接入规则、会计清分规则都是基于配置的,在业务发展的新增账户类型、业务、币种、机构等日常变化都可以基于配置进行。
三、后记
账务中台建设到现在,已经完成了携程体系内账务中台的基本建设,这只是中台建设的第一步,后续规划还包括分布式事务、热点账户的处理;新机构业务接入如何更简洁等等。
作者简介
本文为联合撰文,作者团队负责携程集团支付账务系统、消费金融账务系统、清结算和对账等工作的的开发、设计和运维工作。
本文转载自:携程技术中心(ID:ctriptech)
评论