账务实时交易系统设计思考

   日期:2024-12-01    作者:caijiyuan 移动:http://oml01z.riyuangf.com/mobile/quote/5993.html
账务实时交易系统设计思考 系统设计

作者 : 杨考  微信号 : devin_cn_hd_09_16

本文是【讲解篇】和【技术分享篇】结合起来,由于CSDN文章图片丢失,又补了一次图片。

 

 

账务交易主要是指,在资金流发生的时候,需要根据资金的流入和流出情况,对涉及的账户金额进行增加和减少操作,更新资金的时候,同时需要生成相应的账单,以便后续查询和对账等使用。

提及账务交易,大家并不陌生,古代就开始有账务的概念,小到家庭,大到公司,差别在于记账方式不同而已,家庭一般都只记录流水,而大的公司,账务会有多个掌柜管理,同时根据业务会细分账务类型,完善记账流水,实现资金无缝监控。

 

   

 

     

 

 

 

1.3.1 资金流多样性

  1. 1) 多样性是资金流的一个典型特征

  2.       1.1) 资金有流向

  3.       1.2) 有金额

  4.       1.3) 有流入对象

  5.       1.4) 可以收回资金

  6.       1.5) 资金错误的时候,可以进行调账或者各种形式补偿。

  7. 2) 资金链的关联

  8.       2.1) 资金流可以在一个链条上

  9.       2.2) 也可以在不同的链条上

  10.       2.3) 不同链条上的资金流通过账单进行关联

1.3.2 资金流的模型(有向图:线型、树形、网状)

  1. 1) 资金流简单模型是线型

  2. 2) 复杂一点是树形

  3. 3) 最复杂的是网状

  4. 用专业术语一句话概括描述,资金流的模型就是一个有向图

1.3.3 账单(只读)

          1) 资金流处理的时候,需要生成相应的账单,用来记录每个账户中出、入资金额度,用途描述,以及账户剩余资金的详细信息记录

          2) 账单严格意义来讲,一旦生成,不能修改,如果发生退款或者金额错误,如何进行处理呢

               2.1)  发生退款,原账单保留不变,生成一个退款交易账单

               2.2) 发现金额错误,可以生成一个补偿账单或者调账账单

         总之,账单是只读,不做任何修改,这是账单的特征

 

 

账务实时交易系统,就是一个万能掌柜,首先能处理各种业务,其次是实时,最重要的是准确、可靠。

如何做到实时和准确,下图有详细信息可供参考。

 

 

如上图示,简单总结了账务实时交易系统所处的位置

所有的业务交易都可以虚拟为订单,订单再经过账务实时交易系统的处理,就可以将金额分配到相应的账户。

如下是外卖的账务交易业务模型,现在需要思考如何根据订单的状态,实现金额的更新、分账和账单的生成。

 

 

上面讲了这么多,看看需要接入的实际业务业务有哪些?业务特征是什么?业务模型是否符合理论分析(有向图)

在线支持业务

订单分类

外卖平台单

 

 

 

 

物流订单

物流外单

随意购

自配送

百度快递

骑士打赏

CPC推广费用管理

 

虚拟订单

CPT费用管理

用户提现、打款

用户订单

用户订单

… …

… …

 

如下图示是业务模型的一个汇总

1) 树形

2) 包含多个账户,业务众多,角色众多

3) 多层、多级分账

4) 任何一个层级都可以发生取消分账,或者取消部分或者全部分账

5) 单账户的交易类型丰富(如:骑士账户的申诉、奖励、补账、餐损、惩罚 ...)

6) 如果一个账户存在多种交易类型,可以通过业务或者描述信息来区分每个交易。(将图转化为树,或者将子树连接到整棵资金流树上)

总结:虽然账务交易角色众多,交易类型丰富,但是模型还是很典型的树状模型,当前接入的业务是如下图示模型,或者如下图示模型的子树。

        而且新的业务也不局限于如下模型,凡是资金流无论交易层级有多少,都可以被归结为如下模型的处理范围。

 

2.2.1 业务典型操作归纳

针对业务模型,业务操作很简单的可以归纳为如下几个典型操作

1)入账、

2)出账、

3) 转账、

4) 分账、

5) 取消

2.2.2 资金流的状态(无状态和有状态)

另外通过资金的依赖,可以分为无状态依赖和有状态依赖两种

1) 无状态资金处理-没有任何的状态依赖,即罚款

2) 有状态资金处理-用户的支付金额,需要为业务进行细分再分给相应的商户和骑士等账户

2.2.3 账户操作:

1) 金额的增加

2) 金额的扣减

 

总结,业务和账户操作呈现喇叭型业务模型,或者倒锥形型业务模型

1) 底层操作接口简单

2) 生成数据格式统一

3) 支撑业务多样化

 

 

2.3.1 账户构建元素满足账户多样性

要接入业务众多,因此账户也变得异常的丰富。

如:青岛众包配送收入账户, 实际来讲,就是青岛市(城市)的众包配送(业务)收入账户(主体:财务)

简单的账户构建元素组合,即可完成丰富的账户信息描述。

2.3.2 账户类型是账务交易方式的体现

至于每个账户,因为金额操作的需求,需要设定不同的账户类型

1) 现金账户,完成现金业务交易

2) 冻结账户和淘宝的交易资金冻结功能相同,订单下单,资金入冻结账户,交易完成资金从冻结账户转出到现金账户;

3) 监察账户,提现账户...等就不做详细介绍了。

 

设计的几个原则

1) 功能、准确、实时是基础

2) 性能、效率是关键

3) 健壮和可扩展是平台化所需。

 

 

资金操作,用来承接业务的配置,生成资金流转状态的记录,保证资金准确分配。

对内使用:生成交易明细、提供交易记录实时查询

对外输出:为对账、查账、打款等提供数据支持

 

3.4.1数据唯一性(四元组)

 

数据唯一性是数据准确性的保证

数据唯一性是数据关系、数据关联的前提

 

账户操作的唯一性实现,如下四元组详述

1)订单ID           如用户订单ID,商户订单ID,物流订单ID,各业务选择自己的订单ID订单不一定局限在百度外卖,可以是任何平台的订单【如下示例的 order_id】

2)业务描述       如"百度外卖用户订单""百度外卖商户订单""百度糯米", ... 通过业务准确描述,可以完成不同公司不同业务的订单描述和区别 【如下示例的business_desc】

3)账户ID           平台为主体创建的账户 【如下示例的 account_id】

4)账务资金操作描述    结合业务场景和财务需求进行规定,如"点击消费扣费""转账入账""转账出账","充值入账" 等 【如下示例的 trade_desc】

 

订单ID+业务描述,保证整体账务系统中,业务订单ID的唯一性

账户ID+账务资金操作描述,保证同一账户ID在订单中多次出现时的资金操作唯一(如上 3.3.1 中列举的账户 1111111111777777,同一个资金流中,存在多笔不同的入账、出账操作,需要通过账务资金操作描述进行唯一性区分

保证了资金操作的最小可查询粒度和资金监控的最小粒度

 

order_no  : 业务订单号

 

order_type : 业务类型,区分业务类型,防止不同业务order_no重复,且方便按业务维度查数据

account_id : 账户ID

op_code : 操作码,去重;该订单该账户的唯一标识;可配置;可控

如下图示,就是典型的,同账户通过op_code去重的方式。

 

3.4.2 一棵整树和多可相对子树(绝对父子关系和相对父子关系并存)

 

绝对父子关系和相对父子关系并存

1) 提升操作效率(有效树在一棵绝对树上)

2) 提供按业务分段存储(查询子树即可)

3) 维护一棵整树(资金流完整统一,资金关系简单化)

4) 子树用来承接不同的业务,即资金流是一棵整树,不同的业务在资金流这棵整树上的一个子树。

3.4.3 数据唯一性支撑了图到树的转换

资金流是有向图,需要完成图到树的转换处理

 

 

3.4.4 分账模型表述

 

3.4.4.1 分账模型

 

3.4.4.2 分账配置命令格式(分账模型表述

$postData = array(    'trade_commands' => array(   // trade_commands 批量操作方法配置         array( // 第一组分账操作命令             'trade_method' => 'splitAccount', // 金额操作方法-分账                 // order_id, business_desc, account_id, trade_desc 四元组,本次操作出账的唯一标识,在整个数据表的唯一标识                'order_id' => 'userOrder_150121548745', // 业务订单ID,用户订单                'business_desc' => '百度外卖用户外卖订单', // 业务描述                'account_id' => 98989777783780001, //余额出账账户的账户ID                'trade_desc' => '用户下单', //金额操作描述                 'amount' => 1200, // 出账金额,单位(分)         1200=2500+1000-1500-800   会校验资金平衡关系                 'remark' => '商户接单', // 备注信息                 'split_formula' => array(// 入账账户信息配置                     // order_id, business_desc, account_id, trade_desc 四元组,本次操作入账的唯一标识,在整个数据表的唯一标识                     '55555666666666' => array( // 入账账户1的账户ID,以及配置信息                        'order_id' => 'shopOrder_jljljljlljl', // 商户订单ID,没有生成商户订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID                        'business_desc' => '百度外卖商户外卖订单', // 业务描述                        'account_id' => 55555666666666, //入账账户1的账户ID                        'trade_desc' => '商户营业输入', //金额操作描述, 如果账户 55555666666666 在分账流中只有一次流入,则这里trade_desc可以不设置,如果有多次,则trade_desc不能重复                         'amount' => 2500,                         'remark' => '',                     ),                     // order_id, business_desc, account_id, trade_desc 四元组,本次操作入账的唯一标识,在整个数据表的唯一标识                     '222222233333333' => array( // 入账账户2的账户ID,以及配置信息                         'order_id' => 'logistics_5245787854745', // 物流订单ID,没有生成物流订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID                        'business_desc' => '百度外卖物流冻结账户', // 业务描述                        'account_id' => 222222233333333, //入账账户2的账户ID                        'trade_desc' => '物流接单', //金额操作描述, 如果账户 222222233333333 在分账流中只有一次流入,则这里trade_desc可以不设置,如果有多次,则trade_desc不能重复                         'amount' => 1000,                         'remark' => '',                     ),                      // order_id, business_desc, account_id, trade_desc 四元组,本次操作入账的唯一标识,在整个数据表的唯一标识(本资金流中,1111111111777777两次操作的trade_desc不同)                     '1111111111777777' => array( // 入账账户3的账户ID,以及配置信息                         'order_id' => 'platform_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID                        'business_desc' => '百度外卖新用户补贴', // 业务描述                        'account_id' => 1111111111777777, //入账账户3的账户ID                        'trade_desc' => '新用户补贴出账', //金额操作描述, 如果账户 1111111111777777 在分账流中出现了两次,因此该账户在本次交易流中的四元素必须不同                         'amount' => -1500,                         'remark' => '',                     ),                      // order_id, business_desc, account_id, trade_desc 四元组,本次操作入账的唯一标识,在整个数据表的唯一标识(本资金流中,1111111111777777两次操作的trade_desc不同)                     '1111111111777777' => array( // 入账账户3的账户ID,以及配置信息                         'order_id' => 'platform_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID                        'business_desc' => '百度外卖新用户补贴', // 业务描述                        'account_id' => 1111111111777777, //入账账户3的账户ID                        'trade_desc' => '优质用户嘉奖', //金额操作描述, 如果账户 1111111111777777 在分账流中出现了两次,因此该账户在本次交易流中的四元素必须不同                         'amount' => -800,                         'remark' => '',                     ),                 ),             ),             array( // 第二组分账操作命令                 'trade_method' => 'splitAccount', // 金额操作方法-分账                 // ....             ),

       ), );

 

 

 

 

3.4.5 资金关系存储算法选择

3.4.5.1 资金关系存储模型

3.4.5.2 资金关系存储示例:

array(     // 资金交易上游账户唯一信息     'absolute_parent' => array(   // 绝对上游信息         // order_id, business_desc, account_id, trade_desc 四元组的唯一性

        'order_id' => 'userOrder_150121548745', // 业务订单ID,用户订单

        'business_desc' => '百度外卖用户外卖订单', // 业务描述         'account_id' => 98989777783780001, //余额出账账户的账户ID         'trade_desc' => '用户下单', //金额操作描述     ),     // 本业务资金的入口,在本业务属于分账根节点,相对上游为0,方便业务块操作     'relative_parent' => array(),    // 相对上游信息         'amount' => 2500,    // 分账金额  2500 = 2300+200         'is_split' => 1, // 0:可以分账 1:已分账         'child_details' => array(     // 绝对下游信息             // order_id, business_desc, account_id, trade_desc 四元组的唯一性             '8888888888881111' => array( // 入账账户                 'order_id' => 'shop_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID                 'business_desc' => '百度外卖商户净收', // 业务描述                 'account_id' => 8888888888881111, //入账账户3的账户ID                 'trade_desc' => '商户净收入', //金额操作描述                 'amount' => 2300,                 'remark' => '',             ),             // order_id, business_desc, account_id, trade_desc 四元组的唯一性             '8888888888883333' => array( // 入账账户                 'order_id' => 'platform_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID                 'business_desc' => '百度外卖商户抽佣', // 业务描述                 'account_id' => 8888888888883333, //入账账户3的账户ID                 'trade_desc' => '平台抽佣金额', //金额操作描述,                  'amount' => 200,                 'remark' => '',             ),         ) );

基于四元组唯一性的定义,每笔账户操作在整表中都是唯一的,因此可以破解交易资金流向的网状关系(有向图),实现树状结构关系存储。

1、根据四元组唯一性,入账账户可以快速查到上游出账账户,且双向关系唯一

2、根据四元组唯一性,出账账户可以快速查到资金流入的所有账户,且出账账户和每个入账账户之间的关系唯一

优化点:建立绝对上下游关系相对上游关系

              绝对上游:跨业务账务的上游账务节点是一个真实的唯一节点

              相对上游:每个业务入口账务节点的上游账务节点是0,即该节点是本业务的入口节点

 

3.4.5.3 账单格式

array( // 账单1     'order_id' => 'userOrder_150121548745', // 业务订单ID,用户订单     'business_desc' => '百度外卖用户外卖订单', // 业务描述     'account_id' => 98989777783780001, // 余额出账账户的账户ID     'trade_desc' => '用户下单', // 金额操作描述     'flow_type' => 'out', // 资金出账     'trade_type' => '交易出账', // 交易类型     'amount' => 2500,

    'remark' => '', );

array( // 账单2     'order_id' => 'shop_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID     'business_desc' => '百度外卖商户净收', // 业务描述     'account_id' => 8888888888881111, //入账账户3的账户ID     'trade_desc' => '商户净收入', //金额操作描述, 如果账户 8888888888881111 在分账流中出现了两次,因此该账户在本次交易流中的四元素必须不同     'flow_type' => 'in', // 资金入账     'trade_type' => '交易入账', // 交易类型     'amount' => 2300,     'remark' => '', );

array( // 账单3     'order_id' => 'platform_xxxxxxxx', // 业务订单ID,没有生成平台补贴订单前,可考虑使用资金出方向(用户订单ID),后者创建一个公共的该资金流的私有ID     'business_desc' => '百度外卖商户抽佣', // 业务描述     'account_id' => 8888888888883333, //入账账户3的账户ID     'trade_desc' => '平台抽佣金额', //金额操作描述, 如果账户 8888888888883333 在分账流中出现了两次,因此该账户在本次交易流中的四元素必须不同     'flow_type' => 'in', // 资金入账     'trade_type' => '交易入账', // 交易类型     'amount' => 200,     'remark' => '', );

 

 

 

3.4.7 资金流按业务分区块

 

1、方便按业务块:每个业务作为一棵子树,存在于整体资金流中,方便资金按业务区块的简易、高效管理

                              业务需求:方便每个业务管理本业务的资金

2、方便完整资金流查询:所有业务共用一棵资金流树,整体资金流可以方便查询,且资金关系完整易维护。

                            财务需求:一棵完整的资金流树,方便财务跟踪整体资金流向

 

3.4.8 资金操作接口API

3.4.8.1 完善的API接口

入账、出账、分账、调账和撤销等,实现资金操作接口统一化

 

3.4.8.2 规范、丰富的交易类型

交易类型场景化和统一化

交易类型可定制

丰富交易类型,实现按类型归纳、追踪、查询和统计等

 

3.4.8.3 资金分配状态机

资金操作API的背后,是规范化了的资金分配关系数据

资金分配关系数据,可以确保资金的准确分配

记录资金分配的完整过程

资金分配状态机的载体

同时通过资金分配关系,可以复盘、回放资金交易的完整过程

 

3.4.8.4 资金撤回

1、支持取消任意账户分账(单节点取消分账

2、支持递归取消任意账户下的分账(按业务区块递归取消多节点分账

3、支持递归取消整个资金流的分账(取消整棵资金分配过程

 

 

 

支持批量请求:支持单个请求处理,也支持批量请求处理

统一事务管理:同一个请求中,无论是包含单个请求,还是多个请求,有一个请求失败,整个操作都需要回滚

统一资源管理:其中包括数据库连接、账户锁等资源,操作串行化,数据锁互斥。

统一异常处理:错误抛异常,异常格式统一化...

 

 

 

赘述一下交易回放的实现 : 所有的分账,都有唯一的操作记录,分账数据不复用不重用,这是交易记录可回放的前提,同时也是业务正确性的支撑(需要清除无效的历史数据)。

账户是一个概念,账户可以承载补偿、积分、优惠券等特殊的资金。

上面说了一大堆,如下图示简单总结,小清新一下。

 

 

 

如下是几大热点问题:每个问题的具体处理方案在后面有详述。

简要解析:同步和异步并存,是一个综合方案,可以保证账务处理的实时性,如下方案的优、缺点以及引发的问题,在下图有详述。

 

 

 

4.3.1热点账户的概念: 

     热点账户就是在交易过程中,出现频次特别高的账户,交易频次指的是某个时间段的交易频次一直保持在比较高的次数。

     如果是数据操作错误重试导致某账户瞬时出现高频操作,则不属于热点账户范畴。

 

4.3.2 热点账户的判别标准

    1) 账户每秒有10次以上更新需求

    2) 串行化时账户处理延迟高于1秒以上

 

4.3.3 热点账户的处理方案

 

热点账户类型

账户属性

实时需求

锁需求

处理方式

性能

业务大账户

内部账户

无实时余额查询

无实时提现

无需加锁

异步

满足

大代理商账户

 

对外账户

无实时余额查询

无实时提现

没有加锁需求

异步

满足

热门商户(推广)

对外账户

商户账户

实时余额查询

实时提现

有加锁需求

串行化同步

亟待提升

 

4.3.4 热点账户总结

1) 在异步化背景(账户实时处理的上游,如果已经存在了异步化的处理)下,此时业务所需要的下游的实时性是不可能完全实时的

2) 对于热点账户而言,问题在于一条数据表项的更新频次已经达到了上线,所以解决热点账户的方案可以从解决数据读取的瓶颈出发。

 

 

 

 

 

4.5.1 实时账务更新

4.5.2.1 完全实时设计实现

业务直接调用账务系统交易的API接口,实时处理账务交易,所谓的实时交易,也是进行了一定的优化(同步和异步相结合)。

1) 商户账户扣费,使用的是实时扣费

    a)账户更新使用redis锁代替数据库锁,同样也是悲观锁,不过请求不阻塞,请求延迟很小

    b ) 请求锁失败,业务继续重试请求。

    c ) 平台内部账户仍然采用异步余额更新,减少了分账的锁阻塞几率,有效保证了同步更新的成功率,即减少了锁请求失败的概率。

 

2) 百度外卖推广收入账户,同样采用的是异步更新账户金额的方式。因为这个账户是平台共用,属于热点账户,锁阻塞很严重,但是该账户的任何行为和表现不会影响到商户账户余额更新,所以这里使用异步更新的方式,消除了热点账户的影响。

4.5.2.2 完全实时场景

CPC(按点击扣费)业务,账户提现等

 

4.5.2 上游调用方对用户或者账户进行分桶,防止同一账户被同时更新而造成的冲突

 

 

 

 

 

 

4.6.1 准实时的实现

 

 

各业务的资金分账,入消息队列,调用账务交易平台资金操作接口,账务平台按分账需求和分账状态处理账务消息

4.6.2 准实时性能

通过消息队列处理账务交易信息,QPS基本不是瓶颈

通过对业务和订单散列细分,账务交易系统可以有序、快速处理资金

支持批量请求,从业务层解除数据依赖和状态依赖

最终测试结果:账户余额更新最差延迟时间控制在15秒内。

 

4.6.3 准实时业务场景

用户订单,物流订单等账务处理都是非实时需求场景

 

 

 

 

 

1、账务交易异常,MQ堵塞

 

2、业务订单异常监控

 

3、涉及账户人员入账错误会第一时间联系客服(仅限于少入账 J)

 

4、业务通过订单数据可以修复账务交易错误

 

 

1、交易维度 : 上条记录余额 = 当前发生额 + 当前余额

 

2、按订单维度 : 该订单的所有账户的发生额总支出=发生而总收入

 

3、余额快照 : 账户当日的期末余额=期末余额+发生额

 

4、余额快照 : 当日所有账户的期期末余额=期末余额+发生额

 

5、常规对账

 

 

 

 

 

 

 

设计是一个找平衡点的过程

一条是功能线,(概要如下图)

一条是性能线,(概要如下图)

寻找功能和性能的平衡,如果有任何一点出了问题,就无法支撑业务的需求。

最终的原则,就是在支撑业务需求的前提下,进一步提升性能和可扩展性。

 

找好平衡点

抽象、统一、数据化

深入、专一、平台化

 

谢谢

 

 

 

 

 

 

 


特别提示:本信息由相关用户自行提供,真实性未证实,仅供参考。请谨慎采用,风险自负。


举报收藏 0评论 0
0相关评论
相关最新动态
推荐最新动态
点击排行
{
网站首页  |  关于我们  |  联系方式  |  使用协议  |  隐私政策  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报  |  鄂ICP备2020018471号