并发场景下如何保证余额正确扣减

下班的时候,微信上有个好友发消息说想咨询一个问题,然后就简单说了下他们的业务场景,用户购买商品时,用户账户余额需要减去对应的商品金额。乍一看,感觉没有什么问题,用正常的流程处理即可,这时他又说了:

当两种商品同时进行促销时,用户可能会抢购这两种商品。会出现一种情况:例如用户账户余额1000,商品A价格300,商品B价格500,先抢购A去支付,由于可能会出现长事务或者网络延时等情况,再抢购B商品去支付时,这时假如A商品余额扣减还没完成此时账户金额还是1000,那么此时针对商品B,账户余额=1000-500即余额500。紧接着商品A支付流程完成账户余额:1000-300=700,这时就会出现余额扣减异常问题。正常情况下,两种商品购买完之后,用户余额只剩下200,但是这种并发情况下,会出现余额扣减异常。他问有没有好的方式处理这种情况。

我当时就简单给他说了一下思路:

如果项目中有用到Redis缓存中间件,那么就可以很容易通过Redis分布式锁来实现;后面会单独写文章介绍Redis分布式锁的实现。

如果项目中没有使用Redis,那么可以通过乐观锁的方式去解决这种问题。
简单实现过程就是:在更新账户余额的时候,增加一个条件,即账户原始金额条件的判断,其实类似version版本号。
update user_account set balance = balance-productMoney where user_id=#{userId}
and balance=1000(原始金额)

通过加入原始金额条件后,对于商品A支付后余额扣减:

update user_account set balance = balance-300 where user_id=#{userId}
and balance=1000

对于商品B支付后余额扣减:

update user_account set balance = balance-500 where user_id=#{userId}
and balance=1000

即使在并发情况下去执行上面两条sql语句,只会有一条执行成功,可以根据update执行结果(影响的行数),如果影响的行数不等于0,说明余额更新成功,如果影响的行数等于0,说明余额已经扣减过,此时需要查询新的余额,然后进行扣减操作即可。

上面加粗条件就类似一个版本号的概念。下面简单介绍一下乐观锁悲观锁的概念:

乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
乐观锁优点:从上面的例子可以看出,乐观锁机制避免了长[事务]中的数据库加锁开销(购买商品 A和 B余额扣减过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。

悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他[事务],以及来自外部系统的[事务处理])修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的[排他性],否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

你可能感兴趣的:(并发场景下如何保证余额正确扣减)