文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

高并发扣款,如何保证结果一致性

2024-11-30 02:29

关注

1. 扣款流程是什么样的?

图片

public  void payout(long uid,var payAmount){
   # 查询账户总额
   var  amount= "SELECT amount FROM account WHERE uid=$uid";
   # 计算账户余额
   var balanceAmount = amount-payAmount;
   if(balanceAmount<0) throw 异常
   #更新余额  
   update account set amount=balanceAmount where uid=$uid;   
}

以上流程如果并发量非常低的情况下是没问题的,但是如果在高并发下是很容易出现问题。

2. 在高并发下会出现什么问题?

  1. 订单a和订单 b同一时间都查询到了,账户余额为1000

图片

  1. 订单a扣款200,订单b扣款 100,都满足1000-减去扣款金额大于0

图片

  1. 执行扣款,订单 a修改账户余额为800,订单 b 修改为账户余额为900

图片

此时就出现问题了,如果订单 a 先执行更新,订单 b后执行,那么账户余额最终为900,反之为 800,都不正确,正确余额应该是700,那怎么处理呢?

3. 并发扣款怎么处理?

a. 使用悲观锁

在执行扣款时使用redis、zk或者数据库的for update对账户数据进行行级锁,使执行并发操作串型化操作,这里推荐使用for update操作,因为引用redis、zk还要考虑他们的异常情况,数据库最简单,也是目前的常规做法,本人曾经参与几大银行项目也是这种方式。

  1. 查询余额,在查询语句上加上for update,但是一定要注意where 条件是唯一索引,否则会导致多行数据被锁,同时必须要开始事务,否则for update没效果,使用分布式数据库中间件还要注意,for update可能会路由到读节点上。伪代码:
public  void payout(long uid,var payAmount){
   try{
    begin 事务
      # 查询账户总额
      var amount= "SELECT amount FROM account WHERE uid=$uid for update";
      # 计算账户余额
      var balanceAmount = amount - payAmount;
      if(balanceAmount<0) throw 异常
      #更新余额  
      update account set amount=balanceAmount where uid=$uid;   
    }catch(Exception e){
     rollback 事务;
      抛出异常; 
   }  
  commit 事务     
}

b. 使用乐观锁(CAS)

乐观锁的方式也就是是CAS的方式,适合并发量不高情况,如果并发量高大概率都失败在重试,开销也不比悲观锁小,

注意这也是面试题:CAS 适合在使用场景下使用?

1. 增加版本号方式
  1. 在账户表增加乐观锁版本号
account(uid,amout,version)
  1. 查询余额时,同时查询版本号。
SELECT amount,version FROM account WHERE uid=$uid
  1. 每次更新余额时,必须版本号相等,并且版本号每次要修改。
update account set amount=余额,version=newVersion where uid=$uid and version=$oldVersion
2. 使用原有金额值比对更新

在执行账户余额更新时,where 条件中增加第一次查出来的账户余额,即初始余额,如果在执行更新时,初始余额没变则更新成功,否则肯定是更新了,同时数据库也会返回受影响的行数,来判断是否更新成功,如果没成功就再次重试。

update account set amount=余额  where uid=$uid and amount=$oldAmount

以下是伪代码,遇到失败回滚事务并抛出异常,上层调用方法要考虑捕获异常在进行重试

public void payout(long uid,var payAmount){
     try{
      
      begin 事务
        # 查询账户总额
        var amount= "SELECT amount FROM account WHERE uid=$uid for update";
        # 计算账户余额
        var balanceAmount = amount- payAmount;
        if(balanceAmount<0) throw 异常
        #更新余额  
        int count=update account set amount=$balanceAmount where uid=$uid and amount=$amount;   
        ###注意如果更新成功返回count为1
         if(count<1){
           抛出异常重试;
         }
      }catch(Exception e){
        rollback 事务;
           抛出异常; 
    }
   commit 事务     
  
}

具体到以上示例

订单a 执行

update account set amount=800 where uid=$uid and amount=1000;

订单b 执行

update account set amount=900 where uid=$uid and amount=1000;

以上两笔执行只有一笔能成功,因为amount 变了。

4. 使用乐观锁会不会存在aba 的问题

什么是 aba?

线程 1:获取出数据的初始值是a,如果数据仍是a的时候,修改才能成功

线程 2:将数据修改成b

线程 3:将数据修改成 a

线程 1:执行cas,发现数据还是 a,进行数据修改

上述场景,线程1在修改数据时,虽然还是a,但已经不是初始条件的a了,中间发生了a变b,b又变a,此 a 非彼 a,但是成功修改了,在有些场景下会有问题,这就是 aba

但是以上场景,对账户扣款不会出现问题,因为余额 1000 就是 1000,是相同的,举个例子,

订单a:获取出账户余额为 1000,期望余额是 1000的时候,才能修改成功。

订单b:取了 100,将余额修改成了900。

订单c:存进去了100,将余额修改成了 1000。

订单 a:检查账户余额为1000,进行扣款200,账户余额变成了800。

以上场景账户资金损失吗没有吧,不过为了避免产生误解,推荐还是使用版本号的方式!

5. 总结

以上我们讲了在高并发场景在如何保证结果一致性方式,在并发量高情况下推荐使用悲观锁的方式,如果并发量不高可以考虑使用乐观锁,推荐使用版本号方式。同时乐观锁场景要注意 aba 的问题。

来源:架构成长指南内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯