锁的概念!

锁机制

​ 通过锁机制,能够保证在多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。

​ 所谓的锁,可以理解为内存中的一个整型数,拥有两种状态:空闲状态和上锁状态。加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功。如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。

为什么要使用锁

​ 在多线程情况下完成操作时,由于并不是原子操作,所以在完成操作的过程中可能会被打断,造成数据的一致性。

锁的种类

乐观锁/悲观锁

​ 乐观锁/悲观锁并不是特指某两种类型的锁,而是一种锁的思想。

1、乐观锁

​ 乐观锁总是认为不存在并发问题,每次去取数据的时候,总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

​ 一般会使用“数据版本机制”或“CAS操作”来实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,因为不加锁会带来大量的性能提升。

  • CAS(下面有说明)

  • 数据版本机制

    ​ 实现数据版本一般有两种,第一种是使用版本号,第二种是使用时间戳,以版本号为例,

    ​ 一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

    update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};
    

2、悲观锁

​ 每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步关键字synchronized关键字的实现就是悲观锁。悲观锁适合写操作非常多的场景。

​ 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。

​ 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。

独享锁/共享锁

​ 独享锁是指该锁一次只能被一个线程所持有;共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。

读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

对于Synchronized而言,当然是独享锁。

互斥锁/读写锁

​ 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock;读写锁在Java中的具体实现就是ReadWriteLock。

可重入锁/不可重入锁

1、可重入锁

​ 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。简单来说就是同一个线程可以重复加锁。对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是Re entrant Lock 重新进入锁。对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

public class Test{
     Lock lock = new Lock();
     public void methodA(){
         lock.lock();
         ...........;
         methodB();
         ...........;
         lock.unlock();
     }
     public void methodB(){
         lock.lock(); // 重复获取锁,等待
         ...........;
         lock.unlock();
     }
}

2、可重入锁的实现

​ 每次加锁的时候count值加1,每次释放锁的时候count减1,直到count为0,其他的线程才可以再次获取。

public class RSpitnLock implements Lock {
    private AtomicReference<Thread> currLock = new AtomicReference<>();
    private int count = 0;

    @Override
    public void lock() {
        Thread current = Thread.currentThread();
        if (current == currLock.get()) {
            count++; // 一个线程多次获取锁,count++
            return;
        }
        while (!currLock.compareAndSet(null, current)) { // 获取锁
        }
    }

    @Override
    public void unlock() {
        Thread current = Thread.currentThread();
        if (current == currLock.get()) {
            if (count != 0) {
                count--;
            } else {
                currLock.compareAndSet(current, null); // // 当年线程count=0才能释放锁
            }
        }
    }
}

AtomicReference锁案例

      // 1.创建一个锁对象
        AtomicReference<Thread> currLock = new AtomicReference<>();

        // 2.获取当前线程
        Thread thread = Thread.currentThread();

        // 3.当年线程获取锁
        boolean b = currLock.compareAndSet(null, thread);

        // 4.查看锁别那个线程获取
        Thread thread1 = currLock.get();

        // 5、当前线程和获取锁的线程对比
        System.out.println(Thread.currentThread().getName()+"获取锁:"+b);
//        System.out.println(thread == thread1);

        // 6、启的一个新的线程
        Thread update = new Thread(() -> {
            // 在新的线程中获取锁
            System.out.println(Thread.currentThread().getName()+":获取锁之前,"+currLock.get().getName());
            while (!currLock.compareAndSet(null, Thread.currentThread())) {
            }
            System.out.println(Thread.currentThread().getName()+":获取锁之后,"+currLock.get().getName());
        }, "线程2");
        update.start();

        // 7、休眠3s后main线程释放锁
        Thread.sleep(10000);
        System.out.println(Thread.currentThread().getName()+"修改3s结束开始释放锁。");
        currLock.compareAndSet(thread, null); // 当前线程释放锁

2、不可重入锁

​ 一个线程中多次获取锁,导致死锁。

public static void main(String[] args) {
    Lock lock = new Lock();
    lock.lock(); // 第一次获取锁成功
    // ....
    lock.lock(); // 同一个线程再次获取锁失败
}

class Lock {
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException {
        while (isLocked) {
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock() {
        isLocked = false;
        notify();
    }
}
公平锁/非公平锁

​ 公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

​ 对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。

分段锁/自旋锁

1、分段锁

​ 分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

2、自旋锁

​ 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

CAS/AQS

1、AQS

​ AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。

​ AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

​ CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。

​ AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

请添加图片描述

2、CAS

​ CAS(Compare And Swap),即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。CAS也是一种乐观锁的实现。

​ 举个例子:原本表中的数据name的值为toString, 线程将name的读出来,修改为name=Java,在写入的时候先判断name的值是否和修改之前的值(toString)一致,如果一致就提交,否则就重新读取内容再修改,所以CAS操作长时间不成功的话,会一直自旋,相当于死循环了,CPU的压力会很大。

CAS存在一个ABA的问题

​ 线程1读取到内容A,线程也读取到内容A后把A修改为B写入进去,此时线程3读取到最新内容是B,然后把B改为A写入进去,最后线程A在写入的时候发现是A,所以依然可以写入成功。虽然写入成功但是线程A不知道这个值已经被其他线程修改过了,所以这就是典型的ABA的问题。

请添加图片描述

携带版本号可以有效的防止CAS中ABA的问题

synchronized

概念

​ synchronized是Java中的关键字,是一种同步锁。用来保证被它修饰的方法或者代码块在任意时刻只能有一个线程调用。

应用场景

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

    public class Demo01 {
    
        private static Object object = new Object();
    
        public static void main(String[] args) {
            // 注意,这里是锁的是对象不是代码块,面试题
            synchronized (object) { // // 锁对象
                // ....
            }
            synchronized (Object.class) { // 锁类
            }
        }
    
        public synchronized static void test1() {
            //synchronized(Demo01.class)
        }
    
        public synchronized void test2() {
            // synchronized(this)
        }
    }
    

四种使用场景效果对比

public class ThreadDemo {
    public static void main(String[] args) {

        // 创建两个对象
        User user1 = new User("张三");
        User user2 = new User("李四");

        // 创建两个任务对象
        MyThread myThread1 = new MyThread(user1);
        MyThread myThread2 = new MyThread(user2);

        // 启动20个线程
        for (int i = 0; i < 10; i++) {
            new Thread(myThread1).start();
            new Thread(myThread2).start();
        }
    }
}

class User {
    public String name;
    public User(String name) {
        this.name = name;
    }

//    public synchronized void add() throws InterruptedException {
//    public static synchronized void add() throws InterruptedException {
    public void add() throws InterruptedException {
        synchronized (User.class) { // this和User.class的区别?
            Thread.sleep(1000);
            System.out.println("name:" + name + ",threadNmae:" + Thread.currentThread().getName() + "---> add");
            Thread.sleep(1000);
        }
    }
}

class MyThread implements Runnable {

    private User user;
    public MyThread(User user) {
        this.user = user;
    }

    @Override
    public void run() {
        try {
            user.add();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

内存图

请添加图片描述

synchronized的特点

1、原子性:原子性的操作执行到一半,并不会因为CPU线程调度而被打断。

2、可见性:释放锁所有的数据都会写回内存,获取锁都会从内存中读取最新数据。

3、可重入锁:它是一个可重入锁

4、重量级锁:

​ 他是一个重量级锁,开销很大,开发过程中尽量少用。
​ 底层是通过一个监视器对象(monitor)完成,wait () , notify ()等方法也依赖于monitor,对象监视器锁(monitor)的本质依赖于底层操作系统的互斥锁(MutexLock)实现,而操作系统实现线程切换需从用户态转换到内核态,上述切换过程较长,所以synchronized效率低&重量级。

5、自动加锁和自动释放锁

Lock/ReentrantLock对比

ReentrantLock加锁演示

       // 1.获取锁对象
        Lock lock = new ReentrantLock();

        // 2.加锁
        lock.lock(); // 获取不到锁会一直阻塞
        try {
            // 3.处理业务 .....
        } finally {
            // 5.释放锁
            lock.unlock();
        }

1、使用上的区别:
1)Lock是一个接口,synchronized是Java中的关键字,synchronized是内置的语言实现;

​ 2)synchronized发生异常时,会自动释放线程占用的锁,故不会发生死锁现象。Lock发生异常,若没有主动释放,极有可能造成死锁,故需要在finally中调用unLock方法释放锁;

​ 3)Lock可以让等待锁的线程响应中断,使用synchronized只会让等待的线程一直等待下去,不能响应中断

​ 4)通过Lock可以知道有没有成功获取到锁,synchronized就只能等待。

​ 5)Lock可以提高多个线程进行读操作的效率

2、在锁概念上的区别:

​ 1)可中断锁:响应中断的锁,Lock是可中断锁(体现在lockInterruptibly()方法),synchronized不是。如果线程A正在执行锁中代码,线程B正在等待获取该锁。时间太长,线程B不想等了,可以让它中断自己。

​ 2)公平锁和非公平锁:synchronized是非公平锁,ReentrantLock默认是非平锁,可以设置为公平锁。

​ 3)读写锁:读写锁将对一个资源(如文件)的访问分为2个锁,一个读锁,一个写锁;读写锁使得多个线程的读操作可以并发进行,不需同步。而写操作就得需要同步,提高了效率
ReadWriteLock就是读写锁,是一个接口,ReentrantReadWriteLock实现了这个接口。可通过readLock()获取读锁,writeLock()获取写锁

3、性能比较:
synchronized是一个重量级锁,性能要低于ReentrantLock。

​ 但是synchronized存在也有它的道理,它是因多线程应运而生,它的存在也大幅度简化了Java多线程的开发。它的优势就是使用简单,你不需要显示去加减锁,相比之下ReentrantLock的使用就繁琐的多了,你加完锁之后还得考虑到各种情况下的锁释放,而synchronized就不用关心这些。

Volatile

概念

​ volatile是Java中的关键字,提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

Java内存模型

​ Java虚拟机规范中定义了一种Java内存 模型(Java Memory Model,即JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。Java内存模型的主要目标就是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节。

​ JMM中规定所有的变量都存储在主内存(Main Memory)中,每条线程都有自己的工作内存(Work Memory),线程的工作内存中保存了该线程所使用的变量的从主内存中拷贝的副本。线程对于变量的读、写都必须在工作内存中进行,而不能直接读、写主内存中的变量。同时,本线程的工作内存的变量也无法被其他线程直接访问,必须通过主内存完成。

请添加图片描述

线程可见性

多个线程共享一个数据,其中一个线程修改了数据,另一个线程维持的还是旧数据。

public class Demo02 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        while (true) {
            if (myThread.getFlag()) {
                System.out.println(Thread.currentThread().getId() + ":flag为true:" + System.currentTimeMillis());
            }
        }
    }
}
class MyThread extends Thread {

    private boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("线程1修改为true");
    }

    public boolean getFlag() {
        return this.flag;
    }
}

请添加图片描述

对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。

​ 解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。

内存可见性解决方案

1、加锁

  while (true) {
            synchronized (myThread) {
                if (myThread.getFlag()) {
                    System.out.println(Thread.currentThread().getId() + ":flag为true:" + System.currentTimeMillis());
                }
            }
        }

实现原理

请添加图片描述

2、volatile

class MyThread extends Thread {
    private volatile boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("线程1修改为true");
    }

    public boolean getFlag() {
        return this.flag;
    }
}

实现原理

请添加图片描述

​ 当对volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存
写操作会导致其他线程中的缓存无效。
​ 这样,其他线程使用缓存时,发现本地工作内存中此变量无效,便从主内存中获取,这样获取到的变量便是最新的值,实现了线程的可见性。

本地锁

​ 本地锁也就是单体架构中的锁,可以使用lock或者synchronized来实现,但是这种锁只能锁住当前服务器的资源,如果在集群的情况下就会失效。

高并发下存在问题

@RestController
@RequestMapping("/native/syn")
public class NativeSynController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private String stockKey = "seckill:%s:stock";
    private String orderKey = "seckill:%s:order";

    @PostConstruct
    public void init() {
        redisTemplate.opsForValue().set(String.format(stockKey, 10), "10");
    }

    @RequestMapping("/kill")
    public String kill(Integer id) throws Exception { // 使用同步代码来解决,性能一下就变慢了

        // 1.查询秒杀商品的数量,根据商品id查询商品的数量
        Integer stock = Integer.parseInt(redisTemplate.opsForValue().get(String.format(stockKey, id)));

        // 2、判断库存是否充足
        if (stock >= 1) {
            // 库存-1,
            redisTemplate.opsForValue().decrement(String.format(stockKey, id));

            // 订单+1,
            redisTemplate.opsForValue().increment(String.format(orderKey, id));

            return "商品抢购成功,库存【" + redisTemplate.opsForValue().get(String.format(stockKey, id)) + "】,订单【" + redisTemplate.opsForValue().get(String.format(orderKey, id)) + "】";
        }
        return "商品抢购太火爆了,已经抢购完毕。。。";
    }
}

​ 如果使用浏览器来测试不会出现问题,但是用Jemeter压测这就会发现订单和库存的数据对不上,这就是同一个接口在高并发的情况下会出现问题。

出现问题的原理

​ 假设库存就剩最后一个,此时有10个线程同时去redis查看库存数量,10个线程拿到的都是10,所以10个线程就去减库存和加库存造成了数据不一致的问题,解决这种高并发的问题就要使用锁。

使用同步关键字解决并发问题

1、使用同步方法

   @RequestMapping("/kill")
    public synchronized String kill(Integer id) throws Exception { 

        // 1.查询秒杀商品的数量,根据商品id查询商品的数量
        Integer stock = Integer.parseInt(redisTemplate.opsForValue().get(String.format(stockKey, id)));

        // 2、判断库存是否充足
        if (stock >= 1) {
            // 库存-1,
            redisTemplate.opsForValue().decrement(String.format(stockKey, id));

            // 订单+1,
            redisTemplate.opsForValue().increment(String.format(orderKey, id));

            return "商品抢购成功,库存【" + redisTemplate.opsForValue().get(String.format(stockKey, id)) + "】,订单【" + redisTemplate.opsForValue().get(String.format(orderKey, id)) + "】";
        }
        return "商品抢购太火爆了,已经抢购完毕。。。";
    }

请添加图片描述
测试了1w个请求。

2、同步关键字

 @RequestMapping("/kill")
    public String kill(Integer id) throws Exception { // 使用同步代码来解决,性能一下就变慢了
        synchronized (this) {
            // 1.查询秒杀商品的数量,根据商品id查询商品的数量
            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get(String.format(stockKey, id)));
            // 2、判断库存是否充足
            if (stock >= 1) {
                // 库存-1,
                redisTemplate.opsForValue().decrement(String.format(stockKey, id));
                // 订单+1,
                redisTemplate.opsForValue().increment(String.format(orderKey, id));
                return "商品抢购成功,库存【" + redisTemplate.opsForValue().get(String.format(stockKey, id)) + "】,订单【" + redisTemplate.opsForValue().get(String.format(orderKey, id)) + "】";
            }
            return "商品抢购太火爆了,已经抢购完毕。。。";
        }
    }

请添加图片描述

使用lock锁解决并发问题

	// 创建锁对象
	private ReentrantLock reentrantLock = new ReentrantLock();

    @RequestMapping("/kill")
    public String kill(Integer id) throws Exception {
        // 1.加锁
        reentrantLock.lock();
        try {
            // 1.查询秒杀商品的数量,根据商品id查询商品的数量
            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get(String.format(stockKey, id)));
            // 2、判断库存是否充足
            if (stock >= 1) {
                // 库存-1,
                redisTemplate.opsForValue().decrement(String.format(stockKey, id));
                // 订单+1,
                redisTemplate.opsForValue().increment(String.format(orderKey, id));
                return "商品抢购成功,库存【" + redisTemplate.opsForValue().get(String.format(stockKey, id)) + "】,订单【" + redisTemplate.opsForValue().get(String.format(orderKey, id)) + "】";
            }
        } finally {
            reentrantLock.unlock(); // 释放锁
        }
        return "商品抢购太火爆了,已经抢购完毕。。。";
    }

请添加图片描述

思考:synchronized是如何释放锁的?

服务集群后本地锁的表现

​ 项目始终是同一个,在idea中只需要建一个新的配置文件就可以,项目依然使之前的。

1、搭建集群

请添加图片描述

请添加图片描述

请添加图片描述

2、搭建Nginx做反向代理

docker-compse.yml

version: '3.1'
services:
  nginx:
    restart: always
    image: daocloud.io/library/nginx:latest
    container_name: nginx
    ports:
      - 80:80
    volumes:
      - /opt/docker_nginx/conf.d/:/etc/nginx/conf.d

nginx配置

upstream seckill-cluster {
    server 10.20.154.24:8080;
    server 10.20.154.24:8083;
}
 server {
        listen       80;
        server_name  localhost;

        location / {
           proxy_pass http://seckill-cluster/;
        }
  }

3、并发测试

请添加图片描述

​ 此时在集群的情况下又出现了并发的问题。这时不难发现本地锁在分布式的情况下是不起作用的,因为每个服务都要一把锁,这个锁只能锁住当前服务器,锁不住其他的服务器。

请添加图片描述

SETNX实现分布式锁

分布式锁的出现就是为了解决本地的锁在集群情况下失效的问题。

请添加图片描述

    private String lockKey = "keill-goods-lock"; // 锁

    @RequestMapping("/kill")
    public String kill(Integer id) throws Exception { // 使用同步代码来解决,性能一下就变慢了
        // 1.加锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
        try {
            if (lock) {
                // 1.查询秒杀商品的数量,根据商品id查询商品的数量
                Integer stock = Integer.parseInt(redisTemplate.opsForValue().get(String.format(stockKey, id)));

                // 2、判断库存是否充足
                if (stock >= 1) {
                    // 库存-1,
                    redisTemplate.opsForValue().decrement(String.format(stockKey, id));

                    // 订单+1,
                    redisTemplate.opsForValue().increment(String.format(orderKey, id));

                    return "商品抢购成功,库存【" + redisTemplate.opsForValue().get(String.format(stockKey, id)) + "】,订单【" + redisTemplate.opsForValue().get(String.format(orderKey, id)) + "】";
                }
            } else { // 没有拿到锁
                Thread.sleep(1000);
                kill(id); // 自选
            }
        } finally {
            // 释放锁
            redisTemplate.delete(lockKey);
        }
        return "商品抢购太火爆了,已经抢购完毕。。。";
    }

​ 上面使用了setnx来实现了分布式锁,但是发现1w个请求处理的时间边长了。这是因为每个线程进来后都要去获取锁,获取不到就一直自旋,指导获取到锁才能讲结构响应出来,所以性能降低了。下面是压测结果,1w个请求需要48s,吞吐量是44。

请添加图片描述

使用双重锁机制提高吞吐量

 private String lockKey = "keill-goods-lock";

    @RequestMapping("/kill")
    public String kill(Integer id) throws Exception {
        // 如果库存不充足直接返回
        Integer stock2 = Integer.parseInt(redisTemplate.opsForValue().get(String.format(stockKey, id)));
        if (stock2 >= 1) {
            // 1.加锁
            Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
            try {
                if (lock) {
                    // 1.查询秒杀商品的数量,根据商品id查询商品的数量
                    Integer stock = Integer.parseInt(redisTemplate.opsForValue().get(String.format(stockKey, id)));

                    // 2、判断库存是否充足
                    if (stock >= 1) {
                        // 库存-1,
                        redisTemplate.opsForValue().decrement(String.format(stockKey, id));

                        // 订单+1,
                        redisTemplate.opsForValue().increment(String.format(orderKey, id));

                        return "商品抢购成功,库存【" + redisTemplate.opsForValue().get(String.format(stockKey, id)) + "】,订单【" + redisTemplate.opsForValue().get(String.format(orderKey, id)) + "】";
                    }
                } else { // 没有拿到锁
                    Thread.sleep(1000);
                    kill(id); // 自选
                }
            } finally {
                // 释放锁
                redisTemplate.delete(lockKey);
            }
        }
        return "商品抢购太火爆了,已经抢购完毕。。。";
    }

请添加图片描述
程序中设置双重锁机制后同样都是1w个请求,测试的数据是4s,吞吐量是1954。

使用Lua脚本释放锁

​ 分布式锁最终要保证加锁和设置过期时间是原子性,查询,对比,删除这三个操作必须保持原子性。

// 1.加锁
try{
    // 2.处理业务
}finally{
    // 释放锁的时候使用lua脚本来做,因为lua脚本是一个原子性的操作
    String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
        "then\n" +
        "    return redis.call(\"del\",KEYS[1])\n" +
        "else\n" +
        "    return 0\n" +
        "end";
    DefaultRedisScript<Long> longDefaultRedisScript = new DefaultRedisScript<>(script,Long.class);
    Long execute = redisTemplate.execute(longDefaultRedisScript, Arrays.asList(lockKey), myValue);
    if (execute > 0) {
        System.out.println("释放锁成功了。。。");
    }
}

SETNX实现分布式锁存在一下几个问题

​ 1、获取到锁的线程突然宕机了,导致锁没有释放。

​ 2、锁超时后自动释放了但是任务还没有执行完。

​ 3、A线程释放了B线程的锁

Redisson介绍

https://github.com/redisson/redisson/wiki/1.-%E6%A6%82%E8%BF%B0

​ 基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。

​ 因为Rlock是实现了JUC下面的LOCK接口,所以JUC中的锁怎么用,Rlock中的锁就怎么用。区别在于JUC下面的锁只能锁本地,而Rlock中的锁可以锁分布式。

请添加图片描述

环境搭建

1、pom

  		<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.14.1</version>
        </dependency>

2、RedissonConfig

@Configuration
public class RedissonConfig {
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://192.168.147.102:6379") // redis地址
                .setPassword("root"); // redis密码
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

lock

   @Autowired
    private RedissonClient redissonClient;

    @RequestMapping("/hello")
    public String hello() {

        // 1.获取一把锁
        RLock lock = redissonClient.getLock("redis-loc");
        System.out.println("获取锁成功:" + Thread.currentThread().getId());

        // 2.加锁
        lock.lock(); // 阻塞方法,获取不到锁会一直等待。
        System.out.println("加锁成功:" + Thread.currentThread().getId());

        // 3.执行业务代码
        try {
            System.out.println("执行业务代码:" + Thread.currentThread().getId());
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally { // 不管业务代码是否出现异常都要释放锁,所以释放锁的需要放在finally中
            // 4.释放锁
            lock.unlock();
            System.out.println("释放锁成功代码:" + Thread.currentThread().getId());
        }
        return "hello:"+System.currentTimeMillis();
    }

2、两个lock方法区别

/**
1、加锁,默认的超时时间是看门狗的时间(30s)
2、自动给锁续期(原理是只要占锁成功会启动一个定时任务给锁重新设置超时时间,默认续期的超时时
间是看门狗的时间,定时任务会在三分之一的看门狗的时间被触发一次)
3、获取不到锁阻塞阻塞
4、锁一旦释放了就不在续期
**/
lock();

/**
1、加锁,用户指定获取锁等待的时间
2、会自动给锁续期
3、等待指定的时间后返回布尔值
// 一般使用这个方法比较多,锁的超时时间就是业务执行的最大时间。超过这个时间说明业务执行出
问题了,应该马上释放锁,因为续期没有意义了。
*/
lock(10, TimeUnit.SECONDS); 

请添加图片描述

TryLock

    /**
     * tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,
     如果获取失败(即锁已被其他线程获取),则返回false .
     */
    boolean tryLock(); 

    /**
     * tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别
     * 在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,
     * 就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
     * @param time 等待时间
     * @param unit 时间单位 小时、分、秒、毫秒等
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;


    /**
     * 这里比上面多一个参数,多添加一个锁的有效时间
     *
     * @param waitTime  等待时间
     * @param leaseTime 锁有效时间
     * @param unit      时间单位 小时、分、秒、毫秒等
     */
    boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

公平锁

​ 基于Redis的Redisson分布式可重入公平锁也是实现了,它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。

RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();

// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
...
fairLock.unlock();

联锁

​ 基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();

RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);

// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

红锁

​ 基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。红锁在大部分节点上加锁成功就算成功。

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);

// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

读写锁

​ 基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

    @RequestMapping("/write")
    public String write() throws InterruptedException {

        System.out.println("进入写");
        // 获取插入redis的业务数据
        String str = System.currentTimeMillis() + "";

        // 获取读写锁
        RReadWriteLock rwlock = redissonClient.getReadWriteLock("anyRWLock");

        RLock rLock = rwlock.writeLock();  // 获取锁
        rLock.lock();         // 加锁

        System.out.println("写锁成功:" + Thread.currentThread().getId());
        try {
            // 把业务数据写入到redis
            stringRedisTemplate.opsForValue().set("key", str);

            // 模拟写入时间
            Thread.sleep(10000);
        } finally {
            rLock.unlock();
            System.out.println("写锁释放:"+Thread.currentThread().getId());
        }
        return str;
    }

    @RequestMapping("/read")
    public String read() {

        System.out.println("进入读");

        // 获取读写锁
        RReadWriteLock rwlock = redissonClient.getReadWriteLock("anyRWLock");

        // 获取读锁
        RLock rLock = rwlock.readLock();
        rLock.lock();     // 加读锁
        System.out.println("读锁成功:" + Thread.currentThread().getId());
        String key = null;
        try {
            key = stringRedisTemplate.opsForValue().get("key");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rLock.unlock(); // 释放读锁
        }
        return key;
    }

模拟两个线程同时执行一下操作

读+读:两个线程同时加锁成功,相当于无锁,在reids中会记录每个线程的状态。

写+写:先获取到写锁的线程执行写操作,获取不到的就阻塞。

读+写:等待读锁释放后,写才能获取到写锁。

写+读:等待写释放后,才能获取读锁。

总结如下:

写锁是一个:排他锁(互斥锁)

读锁是一个共享锁

信号量

​ 基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。

RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();

@RequestMapping("/consumer")
@ResponseBody
public String consumer() throws InterruptedException {
    RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
    //        semaphore.acquire(); // 拿取一个,如果不够就阻塞
    //        semaphore.acquire(2); // 一次拿取2个
    //        boolean b = semaphore.tryAcquire(); // 尝试拿取一个,拿不到就返回

    // 等待2s,拿不到就返回false
    boolean b = semaphore.tryAcquire(2, TimeUnit.SECONDS);
    return "consumer:"+b;
}

@RequestMapping("/production")
@ResponseBody
public String production() {
    RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
    semaphore.release(); // 生产一个
    return "consumer";
}

闭锁

​ 基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。

闭锁可以用来确保特定活动直到其他的活动都完成后才开始发生,比如:

  1. 确保一个计算不会执行,直到它所需要的资源被初始化
  2. 确保一个服务不会开始,直到它依赖的其他服务都已经开始
  3. 等待,直到活动的所有部分都为继续处理做好充分准备
  4. 死锁检测,可以使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();

// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();

@RequestMapping("/init")
public String init() {
    // 在其他线程或其他JVM里
    RCountDownLatch latch = redissonClient.getCountDownLatch("anyCountDownLatch");
    latch.countDown(); // 完成一个任务
    return "init";
}

@RequestMapping("/task")
public String task() throws InterruptedException {
    RCountDownLatch latch = redissonClient.getCountDownLatch("anyCountDownLatch");
    latch.trySetCount(5); // 任务的数量
    latch.await(); // 如果任务没有执行完一直在这里等待
    return "task";
}

redisson释放锁异常

java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: 82baf554-625b-4c19-9559-f37dc85f499e thread-id: 692

出现这个错的原因是没有加锁,却调用了释放锁的方法。所以在释放锁之前先判断当前先是否加了锁。

 finally {
     // 判断当前线程是否加锁了,然后在解锁,否则会报错
     if (redissonClientLock.isLocked() && redissonClientLock.isHeldByCurrentThread()) {
         redissonClientLock.unlock();
     }
 }

为什么要使用redisson来做分布式锁

1、给每个锁都会设置超时时间,默认是30s

2、如果30s之内业务代码没有执行完,会有定时任务自动续期,10s中续期1次,续的的时间还是30s。

3、给每个线程加的锁都是唯一的

4、加锁,续期,释放锁都是用luna脚本实现的

5、它是实现JUC下面的Lock接口,和本地锁是来自于同一个接口,API也是一样的。

6、Redisson中也提供了很多的锁,信号量,读写锁。。。

Redisson操作Redis类型

String类型

    @Test
    public void testString(){
        RBucket<Object> name = redissonClient.getBucket("name");
        name.set("admin");
        Object o = name.get();
        System.out.println(o);
        
        RBucket<Object> age = redissonClient.getBucket("age");
        age.set(22,10,TimeUnit.SECONDS);
        System.out.println("ok");
    }

Hash

    @Test
    public void testHash(){
        RMap<Object, Object> user = redissonClient.getMap("user");
        user.put("sex",1);
        user.put("email","toString@qq.com");
        Set<Map.Entry<Object, Object>> entries = user.entrySet();
        System.out.println(entries.size());
        Object email = user.get("email");
        System.out.println(email);
    }

  • 9
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卢卢在路上

人生苦短,及时行乐

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值