【Redis】库存超卖问题解决

一、问题分析

出现超卖问题一般都是线程安全引起的问题。正常情况下,是先查询库存,如果库存小于等于0就购买失败,接着第二个线程查询库存,如果库存小于等于0就购买失败。时序图如下所示

if (voucher.getStock() < 1) {
    // 库存不足
    return Result.fail("库存不足!");
}
//5,扣减库存
boolean success = seckillVoucherService.update()
    .setSql("stock= stock -1")
    .eq("voucher_id", voucherId).update();
if (!success) {
    //扣减库存
    return Result.fail("库存不足!");
}

但正常的业务单个,当然不是线程在竞争资源的,而线程的执行顺序是由CPU调度的,我们无法干预。如双十一这种情况,有可能上百,甚至上千个用户购买同一个商品,这时就会发生线程安全问题。假设有两个用户购买最后1件商品,分别用线程1和线程2表示。线程1查询库存量还剩1时,准备扣减库存时,线程2过来查询库存,因为线程1还没对数据进行修改,所以线程2查询的库存量也是1,就在这时,线程1对库存进行扣减完毕,库存量已经是0了。由于线程2开始查询的库存量是1,接着线程2也开始对库存进行扣减,就出现了超卖。

 

 二、解决办法

针对线程安全问题,常见的解决方案就是加锁。注意,这里的悲观锁和乐观锁并不是真正的锁,而是一种设计理念。

悲观锁:总是悲观的认为线程安全一定会发生,因此每次操作数据之前都会先获取锁,确保线程穿行执行。因为是串行执行,所以执行效率并没有那么好。

乐观锁:乐观的认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时判断有没有线程对数据进行修改。如果没有修改认为数据是安全的,它才会更新数据。如果数据被修改了,则说明有安全问题,可以重试或异常。

那么如何知道数据有没有被修改呢?每条数据都有一个版本号。每次对数据修改前会查询出版本号,修改后版本号都会+1,如果修改前后的版本号不一致,说明数据被修改了,此时修改数据的操作就会失败。如果修改前后的版本号一致,说明数据没有被修改,那么就可以修改数据。

方式一:版本号法

如下时序图,线程1和线程2查询库存和版本号,由于此时都没有修改数据,所以两个线程查询的版本号都是1,假设线程1先执行库存扣减,那么它是一定会成功的,然后版本号就加1变成2了。接着线程2开始扣减库存,修改数据时发现版本号不是1,那么线程2这个执行语句就不会生效。这就防止了线程安全问题。

 方式二:CAS

CAS只是在方式一的基础上简化了操作,版本号法需要额外记录的每条数据的版本,那我们可以寻找数据中和版本号相似的库存字段,每卖一件商品就会减少库存,在修改之前查询库存量作为条件,如果修改时库存量和查询时的不一致,就修改失败了。

小结

悲观锁

优点:简单方便

缺点:串行执行,性能一般

乐观锁

优点:性能好

缺点:成功率不高 

三、代码实践

将库存扣减的代码稍作调整,这个逻辑是,扣减库存时的库存量和查询的库存量一致的情况才认为线程安全,才会做修改操作。但测试发现,即便没有超卖,也会有很多失败的情况。这就是乐观锁的缺点有关了。如果有100个线程同时竞争这个资源,这100个线程最初查询到的库存量都是100,当第一个线程扣减库存量后变为99,而剩下的线程拿100和99进行比对,显然是不同的,也就是说剩下的99个线程都会修改失败。

 //5,扣减库存
boolean success = seckillVoucherService.update()
                .setSql("stock = stock -1")   // 等价于 set stock = stock - 1
                .eq("voucher_id", voucherId)   // 等价于 where id = ? and stock = ?
                .eq("stock", voucher.getStock)
                .update();

 我们只需要将库存条件设置成大于0即可,因为我只要保证这100个线程只能竞争100个库存即可

 //5,扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock -1")   // 等价于 set stock = stock - 1
                .eq("voucher_id", voucherId)   // 等价于 where id = ? and stock > 0
                .gt("stock", 0)
                .update();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值