高并发操作同一条数据,更新丢失数据问题(重复转账,票超卖,订单扣库存问题)

何为更新丢失数据问题:假设数据库中有一条数据,有两个事物A,B,同时对这条数据操作。事物A,B同时读到这条数据,事物A对这条数据进行修改并提交,然后事物B对这条数据修该改但晚于事物A提交。这种情况下事物B就会覆盖掉事物A的更新,事物A的更新就会丢失。这种情况有时会引起比较严重的问题。例如重复转账问题,例如一个账户A有100元余额,要向账户B进行转账100元的操作,通常会分为两步,首先读出账户A的余额,判断是否余额充足,然后进行转账扣款操作。如果同时有两个事物a,b进行这个操作,他们同时读到账户余额余额是100,于是他们都判断余额充足,事物A首先进行转账扣款操作,并提交。这个时候账户B已经得到100元转账,账户A余额是0.然后事物b,也进行转账扣款操作,并提交。这个时候,账户B再次得到100元转账,账户A的余额被修改成0.也就是说账户A最终扣款100,账户B却得到200元。票超卖问题也是类似的。

解决方案:1.在事物读取账户余额的语句中加排他锁,这样两个事物就无法同时读取到相同的余额。这种处理方法就是强制事物串行处理。缺点是并发量不高,处理的慢。

                  2.用乐观锁,给数据加乐观锁。也就是加一个版本号version字段。事物读取数据的同时,读取到版本号,提交的时候把版本号加一,并加上版本号判断。语句如下

                   update  table set Num=0,Version=Version-1 where Version=读到的版本号。这样如果事物A提交成功,版本号加一,事物B后提交时就会修改失败。可以根据修改影响的行数来判断。这样就可以避免更新丢失。可以根据业务进行重试。缺点并发量不能太高。

    以上两种解决方案都是数据库层面的。

下面在以电商抢购,下单与扣库存的业务场景提出如下两种解决方案:

                  3.将数据放到redis 缓存中。用分布式锁,强制同一时间只能有一个事务对数据操作,本质上也是将事务串行化操作。缺点并发量不高。

                  4.利用Redis increment 的原子操作(就是更新与读取是原子性的)和redis的单线程特点(这两个特点就可以保证不同的事务不会读到相同余额的问题),保证库存安全。 事先需要把库存的数量等其他信息保存到Redis,并保证更新库存的时候,更新Redis。 进来的时候 先 get 库存数量是否充足,再执行 increment。以 increment > 0 为准。 检查库存 与 减少库存 不是原子性的。 检查库存的时候技术库存充足也不可下单;否则造成库存不安全,原来类似 方法1. increment 是个原子操作,已这个为准。

redisService.increment(key, -req.getNum().longValue()) >= 0 说明库存充足,可以下单。

redisService.increment(key, -req.getNum().longValue()) < 0 的时候 不能下单,次数库存不足。并且需要 回加刚刚减去的库存数量,否则会导致刚才减扣的数量 一直买不出去。数据库与缓存的库存不一致。

参考文章:https://blog.csdn.net/mifffy_java/article/details/95201752

次方法可以满足 高并抢购等一些方案,真正减扣库存和下单可以异步执行。

订单时效问题,订单取消等 为保证商家利益,同时把商品卖给有需要的人,订单下单成功后,往往会有个有效时间。超过这个时间,订单取消,库存回滚。

订单取消后,可利用MQ 回退库存等。缺点是如果redis 服务停止,将无法下单。

                 

原文地址:https://www.cnblogs.com/gfbppy/p/13757249.html