高频热点账户问题解决

金融账户系统的特点是并发量大、响应快、交易金额大,热点账户问题突出。一个合格的账户系统既要解决上述问题,又必须绝对保证资金安全。

记账分录

充值

借方:三方支付待清算账户(+)

贷方:个人余额账户(+)

三方支付的待清算账户是热点账户,频繁的增加余额。

提现

借方:个人余额账户(-)

贷方:三方支付资产账户(-)

三方支付的资产账户是热点账户,频繁的减少余额。

服务费收款

借方:个人账户(-)

贷方:商户服务费账户(+)

商户服务费账户就是热点账户,会频繁增加余额。

服务费付款

借方:商户服务费账户(-)

贷方:个人账户(+)

商户服务费账户就是热点账户,会频繁减少余额。

记账时,所有涉及的账户余额都要做update更新,高并发情况下,当出现上述类型的热点账户时,由于数据库的行级锁,对同一账户的更新余额操作由并行变成串行,单个请求的响应时间变长,从而拖垮整个记账服务。

解决思路

把热点账户按照金额变动方向分为三种账户:

  • 加频账户(余额增加频繁)
  • 减频账户(余额扣减频繁)
  • 双频账户(余额增加扣减均频繁)

加频账户

采用准实时更新余额,先将金额变动插入临时表中,由定时任务按照一定频率汇总发生额,并更新账户余额,而后删除临时记录。

当加频账户减钱余额不足时,主动去汇总发生额。需要考虑主动汇总发生额和定时任务处理的并发情况,在该定时任务执行时设置redis锁,防止并发。主动汇总时会去判断这个redis锁是否存在,如存在证明定时任务正在执行,无需主动汇总,可能是真的余额不足。

主动汇总同样会设置redis锁,定时任务同样会判断。

减频账户

将减频账户拆分多个子账户,减频子账户设置金额报警,如果某个减频子账户余额不足触发报警,会对该子账户做资金归集,将其他子账户余额归集到该子账户(每个子账户设置可归集金额限制)。

如在交易过程中发现该子账户余额不足,转向使用其他子账户记账。由于拆分子账户,余额查询时需要汇总各个子账户余额返回。记录主账户流水需要记账后余额,这里需要异步计算汇总。当减频账户加钱时,需要平均分配入账到不同的子账户。

双频账户

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

记账死锁问题

高并发情况下,当多个账户之前互相转账时,可能会出现死锁问题。

A账户和B账户双方转账请求并发,账户系统对每个转账请求都会更新A、B余额,这两个更新需要在一个事务里,正常流程线程1先更新A,再更新B,线程2先更新B,再更新A,线程1更新完A后会等待B的锁,不提交事务,线程2更新完B后会等待A的锁,不提交事务,这样两个线程互相等待锁,造成死锁。

思路

解决死锁的最好办法就是避免死锁,避免死锁要从产生死锁的条件入手:

  • 互斥:共享资源X和Y只能被一个线程占用
  • 占有且等待:线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X
  • 不可抢占:其他线程不能强行抢占T1占有的资源
  • 循环等待:线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待

四个添加同时满足就会产生死锁,只要能破坏掉有一个条件,死锁就不会产生。共享资源是没有办法破坏,也就是说互斥是没有办法解决。

破坏占用且等待

只需要同时申请资源就可以,同时申请这个操作是一个临界区,需要一个Java类来管理这个临界区,同时申请资源apply()和同时释放资源free()。

通过增加一个 Allocator 账号管理员对象,并且将其设置为单例,每次进行转账的时候,我们都先通过 Allocator 分配账号,如果分配账号成功,则进行转账,如果失败则重新获取,可以设置一个失败次数或是超时时间,达到失败次数或超时时间则转账失败。

破坏不可抢占条件

对于不可抢占,可以获取了部分资源,再进一步获取其他资源时如果获取不到时,把已经获取的资源一起释放掉。

java.util.concurrent中的Lock接口,提供了如下三种设计思想都可以解决死锁的不可抢占条件:

  • 能够响应中断:线程处于阻塞状态时可以接收中断信号。我们便可以给阻塞的线程发送中断信号,唤醒线程,线程便有机会释放它曾经拥有的锁。这样便可破坏不可抢占条件。
  • 支持超时:如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
  • 非阻塞获取锁:如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也可以破坏不可抢占条件。

破坏循环等待条件

对于循环等待,可以将需要获取的锁资源排序,按照顺序获取,这样就不会多个线程交叉获取相同的资源导致死锁,而是在获取相同的资源时就等待,直到它释放。比如根据账号的主键 id 进行排序,从小到大的获取锁,这样就可以避免循环等待。

你可能感兴趣的:(java)