黑马Redis项目笔记 超卖问题以及乐观锁解决

777 阅读1分钟

超卖问题的分析

在多线程情况下执行这段代码(默认情况下,spring的bean是单例的)

 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("库存不足!");
    }

可能会发生如下问题:

假如多个线程一起执行getStock方法,且此时stock为1的话,都可以通过if判读来进入到扣减库存的代码中。倘若线程不被中断,则多个线程在stock为1时进行了扣减,最终造成了stock小于0的结果。这就是超卖问题。

image.png

超卖问题是典型的多线程安全问题,一般的解决方法就是加锁,有两种解决方案,悲观锁和乐观锁。

悲观锁

认为线程问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。 一般分为synchronized或者lock。 再细分为公平锁,非公平锁,可重入锁等。

乐观锁

本质是判断数据有没有修改过

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

会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如CAS。

乐观锁的典型代表:就是cas,利用cas进行无锁化机制加锁,var5 是操作前读取的内存值,while中的var1+var2 是预估值,如果预估值 == 内存值,则代表中间没有被人修改过,此时就将新值去替换 内存值

其中do while 是为了在操作失败时,再次进行自旋操作,即把之前的逻辑再操作一次。

int var5;
do {
    var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;

版本号法

在每一条数据后面都有一个版本号,在执行修改数据的操作之前先查找版本号,如果修改数据时版本号发生变化则终止修改或者重试。

image.png

CAS法

同理,在修改时与查找时的数据进行对比,否则停止

image.png

代码实现

方案一

在扣减库存时,改为

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个人能扣减成功,其他的人在处理时,他们在扣减时,库存已经被修改过了,所以此时其他线程都会失败

方案二

方案一的成功率太低,所以我们另外改一下,stock>0 即可

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