小戴
作者小戴2019-03-29 09:29
软件开发工程师, 某金融企业

银行核心系统之热点账户

字数 6351阅读 5195评论 0赞 6

做银行系统或支付结算系统的同学应该对热点账户问题很熟悉,刚好前段时间有网友提到这个话题,后续也没有安排讨论,今天我们就来聊聊热点账户。

在银行或者第三方支付系统中的账务数据库的处理中,数据从一个账户转出,或者有数据转入一个账户,账户都会收到记账请求,并都有一个记账处理的过程。随着账务处理业务量的增大,同一时刻、对同一账户修改的请求数越多,会产生频繁加锁解锁的操作,进而可能导致交易超时。因此,系统会在存款模块提供活期账户设置成热点账户的机制。推荐阅读,不足之处,还请多多包涵!

1、此文适合人群
银行从业人员,IT架构师,系统分析师、开发工程师。
2、此文解决问题
对与程序员谈需求的银行业务人员来说,有助于切换到程序员的角度换位思考;对银行业软件开发领域的架构师和程序员来说,有助于了解设计思路、积累我们的专业技术。
3、此文分为五大部分
一、何为热点账户
二、为什么做热点账户设计
(1)死锁
(2)热点字段
(3)热点事件(双11、发红包)
三、关于热点账户的设计方案
(1)汇总明细入账
(2)控制并发数
(3)缓冲入账
(4)子账户拆分
(5)内存数据库 + 缓存入账
(6)硬件、CPU内存升级
(7)方案对比
四、详细方案设计
五、方案设计要点说明Q&A

1何为热点账户

热点账户是系统中被高频操作的账户。如,大量并发的发生余额增加、余额减少、余额冻结、余额解冻等操作。相较于普通的账户,热点账户数量不多,但操作频率极高。

2为什么做热点账户设计

我们都知道账务处理属于强事务性,在记账处理时,会先对账户资源加锁,记账处理完成后会自动释放锁,执行结果要么交易成功,要么失败回滚。

对于普通个人账户来说,在线上实时交易中的并发度是有限的;而对于代收付、服务费之类的平台层账户,常常会在瞬间产生多个并发操作,但所有对应的并发线程中只有一个线程能够持有当前账户的资源锁,其他线程必须等待该锁被释放后再逐一进行记账处理,这样该账户将会被频繁加锁释锁,成为热点账户,影响系统性能。

账务相关的性能问题,我们分为3类:

(1)死锁
转账是两个账户之间的事务,假设在并行的情况下。
事务1:A账户给B账户转账。
事务2:同时B账户也在给A账户转账。

这就可能导致事务1和事务2发生相互等待死锁(A等待B,B等待A)。在技术上通常是跟踪日志或查数据库表,定位是哪一条语句被死锁,产生死锁的机器是哪一台、表锁类型是行锁还是页锁...进行初步分析,常用的临时方案有重新发起联机交易或重跑批量作业,后续的处理方法是修改交通规则,开一个单行道或者是调整汽车时刻表,避免相互死锁。

es9zfm43cqt

es9zfm43cqt

(2)热点字段
在并发交易的过程中,对于使用频率很高的字段,我们称为热点字段。比如序号(Sequence)、流水号、余额、发生额等字段。

(3)热点事件(双11、发红包)
导致热点字段的交易我们称为热点事件。

1.淘宝双11
淘宝购物主要分为2个步骤::一是买家给淘宝代理中间账户;二是淘宝代理中间账户再把钱给卖家,如图:

9t45tqs2h8r

9t45tqs2h8r

淘宝用户在淘宝网注册账户后会拥有一个与淘宝账户--对应的支付宝账号。在买家购买商品后,如果买家用支付宝里的余额付款,款项就会从买方的支付宝转到淘宝代理中间账户;如果买家直接通过网上银行付款,账款就从买家的银行账户转到淘宝代理中间账户。属于联机类交易,在双11的时候,大量买家付钱将成为热点事件,而热点事件将进一步导致热点账户的形成(淘宝账户入账)。

而淘宝代理中间账户其实就是一个代收代付中介,买家付款后,淘宝代理中间账户收到货款并暂时保管资金。通常会在系统闲时通过批量作业的方式将货款支付给卖方,由于没有时间紧迫的约束,不算热点事件。

2.微信发红包
微信发红包主要分为3个步骤:一是包红包,用户把钱从银行转给微信;二是发红包,微信内部分发给不同的用户;三是取现,从微信把钱转给用户的银行,如图:

b1tpf1dxzgs

b1tpf1dxzgs

其中发红包是热点事件,当一个红包拆分成多个时先抢先得,类似于普通商品“秒杀”活动。听着很简单,通常都是红包发出后,瞬间大量的请求同时涌入,红包秒光。当用户打开红包时,update锁记录,导致后面的请求排队等待,等前面一个update完成释放行锁后才能处理下一个请求,抢红包的用户越多则排队等候越严重,可能会导致交易超时。而对于取现收红包,一般用户不会频繁取现,故不是热点事件。

接下来我们聊方案,也希望大家提各种建设性意见~

3 关于热点账户的设计方案

(1)汇总明细入账
汇总明细入账使用“批量作业”方式实现。

首先在系统中新建交易明细登记簿,并设置标志位,由于insert开销很小且支持高并发,所以在联机实时交易时登记账务明细,并将标志位默认为“未入账”;其次是设置定时任务,扫描指定时间内的账务明细,将“未入账”的记录锁起来后sum出一个总金额,一笔入账到指定账户;最后是将这批记录的标志位置为“已入账”并释放锁,从而提高处理效率。

fv1w156zkwm

fv1w156zkwm

该方案是解决收单类的入账业务,但不适用于扣款类业务;另外一个缺点是账户余额更新不及时,因为是先记流水后定时汇总刷新余额,如本来是10次更新,合并后变成1次更新。所以交易无法实时入账,入账的频率取决于定时作业,比如每半个小时、每十五分钟。

(2)控制并发数
同时对同一账户修改的请求越多,请求排队也就越多,这个账户的锁等待问题就越严重。所谓并发度控制就是要控制同一时刻对热点账户请求的数量,可以通过控制账务系统对热点账户操作的请求数来实现,即操作的并发数。

rsqz8cpcg8d

rsqz8cpcg8d

该方案的缺点对业务量有影响、用户的体验会变差,并且热点账户出现的时候,支付或者账务处理失败率会增加。这个结果对账务系统来说是不允许的,较大的银行或者是第三方支付公司用的比较少。

(3)缓冲入账
缓冲入账是将实时同步的记账请求进行异步化,以达到记账实时性和系统稳定性之间平衡的记账手段,这就是”削峰填谷“。

例如,账务系统对同一个账户的处理阈值为100笔/s,24小时不间断服务(一天能处理86400000笔)。当业务高峰期来临的时候,热点账务的请求数会达到200笔/s。当账户的交易低于100笔/秒的时候,账务系统几乎还是实时地处理了记账请求,而当交易大于100笔/秒的时候,账务系统先返回结果,把账务处理丢到可靠的处理队列中,等并发量不大的时候慢慢消化,对用户来说感受到的体验还是很快就记账成功了。

1xpcu1o1o4j

1xpcu1o1o4j

该方案需要动态判断流量低峰高峰,维护请求队列,一旦账户的日间交易量暴增,导致日间队列根本来不及消化,整个队列越来越长,那就不存在谷可以填,这时候肯定会带来用户大量的投诉,异步请求中结果不可控。

(4)子账户拆分
子账户拆分是指创建与热点账户对应的多个影子账户,所述影子账户与所述账户的数据结构相同,将所述影子账户设置为隐藏,即对客不可见,并将所述账户的余额分散至各个影子账户。当账务系统接收到账务请求的时候,通过前置进行hash分配(具体的hash函数会有更多方案)选择影子账户进行记账,这样就将原来对一个账户的请求分散到多个影子账户中,分散了账务热点。

ruot06uz9ys

ruot06uz9ys

该方案中对于子账户的扣款进行负载,可以满足对同一账户的高频访问负载到其子账户上,极大满足了并发的需求,子账户的余额可能是不足的,但账户的总余额是够的,这样可能影响账务处理的成功率,并且处理对子账户的扣款和入账来说需要做到金额相对平均比较复杂,对记录账户期初余额期末余额处理涉及到并发,相对复杂。

(5)内存数据库 + 缓存入账
内存数据库 + 缓存入账是先提高单台数据库服务器处理能力(I/O,CPU),由于不需要查询和实时更新数据库,然后每隔自定义的一段时间将缓存中的余额异步更新至数据库中。
9grhieulq5

9grhieulq5

使用redis做数据前置处理,将数据库中的热点账户金额初始同步到redis中,然后将操作记录流水,通过job定时任务刷新流水到业务表。这样将db和缓存分开极大的加大了并发性能,但可能会出现金额混乱的问题。
n33om2ino8s

n33om2ino8s

假设redis初始金额为100:
(a) 当线程1对redis账户金额进行原子减操作时,剩余金额40,并记录流水表等待异步入账;

(b) 当线程2对redis账户金额进行原子减操作时,剩余金额-20,此时金额已经为负,按照业务要求金额不能为负所以必须要做反向操作;

(c) 当线程2还没有对redis余额进行反向操作维护的时候又出现线程3进行充值操作,此时金额又变成-20+100=80,已经出现金额混乱,对业务要求的期初余额期末余额无法准确的满足,所以对redis的金额进行同时冲扣会带来余额的并发问题。

总的来说,建议在【1.汇总明细记账】和【5.内存数据库+缓存入账】的基础上进行改良来满足对我们的业务需求,因为对缓存进行操作和延迟批量流水入账可以极大的满足我们对性能的需求。

(6)硬件、CPU内存升级
ps8x5mwarvb

ps8x5mwarvb

(7)方案对比
1.汇总明细入账
对账户的冲扣操作以流水的形式记录下来,通过定时job来将出入账流水更新到业务表中,对于金额的校验需要通过流水数据和当前可用余额来判定,有并发问题,计算很难准确。适用于入账类业务。

2.控制并发数
对单个账户并发操作进行限流降级控制,使得系统健康的完成入账出账操作,但是在并发很高的情况下还是会杀死很多正常的冲扣功能。

3.缓冲入账
需要动态判断流量低峰高峰,维护请求队列,且异步请求中结果不可控。

4.子账户拆分
通过算法选择的影子账户扣款,影子账户的余额可能是不足的,但账户的总余额是够的,这样可能影响账务处理的成功率。

5.内存数据库 + 缓存入账
对redis的金额进行同时冲扣会带来余额的并发问题。

6.硬件、CPU内存升级
属于备选方案,无法从根本上解决单点账户的并发压力。

4 详细方案设计

方案设计前提:
(1):【对账户的余额的更新】:准确的更新账户余额,不允许出现多扣,少扣等情况。
(2):【对账户操作记录的更新】:准确的记录账户流水表中期初余额,期末余额,操作金额等情况,不允许出现任何的金额错误发生。

前期准备:
(1) :新增延迟入账【流水表】,新增入账,出账数据先入【流水表】,通过定时任务将【流水表】入账和出账数据同步到业务数据表中,并且负责新增入账数据的缓存同步工作。
下面的方案会对此表统一称为【流水表】

2u77344xw3i

2u77344xw3i

(2) :新增【redis】数据结构【SortedSet(有序集合)】 key为【hotspot_account】

下面会对这个数据集合称为【缓存操作记录】
deo02sychva

deo02sychva

其中score为当前账户操作时间【新覆盖旧】,member为出入账的账户ID。key【hotspot_account】,所有账户的入账出账操作需要记录到hotspot_account中,主要是提供给【图1中定时任务】获取所有账户流水ID。

(3)新增【redis】数据结构【SortedSet(有序集合)】 key为【hotspot_account_currentbalance】
下面会对这个数据集合称为【缓存余额】
lduegoh9w6

lduegoh9w6

其中:
score为当前账户可用余额,【热点账户新操作流程之前需要将数据库中热点账户的数据同步到hotspot_account_currentbalance中】
member为账户ID

到此,前期准备工作已经全部结束。

当账户金额充值新增时:
1:记录redis操作记录【hotspot_account】
w0jluutc5h

w0jluutc5h

如图所示红色数据部分,当账户110000056666660010入账时,插入或更新数据,member=110000056666660010,score为当前时间戳(秒)。
ps:操作指令【ZINCRBY key increment member】,当 key 不存在,或 member 不是 key 的成员时, ZINCRBY key increment member 等同于 ZADD key increment member 。

2:新增【流水表】,设置入账状态为未入账
5lihxqa0cyg

5lihxqa0cyg

当账户金额扣减时:
1:同金额充值相同首先记录redis操作记录【hotspot_account】。
2:直接对缓存hotspot_account_currentbalance对应的金额进行扣减。
fngzflp8aew

fngzflp8aew

3:定时任务
定时任务的作用是将流水表的数据更新到【账户表】,和【流水明细表】,并且设置【流水表中】数据已入账,同时要将新入账数据流水到更新【hotspot_account_currentbalance】中的可用账户余额,让扣减操作得以继续进行。以下操作流程:
5h6zbjbjz0l

5h6zbjbjz0l

5 方案设计要点说明Q&A

(1)延迟入账,金额扣减和充值都以流水的形式记录,由定时任务来延迟入账,可能会出现已入账数据,但是定时job还没有入账而引发金额不足的情况,但是会在下次定时job启动之后入账,业务可接受。

(2)缓存账户余额来判定当前余额是否充足,只有缓存余额充足才会记录【流水表】,可以确保金额不会扣成负,反过来也就是说只要是【流水表】中存在扣账流水,那么此时金额—定充足,redis是单进程单线程的,所以redis的zincrtjy扣减操作是线程安全的。

(3)精确入账,所有的入账出账操作都要先经过缓存【hotspot account currentbalance】, 所以当定时任务启动的时候只会入账缓存中记录过的数据,避免循环所有的热点数据,堆 加定时JOB执行效率。

(4)【为什么定时JOB入账后要判断当前缓存中余驳是否大于0】,当定时任务同步【流水表】
和业务数据表之后需要把入账的数据同步到缓存中,并且当【流水表】中有充值流水的话需要把充值金额原子加入到缓存余额,来同步db和缓存余额。

1:当缓存金额大于0的时候,说明目前缓存中金额充足无需同步,此时缓存中余额可能比数据库中的小,不过对业务无影响。

2:当缓存金额小于0的时候,说明目前金额不足了需耍将数据中的余额同步到缓存余额中,但是这个入账过程中可能会有其他成功插入到【流水表】的流水,但是到目前为止,肯定不会有扣减的数据再插入到【流水表】中了,因为金额己经为0,所以这个时候大胆放心的再冲【流水表】看是否有并发的数据,如果存在再更新一下业务表数据,然后把当前account中的可用余额直接set到缓存中的可用余额即可。

(5)设计方案中依靠zincrby的原子扣减操作可以保证缓存中可用余额小于等于DB中实际可用余额。

(6)【定时JOB从流水表中入账业务表数据成功,并且同步缓存中余额后为什么最后一步要删除10秒钟之前的缓存操作日志数据】,这是因为如果10秒钟之内都不存在当前账户操作的记录那么就可以认为在定时任务对当前账户进行操作的过程中没有并发对这个账户的充值和扣款操作,所以可以删除掉缓存中对这个账户ID的操作记录,【可以确定的是我们的入账出账不会超过10秒】,反过来,如果误删除掉缓存中对这个账户的操作记录,也不会对之后的定时JOB有任何影都,因为所有的入账数据都保留在【流水表中】,这个删除10秒钟的操作意在减轻定时JOB的工作量而已。

结束语
热点账户问题由来已久,一直是账户系统设计中的一个难点和瓶颈。但热点账户机制会对系统性能有所损耗,业内的大牛们也一直在寻找一种更为优化的解决方案,其中也不乏很多优秀的解决方案,但随着业务的不断攀升和互联网的高速发展,也就显得捉襟见肘。不过仍有提升空间,例如控制余额不透支的方案等,若有问题请留言,期待与大家探讨。

本文作者:代堂鸣

个人公众号:小代嘚吧嘚

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

6

添加新评论0 条评论

Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广