【Redis实现秒杀业务③】超卖问题之乐观锁具体实现

版本号法

版本号机制实现的方式常用的也有两种:

  1. 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。
    当我们提交更新的时候,判断数据库表对应记录 的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
    如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修 改,那么,先提交的操作会把数据version更新为2,当A在B之后提交更新时发现数据的version已经被修改了,那么A的更新操作会失败。

  2. 使用时间戳(timestamp)。这种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

在这里插入图片描述

CAS算法

CAS即Compare And Swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。

CAS操作包含三个操作数——内存位置的值(V)、预期原值(A)和新值(B)。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。

通过乐观锁+自旋的方式,解决数据更新的线程安全问题,而且锁粒度比互斥锁低,并发性能好。

循环实现:

public void addCount(String goodsId, Integer count) {
    
    
    while(true) {
    
    
        Goods goods = dao.selectByGoodsId(goodsId);
        if (goods == null) {
    
    
            throw new Execption("数据不存在");
        }
        int count = goods.getCount() + count;
        goods.setCount(count);
        int count = dao.updateCount(goods);
        if (count > 0) {
    
    
            return;
        }   
    }
}

递归实现:

public void addCount(String goodsId, Integer count) {
    
    
     Goods goods = dao.selectByGoodsId(goodsId);
        if (goods == null) {
    
    
            throw new Execption("数据不存在");
        }
        int count = goods.getCount() + count;
        goods.setCount(count);
        int count = dao.updateCount(goods);
         if (count == 0) {
    
    
        addCount(goodsId, count)
    }
}

CAS算法的缺点

【1】循环时间长开销很大:自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。
【2】只能保证一个共享变量的原子操作:只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环 CAS的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用CAS 来操作 ij。从 Java1.5 开始 JDK 提供了 AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。
【3】ABA 问题:因为 CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用 CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A-B-A 就会变成1A-2B-3A。

图解

在这里插入图片描述

代码实现

我们可以通过如下这种方式进行处理,如果数量和自己查到的数量相等,就购买

  // 如果数量和自己查到的数量相等,就购买
  boolean updateBoolean = this.update().setSql("count = count - 1")
          .eq("id", id).eq("count", fruits.getId()).update();
  
  if (!updateBoolean) {
    
    
      return ResponseEntity.status(400).body("水果卖光了,下次再来吧!");
  }

但是这样会让其他本应该可以购买的用户买不了,所以改动如下

        // 如果数量和自己查到的数量相等,就购买
//        boolean updateBoolean = this.update().setSql("count = count - 1")
//                .eq("id", id).eq("count", fruits.getId()).update();
		// 如果剩余数量大于0,就可以购买
        boolean updateBoolean = this.update().setSql("count = count - 1")
                .eq("id", id).gt("count", 0).update();
        if (!updateBoolean) {
    
    
            return ResponseEntity.status(400).body("水果卖光了,下次再来吧!");
        }

猜你喜欢

转载自blog.csdn.net/CSDN_SAVIOR/article/details/125275828