背景
我们需要给所有前台业务提供统一的账户系统,用来支撑所有前台产品线的用户资产管理,统一提供支持大并发万级TPS、亿级流水、数据强一致、风控安全、日切对账、财务核算、审计等能力,在万级TPS下保证绝对的数据准确性和数据溯源能力。
特别注意:资金类系统只有合格和不合格,哪怕数据出现只有0.01分的差错也是不合格的,局部数据不准也就意味着全局数据都不可信。
本文只分享系统的核心模型部分的设计,其他常规类的(如压测验收、系统保护策略-限流、降级、熔断等)设计就不做多介绍,如果对其他方面有兴趣欢迎进一步交流。
业务模型
基本账户管理
根据交易的不同主体,可以分为个人账户、机构账户。账户余额在使用上没有任何限制,很纯粹的账户存储、转账管理,可以满足90%业务场景。
子账户功能
一个用户可以开通多个子账户,根据余额属性不同可以分为基本账户、过期账户,根据币种不同可以分为人民币账户、虚拟币账户,根据业务形态不同可以自定义。(不同账户的特定功能是通过账户上的账户属性来区分实现。)
过期账户管理
该账户中的余额是会随着进账流水到期自动过期。如:在某平台充值元送元,其中元是有过期时间的,但是元是没有时间限制的。这里的元存在你的基本账户中,元存在你的过期账户中。
特别注意:过期账户的每一笔入账流水都会有一个到期时间。系统根据交易流水的到期时间,自动核销用户过期账户中的余额,记为平台的确认收入。
账户组合使用
支持多账户组合使用,根据配置的优先扣减顺序进行扣减余额。比如:在基本账户和过期账户(充值账户)中扣钱一般的顺序是优先扣减过期账户的余额。
应用层设计
根据上述业务模型,账户系统是一个典型的数据密集型系统,业务层的逻辑不复杂。整个系统的设计关键点在于如何平衡大并发TPS和数据一致性。
热点账户
前台直播类业务存在热点账户问题,每到各种活动赛事的时候会存在90%DAU给少数几个头部主播打赏的场景。DB就会有热点行问题,由于行锁关系并发一大肯定大量超时、RT突增、DB活跃线程增加等一系列问题,最终DB会被拖挂。
账户类系统有一个特点,原账户的扣减可以实时处理,目标账户可以异步处理,我们可以将转账动作拆解为两个阶段进行异步化。(可以参考银行转账业务。)
示例:A给B转账元,原账户A的元余额扣减可以同步处理,B账户的增加可以异步处理。这样哪怕10w人给主播打赏,这10w人的账户都是分散的,而主播的余额增加则是异步处理的。
账户转账扣减A账户余额,记录A账户出账流水,记录B账户入账流水,这三个动作可以在一个DBTransaction中处理,可以保证源账户进出帐一致性。目标账户B的入账可以异步处理,为了保证万无一失且满足一定的实时性,需要两步结合,程序里通过MQ走异步入账,同时增加DB的兜底JOB定时扫描入账流水记录为未到账的流水进行入账。
特别注意:我们通过异步化缓解热点行处理,但是如果收款方强烈要求收款必须在一定的时间内完成,我们还是需要进一步处理,后面会讲到。
过期账户
通常过期账户用来管理赠送类账户,这类账户有一定的时效性,用户在使用上也是优先扣减此类账户余额。这类使用需求其实覆盖面不大,真正用户账户余额不使用等着被系统过期的很少,毕竟这是一个很傻的行为。
过期账户的两种核销情况:第一种是用户使用过期账户时的核销。第二种是某个过期流水到了过期时间,系统自动核销记为平台的确认收入。
过期账户核销逻辑:用户充值元到基本账户,平台赠送元到赠送账户。此时,基本账户记录进账流水+元,赠送账户记录进账流水+元并且该笔流水的过期时间为-12-:59:59(过期时间由前台业务方设置)。
系统自动核销:如果用户不在此时间之前用完就会被系统自动划进平台的收入,赠送账户余额扣减-元。
用户使用核销:如果用户在过期时间前陆续在使用赠送账户,比如使用元,那么我们需要核销原本进账的元的那笔流水,减少-元。也就是说,该笔过期流水已经核销掉元,带过期核销元,到期后只要核销元即可,而不是元。
过期账户每次使用均产生待核销负向流水,系统自动核销前必须保证没有任何负向流水记录才可以去扣减赠送账户余额。
考虑到极端情况下,刚好过期JOB在进行自动过期核销,用户又在此时使用过期账户,这点需要注意下。可以简单通过加DB-X锁解决,这个场景其实非常稀少。
数据层设计
在应用层设计的时候,我们通过异步化方式来绕开热点问题。同样我们在设计数据层的时候也要考虑单次操作DB的性能,比如控制事务的大小,事务跨网络的次数等问题。当然还包括金额存储的精度问题,精度问题处理不好也会影响性能。
浮点数问题
如果我们用浮点数近似值来存储金额,那么就一定会有偏差,随着金额越大时间越长偏差就会越大。比较好的方式是通过整型来存储,通过放大金额比例来达到不同的业务场景下对金额比率的要求。
示例:正常的1.12元,存储比率是1=元,那么表里的存储值就是,不同的货币比例都可以自由缩放,永远都可以保持最准确的精度。
分库分表+读写分离
根据业务特点和未来增量规划,将DB分为16个逻辑库,前期使用2个物理库承载。16个逻辑库,按照每次2倍扩容,最大扩容上限是16个物理库。单实例的配置8c32g2tconniops。
示例:按照单次TPS-rt1ms计算,TPS1w需求,每台承载5kTPS,单库的活跃线程大概在8-10个(考虑网络延迟)。最后到达瓶颈的都是iops,因为只要rt足够短,最终压力都会在IO上。
分库按照uid分为16个库,账户表不分表默认16张。每张表按照1kw*16=1.6亿个账户。
特别注意:单表能存储多少要综合考虑,比如查询类型,单次查询的RT,冷热数据占比(innodb_buffer_pool利用率)、是否充分发挥了索引,索引是否达到3星级别,索引片中没有经常变更的字段等。
账户流水表按照日期分表张,流水数据会随着时间推移逐渐变成冷数据,定期归档冷数据。(这里约定了,流水查询只能按照uid+日期查询。如果运营类的需求,要横跨分片key获取,走OLAP方案clickhouse、hive等)
分库分表采用分布式数据库产品DRDS,1个主库集群+2个读库集群(读库做了读负载均衡,可以按需扩容)。
特别注意:读负载均衡器: