【Redis】乐观锁解决超卖问题

修改代码方案一

VoucherOrderServiceImpl 在扣减库存时,改为:

boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1") //set stock = stock -1
            .eq("voucher_id", voucherId).eq("stock", voucher.getStock()).update(); //where id = ?and stock = ? 

以上逻辑的核心含义是:只要我扣减库存时的库存和之前我查询到的库存是一样的,就意味着没有人在中间修改过库存,那么此时就是安全的,但是以上这种方式通过测试发现会有很多失败的情况,失败的原因就跟乐观锁的缺点有关了:在使用乐观锁过程中假设100个线程同时都拿到了100的库存,然后大家一起去进行扣减,但是100个人中只有1个人能扣减成功,其他的人在处理时,他们在扣减时,库存已经被修改过了,所以此时其他线程都会失败。

但是从业务角度来分析,这99个人完全可以成功,那为什么失败了?因为乐观锁太小心了,它觉得只要有人改了就有安全问题,其实没有。这里是产生了并发修改,但是没有业务上的安全问题,因为对于库存来讲,只要大于0。


修改代码方案二

之前的方式要修改前后都保持一致,但是这样我们分析过,成功的概率太低,所以我们的乐观锁需要变一下,改成 stock大于0 即可

boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).gt("stock", 0) //where id = ? and stock > 0
                .update();

测试

重启程序,修改数据库,库存改为100,同时清除订单

image-20240527193223058

最后执行JeMeter的200条线程,异常情况为 50%,并且查看数据库,库存没有超卖,而且订单也恰好是100个

image-20240527195012026

总结

超卖这样的线程安全问题,解决方案有哪些?

1.悲观锁:添加同步锁,让线程串行执行

  • 优点:简单粗暴

  • 缺点:性能一般

2.乐观锁:不加锁,在更新时判断是否有其它线程在修改

  • 优点:性能好

  • 缺点:存在成功率低的问题

有些业务中不是库存,它只能通过数据有没有变化来去判断是否安全,这种情况下想要提高成功率,我们还可以采用分批加锁的方案,即分段锁,即我可以将数据的资源分成几份,例如库存总共是100,我可以将100库存分到十张表中,每张表中库存量是10,然后抢的时候就可以去多张表中分别抢,这样一来成功率相当于提高了十倍。

这种分段锁的方案相当于每次锁定的资源少,不再是将100个资源全锁定了。这种思想在HashMap中其实有用到,它就可以解决成功低的问题。

虽然我们最终选择了乐观锁,但并不是说乐观锁就是完美的了,它毕竟还要去访问数据库,对数据库的压力还是非常大的,因此在真正的秒杀场景下,特别是像淘宝、京东这样的高并发的场景,仅仅使用乐观锁还是不够的,所以我们接下来继续要去对秒杀的这种方案进行优化,进一步的提高它的性能。


知识小扩展

针对cas中的自旋压力过大,我们可以使用Longaddr这个类去解决

Java8 提供的一个对AtomicLong改进后的一个类,LongAdder

大量线程并发更新一个原子性的时候,天然的问题就是自旋,会导致并发性问题,当然这也比我们直接使用syn来的好

所以利用这么一个类,LongAdder来进行优化

如果获取某个值,则会对cell和base的值进行递增,最后返回一个完整的值

1653370271627

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值