转自 热点账户高并发记账方案

热点账户高并发记账带来的问题?

记账处理过程主要包括两部分,一是记录记账凭证,二是更新账户的余额。为了保证账户不被其他请求影响数据的准确性,在进行记账处理时,会先对账户的资源加锁,记账处理完毕后会自动释放锁。随着账务处理业务量的增大,账务数据库中的账户常常会在瞬间产生多个并发操作,但所有对应的并发线程中只有一个线程能够持有当前账户的资源锁,其他线程必须等待该锁被释放后再逐一进行记账处理,这样该账户将会被频繁加锁释锁,使该账户成为账务数据库热点,产生性能瓶颈点,严重影响账务数据库的性能。
热点账户带来的其实是性能问题,多笔交易需要给同一账户记账时,会产生一笔交易等待前一笔交易记账完成才能接着记账的问题,这样就会产生事务等待问题。
image.png
一般是减少事务时间,从业务层面优化减少不必要的事务操作。

解决方案1: 并发控制

通过控制上游支付交易的请求数据的并发请求数来实现。
image.png

  • 优点 实现简单,代码都不怎么改。
  • 缺点 这个是牺牲用户体验来保障系统性能,支付或者账务处理的失败率会提升,用户体验很差,想想抢票抢不到那种感觉吧,一般不在生产上用。

    解决方案2:汇总明细入账

    实时交易全部 insert 账户明细数据,insert 的开销比 加锁开销少。 定时跑批将一段时间内的账务明细汇总成一条,一笔入账到指定账户。
    image.png

  • 优点

避免了限流问题,交易都可以进来,实现也不复杂。

  • 缺点

交易不能实时入账,账户加钱没啥问题,很实用,但是账户支出,减钱会出现账户透支问题。

解决方案3:缓冲入账

缓存入账,将入账过程异步处理,使用 MQ 消息队列,可以达到削峰填谷的作用,是流量稳定的进来,然后稳定的处理即可。
image.png

缓存记账流程

实时记账 转换为准实时记账
image.png

  • 优点

可以让流量比较平缓,出现交易量暴增时,系统也能够处理。提升系统稳定性和实时性,可以准实时记账。

  • 缺点

随着队列堆积的消息越来越多,记账请求来不及处理。 这种方式也会导致 账户减钱出现账户透支的问题。

解决方案4:子账户拆分

具体来讲,就是将一个热点账户对应多个影子账户, 将账户余额分散到各个影子账户,这样就没有热点账户问题。 每次请求来的时候,通过 hash 选择合适的影子账户记账,这样账户请求形成了分散热点。
image.png
入账流程
image.png

  • 优点

热点分散了,解决了热点账户问题。

  • 缺点

当选择影子账户扣款的时候,可能出现扣款不成功的情况,但是总的影子账户余额是够的,这样就会影响这笔扣款交易。
实际根据实际业务使用场景,按照金额变动方向,分成加频账户(余额增加频繁)、减频账户(余额扣减频繁)、双频账户(余额增加扣减均频繁)。
三种账户的处理方式各不一样:

  • 加频账户处理:准实时更新余额。先将金额变动插入临时表中,由定时任务按照一定频率汇总发生额,并更新账户余额,而后删除临时记录。当加频账户减钱余额不足时,主动去汇总发生额。这里需要考虑主动汇总发生额和定时任务处理的并发情况,在该定时任务执行时设置redis锁,防止并发,主动汇总时会去判断这个redis锁是否存在,如存在证明定时任务正在执行,无需主动汇总,可能是真的余额不足。主动汇总同样会设置redis锁,定时任务同样会判断。

  • 减频账户处理:将减频账户拆分多个子账户,减频子账户设置金额报警,如果某个减频子账户余额不足触发报警,会对该子账户做资金归集,将其他子账户余额归集到该子账户(每个子账户设置可归集金额限制)。如在交易过程中发现该子账户余额不足,转向使用其他子账户记账。由于拆分子账户,余额查询时需要汇总各个子账户余额返回;记录主账户流水需要记账后余额,这里需要异步计算汇总。当减频账户加钱时,需要平均分配入账到不同的子账户。

  • 双频账户处理:将双频账户拆分多个子账户。加钱时,准实时更新余额,先将子账户金额变动插入临时表中,由定时任务按一定频率汇总发生额,将汇总的发生额更新进对应的子账户,并删除金额变动记录;减钱按照之前减频账户的逻辑执行。

解决方案5: Redis+MySQL

选用内存数据库实时处理记账请求,然后异步更新到关系型数据库上。
image.png