1 账户记账理论
“借贷”的理解
两个字意思都有歧义,不要尝试从字面意义去理解,在会计中借贷只是两个记账符号,表示两个相反的方向,不绝对代表数量的增减,所有的账户都是左借右贷,资产类账户增加是借,减少是贷,负债类账户刚好相反,增加是贷,减少是借。
对第三方支付平台而言,也同样存在资产类账户和负债类账户:
资产类账户:在上一篇文章中提到,交易过程中资金会被截断,用户支付时,是先从用户银行账户入账到了支付平台的账户,而没有直接到商户的银行账户,支付平台账户其实就是财付通开在央行的备付金账户(备付金的具体解析可以参考另外一篇文章)。这个是财付通的资产,对央行来说这笔钱是财付通的,所以这个是资产类账户。
负债类账户:当这笔钱入账到了财付通备付金账户后,财付通内部必须要知道这笔钱真正要归属到那个商户(央行备付金只是一个所有资金的总额,不会记录具体某一笔钱归属于谁),这就对应到前面提到的为了保障每个用户的资金清晰明了和可追溯,财付通内部建立了一套账户体系。这种账户对财付通来说是负债,因为这些钱是别人的,最终要转移到其他主体,所以财付通内部账户是负债类账户。
按照属性分类,可以将内部账户分成以下几种:
个人账户,一个人一个账户,这个被叫做是C账户,是现金账户,个人可以完全支配。
商户账户,商户账户因为需要结算等,所以一个商户有两个账户,一个B账户(中介账户,理解为待结算账户),一个C账户(现金账户)。当用户支付时,实际支付到了商户的B账户,这个账户里面的资金商户不能自由支配,待平台结算后(扣除手续费等),资金被转移到C账户,这个账户商户是可以自由支配的,比如可以提现到商户自己的银行卡。
银行账户,第三方支付公司为各个银行设置的账户,这个账户是一个总账账户,记录与银行之间的资金变动,一般不记录余额,而只是记录流水,方便跟各个银行进行对账。
总体的关系如下:
账户余额就是账户中的资金数目,任何余额的变动都需要记录流水,而凭证是用来记录交易过程的信息。
凭证单理论上可以放到业务层,不放在账户核心层,这样账户层就只有余额和流水。三者主要内容包括:
余额
账户余额记录用户的资金数目,当发生交易时,会对余额进行 update 操作,增加或减少资金的数据。一般包括:
账户 ID,这个 ID 有的平台会有编码规则,比如某些位用来区分个人或者商户,区分币种以及校验位等。
币种。
余额。
冻结余额,在途交易,还不能确定是否要扣减余额,就可以通过冻结余额来反应。
账户状态,比如是正常态、支付或者冻结等。
流水
当余额发生变更时,需要记录流水,以此来跟踪余额的变化,一般包括如下内容:
流水 ID。
凭证 ID,就是下面凭证中的 ID。
发生金额。
借贷方向。
对方账户 ID。
凭证
凭证用来记录交易过程中的信息,是用户交易的依据。凭证对应到支付平台内部的各种单类,比如充值单,体现单,交易单等等。一般包括:
凭证 ID。
交易参与方,可能是两方,也可能是多方。
交易金额、交易类型。
交易状态,比如支付中,支付成功,转退款等。
交易渠道。
账户性能问题由以下两方面引入:
复式记账法,一笔交易需要在两个账户中进行记录,在海量支付系统中,两个账户大概率上不在同一台 DB 上,甚至也极有可能不在同一 IDC,这就需要引入分布式事务,分布式事务本质上是进程间的同步调用,性能会大幅下降,同时引入了较高的复杂性,比如需要考虑事务一半成功一半失败时的补偿机制或者回滚机制。所以复式记账法带来的分布式事务导致可靠性以及性能都会受到影响;
热点账户,某些账户的交易十分频繁(比如京东账户,拼多多账户),而每笔交易都会对应到余额字段的 update 操作,更新时需要对账户进行加锁操作,频繁加锁释锁会对 DB 造成极大的性能压力,可能会超过 DB 的能承载的极限。压测显示 MySQL 单条记录 update 最大性能500次/s(after_commit 模式)。
解决这些问题有以下一些方案:
先借后贷。
余额更新简化。
合并入账。
多账户。
通常情况下,复式记账的借贷需要在同一个事务里面完成,但分布式事务引入了性能和可靠性问题,所以可以将一个分布式事务拆分成两个本地事务,即“先借后贷”,一个是“借”的实时事务,加一个“贷”的异步事务。当完成实时事务后就可以对外返回成功,异步事务后续进行,这样简化了事务复杂性,当如也会引入其他的问题,后面会讲到。
举两个例子来说明整个过程,分别是快捷支付和余额支付:
快捷支付:用户银行账户支付给商户。借贷账户分别是财付通备付金账户(资产类账户)以及商户的内部 B 账户(负债类账户),T 型账如下图所示:
可以看到,原则是:
先:增加资产,或减少负债。
后:减少资产,或增加负债。
引入的问题
所有将分布式事务拆分成本地事务后引入的问题都是类似的,在事务理论中的“ACID”中,拆分后牺牲了C(一致性)强一致性,但每个本地事物是一致的,另外也缺乏“I”(isolation)隔离型,缺乏隔离型会导致比如“脏读”的异常,即其他事务读到了尚未完全完成事务的更新,这个也很好理解,比如用户本该加100块,但由于异步导致这100块没有马上加上。这个看起来是个大问题,但具体分析:
业务层面而言,这个是可以接受的,因为实行“先借后贷”,所以商户或者用户的可用额度只会小于实际的可用额度,不会出现短款,整体的风险是可控的。另外,从用户的角度来说,短暂的余额不一致也是可以接受的,因为用户可能根本感知不到。
从**“事务回退”**的角度来看,对支付平台来说,资产增加和负债减少都是可以回退的,因为资产在支付机构账上,即使做错账了,可以进行退款等逆向操作。但如果先减少平台资产,比如先给客户打款了,这个是不可回退的,因为客户可能马上就把钱给用完了。“先借后贷”规避了拆分成本地事物的风险。
虽然风险可控,但还是一定要考虑好程序异常时对业务的回滚、重试以及对账机制,保障整个过程结果的最终一致性。
先借后贷以及合并入账只能解决入账时的热点问题,而出账都是需要实时更新(为避免透支风险)。为了解决出账热点,需要引入多账户体系,即通过新建多个账户,把业务均摊到多个账户上,从而解决热点问题。
多账户不仅可以解决出账热点问题,对入账热点问题也有效,本质上多账户就是把一条 DB 记录的更新均分到了多个 DB 记录上。
按照账户的承载功能不同,多账户方案可以分成两种:
功能分离型多账户,出入帐功能分离,有的子账户承载入帐,有的子账户承载出账,有的承载两者。
功能完整性多账户,所有的分账户都同时承载出账和入帐。
功能分散型多账户
功能分离型有两个出发点,一是业务特性,出入帐性能压力不一样;二是风险和管理上的考虑,一个完整账户比多个完整账户的风险和管理难度要小很多。功能分散型子账户有以下几类:
入账子账户,只承载入帐请求。
出账子账户,只承载出账请求。
完整子账户,可以同时承载出入帐,这个账户也可以认为是一个主账户。
出账和入账的账户个数根据业务特性设置。
入账子账户完成入账操作后,定时将资金向上归集到主账户,之后主账户将资金调拨到出账子账户,供出账操作,某些情形下。根据业务特性,资金调拨有一下一些场景:
一定周期内只有一笔出账,则不需要往下调拨了。直接由主账户完成出金。
针对 2B 业务,特性是低频大额,此时可以将资金绝大部分留在主账户上,由主账户完成大额出金操作。
针对 2C 业务,特性是高频小额,此时可以将资金尽量分散到多个出账子账户,由出账子账户完成出金操作。
功能完整型多账户
每个子账户都会同时承载入账业务和出账业务,子账户之间时并列的关系:
入账时,应该尽量均衡的选择子账户。
出账时,应该选择余额大于出账金额的账户,如果没有,则选择余额最大的子账户,目的是减少资金调拨的次数。
多账户设计要点
需要保存用户和多个账户的逻辑对应关系,这个对应关系可以放在业务层,也可以放在核心账户层。
子账户的部署模式可以是同 DB 分布,同 IDC 多 DB 分布,或者跨 IDC 分布,实现难度依次增加,需要根据性能要求以及容灾要求选择合适的部署方式。
分账户后资金调拨是个难点,需要仔细设计,尽量避免资金的来回调拨。
如果是跨 DB 或者跨 IDC 部署时,资金总额的查询会很麻烦,可以考虑采用 CQRS(命令查询指责分离)的设计模式,设置一个集中化的查询副本。
财付通记账核心的事务处理经历过几个阶段:
本地事务:早期用户量和业务压力都不大,几台 DB 机器就完全能够处理,所以本地事务即能满足要求;
分布式事务:随着业务发展,本地事务已经无法满足需求,所以迭代到了分布式事务处理,采用两阶段提交来实现分布式事务,这个是财付通历史上比较重要的一个里程碑;
柔性事务:微信支付的兴起,对性能以及可靠性的要求都提高了很多。而分布式事务提供的 ACID 保证本质上是多台 DB 的同步调用,是以损害可用性、性能为代价的。因为只有在参与事务的各方都能够正常工作,分布式事务才能够顺利进行,只要有一个工作不正常,整个事务就不能完成。而BASE柔性事务的思想就是将分布式事务分割成多个本地事务,同时通过一定机制串接以达到最终一致性。BASE 理论最早是 eBay 工程师在2008发表的论文中提出的,可以参考《Base: An Acid Alternative》https://queue.acm.org/detail.cfm?id=1394128
目前财付通的记账核心大部分场景是基于柔性事务,少部分是基于分布式事务。事务拆分的依据就是上面提到的“先借后贷”。
下面简单介绍下基本的账务处理流程,以 C2B 支付为例,即个人支付给商户的场景,事务处理如下所示,可以分成三部分,买家账户处理,凭证处理,以及卖家处理:
begin
update 买家余额,余额减少
insert 买家流水
insert 支付凭证交易单
update 卖家余额,余额增加
insert 卖家流水
commit
远程日志:当有一笔支付请求发送到事务管理器后,事务管理器首先通过本地的远程日志代理写远程日志,日记代理将日志发往异地的远程日志服务,远程日志服务将日志记录到日志文件里面持久化,同时会将事务信息记录到黑名单 kv 中。黑名单校验模块将会校核黑名单 kv,对于已经同步到异地备 DB 中的用户将会消除;
买家本地事务:远程日志记录完成后,事务管理器发起第一个事务,调用资源管理器,资源管理器将与账务 DB 交互,减买家余额,记录流水,并且记录交易单;完成第一个事务后,事务管理器将发送异步消息给异步入账服务;
卖家本地事务:异步入账服务收到异步消息后,将发起第二个事务,调用资源管理器,资源管理器与账务 DB 交互,加卖家余额,记录流水。
📢📢Google Pay是怎么设计它的支付钱包系统的?点击下方立刻探索👇