一直在思考,记录一点一滴
分类: 架构设计与优化
2018-06-21 10:24:05
账务交易主要是指,在资金流发生的时候,需要根据资金的流入和流出情况,对涉及的账户金额进行增加和减少操作,更新资金的时候,同时需要生成相应的账单,以便后续查询和对账等使用。
提及账务交易,大家并不陌生,古代就开始有账务的概念,小到家庭,大到公司,差别在于记账方式不同而已,家庭一般都只记录流水,而大的公司,账务会有多个掌柜管理,同时根据业务会细分账务类型,完善记账流水,实现资金无缝监控。
1) 多样性是资金流的一个典型特征,
1.1) 资金有流向,
1.2) 有金额,
1.3) 有流入对象,
1.4) 可以收回资金,
1.5) 资金错误的时候,可以进行调账或者各种形式补偿。
2) 资金链的关联
2.1) 资金流可以在一个链条上,
2.2) 也可以在不同的链条上,
2.3) 不同链条上的资金流通过账单进行关联
1) 资金流简单模型是线型,
2) 复杂一点是树形,
3) 最复杂的是网状,
用专业术语一句话概括描述,资金流的模型就是一个有向图
1) 资金流处理的时候,需要生成相应的账单,用来记录每个账户中出、入资金额度,用途描述,以及账户剩余资金的详细信息记录
2) 账单严格意义来讲,一旦生成,不能修改,如果发生退款或者金额错误,如何进行处理呢?
2.1) 发生退款,原账单保留不变,生成一个退款交易账单
2.2) 发现金额错误,可以生成一个补偿账单或者调账账单
总之,账单是只读,不做任何修改,这是账单的特征
账务实时交易系统,就是一个万能掌柜,首先能处理各种业务,其次是实时,最重要的是准确、可靠。
如何做到实时和准确,下图有详细信息可供参考。
如上图示,简单总结了账务实时交易系统所处的位置,
所有的业务交易都可以虚拟为订单,订单再经过账务实时交易系统的处理,就可以将金额分配到相应的账户。
上面讲了这么多,看看需要接入的实际业务业务有哪些?业务特征是什么?业务模型是否符合理论分析(有向图)?
在线支持业务 |
订单分类 |
外卖平台单 |
物流订单 |
物流外单 |
|
随意购 |
|
自配送 |
|
百度快递 |
|
骑士打赏 |
|
CPC推广费用管理 |
虚拟订单 |
CPT费用管理 |
|
用户提现、打款 |
|
用户订单 |
用户订单 |
… … |
… … |
如下图示是业务模型的一个汇总,
1) 树形,
2) 包含多个账户,业务众多,角色众多
3) 多层、多级分账,
4) 任何一个层级都可以发生取消分账,或者取消部分或者全部分账
5) 单账户的交易类型丰富(如:骑士账户的申诉、奖励、补账、餐损、惩罚 ...)
6) 如果一个账户存在多种交易类型,可以通过业务或者描述信息来区分每个交易。(将图转化为树,或者将子树连接到整棵资金流树上)
总结:虽然账务交易角色众多,交易类型丰富,但是模型还是很典型的树状模型,当前接入的业务是如下图示模型,或者如下图示模型的子树。
而且新的业务也不局限于如下模型,凡是资金流无论交易层级有多少,都可以被归结为如下模型的处理范围。
针对业务模型,业务操作很简单的可以归纳为如下几个典型操作:
1)入账、
2)出账、
3) 转账、
4) 分账、
5) 取消
另外通过资金的依赖,可以分为无状态依赖和有状态依赖两种:
1) 无状态资金处理-没有任何的状态依赖,即罚款,
2) 有状态资金处理-用户的支付金额,需要为业务进行细分再分给相应的商户和骑士等账户,
1) 金额的增加
2) 金额的扣减
总结,业务和账户操作呈现喇叭型业务模型,或者倒锥形型业务模型
1) 底层操作接口简单
2) 生成数据格式统一
3) 支撑业务多样化
要接入业务众多,因此账户也变得异常的丰富。
如:青岛众包配送收入账户, 实际来讲,就是青岛市(城市)的众包配送(业务)收入账户(主体:财务)
简单的账户构建元素组合,即可完成丰富的账户信息描述。
至于每个账户,因为金额操作的需求,需要设定不同的账户类型,
1) 现金账户,完成现金业务交易;
2) 冻结账户和淘宝的交易资金冻结功能相同,订单下单,资金入冻结账户,交易完成资金从冻结账户转出到现金账户;
3) 监察账户,提现账户...等就不做详细介绍了。
设计的几个原则,
1) 功能、准确、实时是基础,
2) 性能、效率是关键,
3) 健壮和可扩展是平台化所需。
资金操作,用来承接业务的配置,生成资金流转状态的记录,保证资金准确分配。
对内使用:生成交易明细、提供交易记录实时查询
对外输出:为对账、查账、打款等提供数据支持
数据唯一性是数据准确性的保证
数据唯一性是数据关系、数据关联的前提
账户操作的唯一性实现,如下四元组详述
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去重的方式。
绝对父子关系和相对父子关系并存
1) 提升操作效率(有效树在一棵绝对树上)
2) 提供按业务分段存储(查询子树即可)
3) 维护一棵整树(资金流完整统一,资金关系简单化)
4) 子树用来承接不同的业务,即资金流是一棵整树,不同的业务在资金流这棵整树上的一个子树。
资金流是有向图,需要完成图到树的转换处理
$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', // 金额操作方法-分账
// ....
),
),
);
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,即该节点是本业务的入口节点
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' => '',
);
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,即该节点是本业务的入口节点
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' => '',
);
1、方便按业务块:每个业务作为一棵子树,存在于整体资金流中,方便资金按业务区块的简易、高效管理
业务需求:方便每个业务管理本业务的资金
2、方便完整资金流查询:所有业务共用一棵资金流树,整体资金流可以方便查询,且资金关系完整易维护。
财务需求:一棵完整的资金流树,方便财务跟踪整体资金流向
入账、出账、分账、调账和撤销等,实现资金操作接口统一化,
交易类型场景化和统一化
交易类型可定制
丰富交易类型,实现按类型归纳、追踪、查询和统计等
资金操作API的背后,是规范化了的资金分配关系数据
资金分配关系数据,可以确保资金的准确分配
记录资金分配的完整过程
资金分配状态机的载体
同时通过资金分配关系,可以复盘、回放资金交易的完整过程
1、支持取消任意账户分账(单节点取消分账)
2、支持递归取消任意账户下的分账(按业务区块递归取消多节点分账)
3、支持递归取消整个资金流的分账(取消整棵资金分配过程)
支持批量请求:支持单个请求处理,也支持批量请求处理,
统一事务管理:同一个请求中,无论是包含单个请求,还是多个请求,有一个请求失败,整个操作都需要回滚
统一资源管理:其中包括数据库连接、账户锁等资源,操作串行化,数据锁互斥。
统一异常处理:错误抛异常,异常格式统一化...
赘述一下交易回放的实现 : 所有的分账,都有唯一的操作记录,分账数据不复用不重用,这是交易记录可回放的前提,同时也是业务正确性的支撑(需要清除无效的历史数据)。
账户是一个概念,账户可以承载补偿、积分、优惠券等特殊的资金。
上面说了一大堆,如下图示简单总结,小清新一下。
如下是几大热点问题:每个问题的具体处理方案在后面有详述。
简要解析:同步和异步并存,是一个综合方案,可以保证账务处理的实时性,如下方案的优、缺点以及引发的问题,在下图有详述。
热点账户就是在交易过程中,出现频次特别高的账户,交易频次指的是某个时间段的交易频次一直保持在比较高的次数。
如果是数据操作错误重试导致某账户瞬时出现高频操作,则不属于热点账户范畴。
1) 账户每秒有10次以上更新需求
2) 串行化时账户处理延迟高于1秒以上
热点账户类型 |
账户属性 |
实时需求 |
锁需求 |
处理方式 |
性能 |
业务大账户 |
内部账户 |
无实时余额查询 无实时提现 |
无需加锁 |
异步 |
满足 |
大代理商账户
|
对外账户 |
无实时余额查询 无实时提现 |
没有加锁需求 |
异步 |
满足 |
热门商户(推广) |
对外账户 商户账户 |
实时余额查询 实时提现 |
有加锁需求 |
串行化同步 |
亟待提升 |
1) 在异步化背景(账户实时处理的上游,如果已经存在了异步化的处理)下,此时业务所需要的下游的实时性是不可能完全实时的
2) 对于热点账户而言,问题在于一条数据表项的更新频次已经达到了上线,所以解决热点账户的方案可以从解决数据读取的瓶颈出发。
以CPC业务模型为例,用户点击一次商户店铺,从商户账户扣除x元,存入百度外卖推广收入账户(这个账户是平台内部账户)
各业务的资金分账,入消息队列,调用账务交易平台资金操作接口,账务平台按分账需求和分账状态处理账务消息
通过消息队列处理账务交易信息,QPS基本不是瓶颈
通过对业务和订单散列细分,账务交易系统可以有序、快速处理资金,
支持批量请求,从业务层解除数据依赖和状态依赖
最终测试结果:账户余额更新最差延迟时间控制在15秒内。
用户订单,物流订单等账务处理都是非实时需求场景
1、账务交易异常,MQ堵塞
2、业务订单异常监控
3、涉及账户人员入账错误会第一时间联系客服(仅限于少入账 J)
4、业务通过订单数据可以修复账务交易错误
1、交易维度 : 上条记录余额 = 当前发生额 + 当前余额
2、按订单维度 : 该订单的所有账户的发生额总支出=发生而总收入
3、余额快照 : 账户当日的期末余额=期末余额+发生额
4、余额快照 : 当日所有账户的期期末余额=期末余额+发生额
5、常规对账
设计是一个找平衡点的过程,
一条是功能线,(概要如下图)
一条是性能线,(概要如下图)
寻找功能和性能的平衡,如果有任何一点出了问题,就无法支撑业务的需求。
最终的原则,就是在支撑业务需求的前提下,进一步提升性能和可扩展性。
找好平衡点
抽象、统一、数据化
深入、专一、平台化
谢谢!