由于企业每条业务线都有各自的用户、商家以及运营补贴策略。在开始阶段我们并没有统一的账务系统,每个业务线都有类似账务系统相应的系统。导致的问题就是资金池业务吻合严重,对账难,以及数据不统一,另外成本也非常高,为此我们做了统一的账户系统。账务系统作为到家业务线的基础服务,为到家业务提供统一清算、账户、对账、财务报表能力。

一、账务系统概况

目前账户系统的日均流水金额达上亿级别。我们按照业务线进行分库,保证每个业务线落在同一数据源上,对于数量快速增长的表水平拆分。另外还对一些数据量增长较快,但是只访问近期新增数据的表做了冷热数据分离,定期备份。

二、账务系统架构

1.1、支付平台架构

以整个支付的流程来对支付平台的架构做个说明:用户下单 -> 交易生成账单 -> 用户确认支付 -> 交易校验账单 -> 生成交易请求 -> 收银台调起三方完成支付 -> 同步交易、同步业务线。此时交易系统会对卡券进行核销。
交易系统完成后,账单会推送到账务系统,账务系统进行记录,清算,入账等。

1.2、清结算流程

因为业务的特色,清算分为不同的模块,有银行渠道、合作机构、分佣、优惠券、保险等。清算后生成账户流水,对账户流水做归档、核算等处理。

1.3、对账系统

对账系统是一个非常核心的系统。目前分为多级别对账和多频次对账。多级别分为分账对账:对各种流水,以及总账对账,总分对账:流水与总金额的对账。多频次对账分为日对账,准实时对账,交易推送账单之后约十分钟进行检查,账务系统会查询这笔账单是否存在,以及对账单的金额进行对账。对账系统会尽快的发现问题,进行差错处理。出错处理主要是挂账、补单、退款和登账。

1.4、监控系统

账务系统最核心的问题是稳定、数据准确、无异常,且能在第一时间发现问题,尽量降低问题带来的影响。因此监控系统显得尤为重要。
目前监管系统主要用来做异常报警和数据埋点。异常报警很好理解,就是系统中如果出现错误就会在开发时打错误日志,扫描到有错误出现就会通过手机短信发送给开发人员,开发人员就会尽快查询报警原因和对日常报警是否有数据影响,进行相应的处理。
数据埋点,就是对关心的数据做埋点(如每天总额、账单总额、商家提现总额),对埋点数据进行收集、分析、展现。若展现的数据跟平常有较大差异,我们会找出数据差异原因,看是否有问题。保证有问题能及时发现,及时补救处理。

三、账务系统的挑战

从技术层面来说,主要分幂等性、数据一致性两块。

3.1、幂等性

大多数系统会拆分成多个子系统服务,而一个子系统往往会调用另一个服务,服务之间相互调用就有可能出现服务器处理完毕后没有返回结果的情况。客户端没有接收到返回结果,就可能重复调用。幂等性是系统的接口对外的一种承诺(而不是实现), 承诺只要调用接口成功, 外部多次调用对系统的影响是一致的。
目前账户系统主要是根据业务需求做了几种幂等性
1、充值时对充值流水号做了唯一的处理,外部调用充值接口时,传一个唯一的流水号,判断这个流水号在系统中存在,就返回,多次调用,也不会给这个账户进行累加充值。
2、对奖惩做了外部唯一 ID 的幂等性,调用者会传入唯一 id ,根据唯一 id 判断此笔奖惩是否已经进行清分入账。
3、订单则有区别,因为订单有一个退款的流程,如果把订单做唯一性,订单发生退款时无法区分这个订单是新增加的,还是退款的,这样的话账户的金额就可能不准确。因此采用了账单金额轧差。
订单轧差是什么呢?举个例子,商家第一次支付的金额,传到账务系统进行清算,比如通过支付宝支付 100 元,那么给这个商家进行入账,入 100 元,下次该订单发生了退款,假设退 90 元(此时交易系统会将该订单实际支付金额传到账务系统,即 10元),实际上本次需要给商家入账 -90 元,而不是 10 元,此次用账单的金额 -90 元跟旧的金额 100 进行一个轧差,利用轧差结果进行清算入账。
4、提现失败返还,根据申请提现生成的提现 id 反查账户流水表,看是否有提现并且没有提现返还,根据流水金额反向生成流水返还记录及入账,保证幂等性。

3.2、数据一致性

1、本地事务
按照业务线进行分库,使得对同一个业务线的操作均落到同一数据源上,利用本地事务进行数据的访问和更新,从而保持数据一致性。
2、柔性事务(需幂等)
主要采用了 TCC 模式和事务补偿的模式。
T:Try。 完成所有业务检查,预留出必须业务资源);
C:Confirm。真正执行业务,不做任何业务检查,只是用 Try 阶段预留的业务资源;
C:Cancel。取消执行业务,释放资源。
在 Try 这段,是对这个数据进行一个验证,验证通过之后,将数据资源进行预留。在 Confirm 阶段,只对这个数据直接进行操作,而不进行验证处理,但是只操作预留这部分的数据。若是发生取消的操作,将预留的操作返回到账户。TCC 模式主要是用于余额消耗。当商家只能用余额支付时,先将其余额支付的金额进行冻结,真正对账单进行结算时,只使用第一部分冻结的金额,而期间不做任何验证,订单完成支付时,对它冻结的这部分金额做真正扣减。如果中间订单取消了,将冻结的金额进行返还,这样保证账户里的金额真正发生消耗时是充足的,不用进行反复的检查。
事务补偿就是操作失败后的补偿。申请提现的时候将账户的金额进行扣减,只要申请提现成功,那么账户金额就做扣减。真正提现出金结果,则会通知账务系统,若出金失败,账务系统对提现系统的 ID 进行查询,按照提现申请时生成的账户生成提现失败返还流水,按照返还流水更新账户金额。
3、消息队列(消息应答ACK机制)
交易系统是通过发送异步消息到账务系统,账务系统处理成功之后,ack 消息;处理失败则不 ack 消息。利用消息系统重复发送机制再次发送消息,保证账务系统每条消息均可处理成功。但可能会发生 ack 消息后,消息系统未接收到,那么就会重复的发送。所以用消息队列 ack 机制,一定要保证幂等性。例如:RabbitMQ支持消息应答。消费者发送一个消息应答,告诉RabbitMQ这个消息已经接收并且处理完毕了,RabbitMQ就可以删除它了。
4、分布式锁
直接锁定资源避免了多节点操作引起的数据不一致,只有业务上的硬性要求的时候才用,目前用于控制提现申请的频率。