• 账户余额的批量入账与扣账实现


    1,问题:

    在高并发系统中,存在热点账户现象,即一个账户有大量的入账和扣账请求,在这样的背景下,频繁的更新账户的余额会对数据库造成较大的压力。

    2,解决思路:

    update改为insert。创建待入账流水表和待扣账流水表。批量更新账户余额。

    4,引出的新问题:

    入账好说,扣账需要注意一点,就是在余额系统中,余额的值不能为负。

    5,解决思路:

    在插入待扣账流水之前要进行余额检查。即余额-代扣流水总额>0。

    6,引出的新问题:

    待扣账流水终究要更新到余额的,这时会有并发问题,描述如下:

      定时执行批量更新的线程 插入待扣流水线程  
    T1 按照时间段获取一批待扣流水    
    T2 更新余额完成(开启事务A)    
    T3   查询余额(会获得已经更新的旧值)  
    T4   查询待扣流水总额  
    T5 删除待扣流水(事务A提交)    
    T6   余额检查  

    这会产生实际余额可扣账,但是检查失败的情况。但是,从业务安全的角度来说,余额没有变负,牺牲了用户体验。还会有一种问题:

      定时执行批量更新的线程 待扣账流水线程
    T1 按照时间段获取一批待扣流水(100元)  
    T2   查询余额(110元)
    T3 更新余额(开启事务A)(110元-100元=10元)  
    T4  删除待扣流水(事务A提交)  
    T5    查询待扣流水总额(0元)
    T6    余额检查(110元-0元-本次扣账100元>0,检查通过)

    T4,T5还可能发生一部分删除,一部分未删除时,T5开始查询。都会造成余额实际为负的情况。于是我们采用另外一种方式:

      定时执行批量更新的线程 待扣账流水线程
    T1 按照时间段获取一批待扣流水(100)  
    T2   查询待扣流水总额(100)
    T3 更新余额(开启事务A)(110-100)  
    T4 删除待扣流水(事务A提交)  
    T5   查询余额(10)
        余额检查(10-100-本次扣账<0)(即使查询余额发生在事务A未提交时,那么就会变成110-100-本次扣账。也是安全的)

    待扣账流水线程的业务逻辑修改为先汇总待扣总额,然后查询余额,这样最坏的情况就是如表中所展示的情况。可能会造成扣款失败,但是对于业务来说是安全的,不会出现余额为负。

    7,还有问题:

    更新余额的线程和待扣流水的线程问题看似解决,但是有一个很严重的问题。

      待扣账流水线程A 待扣账流水线程B
    T1 查询待扣流水总额(100) 查询待扣流水总额(100)
    T2 查询余额(110)  
    T3   查询余额(110)
    T4 余额检查(110-100-本次10>=0)通过  
    T5   余额检查(110-100-本次10>=0)通过
    T6 插入待扣流水 插入待扣流水

    好蠢啊,有没有。余额对我们来说是竞争资源,这个在扣账的情况下真的没法并发,不上锁是不行的!前面已经说了,目的就是为了解决update的行锁竞争问题!咋办呢?

    引入一个新的组件吧,用redis。内存计算要比数据库行锁快的多,而且redis是线程安全的。

      待扣账流水线程A 待扣账流水线程B
    T1
    EXISTS 账户ID?
     
    T2  return false
    EXISTS 账户ID?
     
    T3  
    return false
    T4 获取redisLock成功
    获取redisLock失败
    T5 get 余额 from  DB
    50毫秒后重试 
    T6 set 账户ID 余额  
    T7  DECRBY 本次扣账金额>0  EXISTS 账户ID?
    T8    return true
    T9 插入待扣流水 DECRBY 本次扣账金额>0
    T10   插入待扣流水
         

    Redis Decrby 命令将 key 所储存的值减去指定的减量值。

    如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECRBY 操作。

    这里为啥用了redisLock呢?为了达成只有一个线程set redis的值。其他线程等待的效果。当然了,看系统的并发,如果你不害怕缓存击穿现场。直接setnx也是可以的呀。

    现在不要忘记那个批量更新余额的线程。

    插入待扣流水的线程伪代码实现一下:

    这个方法就叫tryGet方法吧。

    boolean exists = exists account_id ?
    
    if(exists){
      BigDecimal balance = get value by account_id from redis;
      if(balance - 本次扣减>0){
        BigDecimal afterDec = DECRBY 本次扣账金额;
        if(afterDec<0){
          //被余额检查扣成了负数,你得把本次扣减的还回去。
          //十分重要,一定要还回去
          INCRESBY 本次扣减金额。
        }else{
          int count = insert;
          if(count<0){
            //还回去
            INCRESBY 本次扣减金额。
          }
        }

      }
       }else{   if(redisLock.tryLock()){
        try{
          BigDecimal balance = query from DB where account_id = ?;
          set Redis account_id balance;
        }finally{
          redisLock.unlock();
        }   }else{
        sleep 50ms;
        tryGet();   } }

     批量扣账的线程只负责把待扣流水更新入余额。

    批量待入流水除了更新余额,还要更新redis的值。

    使用redis会面临严重的问题,你无法保证调用Redis一定成功!!!redis也没有流水号什么的,不会像业务系统有幂等性!!!用来做余额检查有天然的缺陷啊!!!使用补偿也无法解决啊。大神有方案留言。

     

      批量更新余额线程 插入待扣流水线程
    T1 按照时间段获取一批待扣流水
    EXISTS 账户ID?
    T2   return true
    T3 更新余额(开启事务A)  
    T4 删除待扣流水(事务A提交)  
    T5 tryGet方法  
    T6    
    T7    
    T8    
    T9    
    T10    
    T11    
    T12    
    T13    
  • 相关阅读:
    【JavaScript从入门到精通】第二课 初探JavaScript魅力-02
    【JavaScript从入门到精通】第一课 初探JavaScript魅力-01
    程序员技术周刊
    【Geek软技能】程序员,为什么写不好一份简历?
    众里寻他千百度?No!这项技术只需走两步就能“看穿”你!
    PornHub 正式发布 AI自动标注色情演员引擎
    9 月份 GitHub 上最火的 JavaScript 开源项目!
    累了吗?来挑战一下算法趣题,看看自己是哪个段位的程序猿吧!
    Chrome 开发者控制台中,你可能意想不到的功能
    现代软件工程 作业 最后一周总结
  • 原文地址:https://www.cnblogs.com/coolgame/p/10504915.html
Copyright © 2020-2023  润新知