【Redis】之 Redisson 分布式锁

一、Redisson 详解


1、Redisson 介绍

Redisson 是一个 Redis 客户端,并且 Redisson 功能强大,所以使用 Redisson 可以很方便实现 Redis 分布式锁。关于分布式锁的更多知识可以参考我的另一篇博客:【Redis】之分布式锁

基于 Redis 实现的分布式锁存在一个锁的续期问题:持有锁的线程在锁过期时间内还没有执行完业务,此时锁超时被自动释放,这样会导致多个线程同时持有锁的问题,所以需要给锁的过期时间进行续期。

而 Redisson 就是能够很好的给我们解决锁的续期问题,同时 Redisson 还给我们实现了各种各样的锁,比如红锁(RedLock)、 可重入锁(Reentrant Lock)、公平锁(Fair Lock)、读写锁(ReadWriteLock)、 信号量(Semaphore)。

更多关于 Redisson 介绍可以参考官方文档:Redisson的分布式锁和同步器

2、Redisson 原理

2-1、看门狗机制

Redisson 实现锁的续期功能使用的是看门狗机制,具体原理是:Redisson 在获取锁之后,会维护一个看门狗线程,当锁即将过期还没有释放时,不断的延长锁 key 的生存时间:
【Redis】之 Redisson 分布式锁_第1张图片
2-2、加锁机制:

  • 线程去获取锁,获取成功:执行lua脚本,保存数据到redis数据库;
  • 线程去获取锁,获取失败:一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。

2-3、watchdog 自动延期机制:

redisson在获取锁之后,会维护一个看门狗线程,在每一个锁设置的过期时间的1/3处,如果线程还没执行完任务,则不断延长锁的有效期。看门狗的检查锁超时时间默认是30秒,可以通过 lockWactchdogTimeout 参数来改变。

加锁的时间默认是30秒,如果加锁的业务没有执行完,那么每隔 30 ÷ 3 = 10秒,就会进行一次续期,把锁重置成30秒,保证解锁前锁不会自动失效。

那万一业务的机器宕机了呢?如果宕机了,那看门狗线程就执行不了了,就续不了期,那自然30秒之后锁就解开了呗。

需要注意:

  • 看门狗启动后,对整体性能也会有一定影响,默认情况下看门狗线程是不启动的;
  • 如果使用 Redisson 进行加锁的同时设置了锁的过期时间,也会导致看门狗机制失效;

如何理解锁的失效时间和锁的过期时间?

  • 过期时间:这是在 Redis 设置的时间,时间到了之后会由 Redis 自身删除;
  • 失效时间:在使用 Redisson 加锁时,会设置一个失效时间,这个时间是给 Redisson 在时间到了之后主动解锁用的。

所以我们如果要使用 Redisson 的自动续期功能的话,就不能设置锁的过期时间,只需要设置失效时间即可,锁的解除就交给 Redisson 执行。

2-4、Redisson 分布式锁的关键点总结:

  • 对 key 不设置过期时间,由 Redisson 在加锁成功后给维护一个 watchdog 看门狗,watchdog负责定时监听并处理,在锁没有被释放且快要过期的时候自动对锁进行续期,保证解锁前锁不会自动失效;
  • 通过Lua脚本实现了加锁和解锁的原子操作;
  • 通过记录获取锁的客户端id,每次加锁时判断是否是当前客户端已经获得锁,实现了可重入锁。


二、Redisson 实战


1、项目整合 Redisson

1-1、引入依赖


<dependency>
    <groupId>org.redissongroupId>
    <artifactId>redissonartifactId>
    <version>3.12.0version>
dependency>

1-2、配置 Redisson 客户端对象

@Configuration
public class MyRedissonConfig {

    /**
     * 所有对Redisson的使用都是通过RedissonClient
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws IOException {
        // 1、创建配置
        Config config = new Config();
        // Redis url should start with redis:// or rediss://
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");f

        // 2、根据Config创建出RedissonClient实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }

}

配置好后就可以通过依赖注入方式创建 Redisson 客户端对象:

@Autowired
private RedissonClient redissonClient;

2、Redisson 加锁

2-1、普通加锁

@ResponseBody
    @GetMapping(value = "/hello")
    public String hello() {

        // 1、获取一把锁,只要锁的名字一样,就是同一把锁
        RLock myLock = redisson.getLock("my-lock");

        // 2、加锁,阻塞式等待,默认加的锁都是30s
        myLock.lock(); 
        try {
            System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
            try { TimeUnit.SECONDS.sleep(20); } 
            catch (InterruptedException e) { e.printStackTrace(); }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            // 3、解锁  假设解锁代码没有运行,Redisson会不会出现死锁
            System.out.println("释放锁..." + Thread.currentThread().getId());
            myLock.unlock();
        }

        return "hello";
    }

设置锁的到期时间:

// 设置10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
myLock.lock(10,TimeUnit.SECONDS); 

问题:在锁时间到了以后,如何自动续期

  • 1)、如果我们传递了锁的超时时间,就会发送给 redis 执行脚本,进行占锁,默认超时就是我们制定的时间;
  • 2)、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】,只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒:internalLockLeaseTime =【看门狗时间】 / 3 = 10s

2-2、读写锁

// 测试写锁
public String writeValue() {
    String s = "";
    RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock"); // 构造读写锁
    RLock rLock = readWriteLock.writeLock(); // 获取写锁
    try {
        //1、改数据加写锁,读数据加读锁
        rLock.lock();
        s = UUID.randomUUID().toString();
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        ops.set("writeValue",s);
        TimeUnit.SECONDS.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }

    return s;
}

// 测试读锁
public String readValue() {
    String s = "";
    RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
    
    RLock rLock = readWriteLock.readLock();
    try {
        // 加读锁
        rLock.lock();
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        s = ops.get("writeValue");
        try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }

    return s;
}

读写锁保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁,写锁没释放读锁必须等待:

  • 读锁 + 读操作:相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁,他们都会同时加锁成功;
  • 写锁 + 读操作:必须等待写锁释放;
  • 写锁 + 写操作:阻塞方式;
  • 读锁 + 写操作:有读锁,写也要等待。

总结就是:加读锁,可以读不能写;加写锁,既不能读也不能写。

2-3、信号量

/**
  * 信号量也可以做分布式限流
  */
public String park() throws InterruptedException {

    // 构建信号量
    RSemaphore park = redisson.getSemaphore("park");
    
    // 获取一个信号、获取一个值,redis 中的 park 值减一
    park.acquire();   				 // 阻塞  
    boolean flag = park.tryAcquire(); // 非阻塞

    if (flag) {
        //执行业务
    } else {
        return "error";
    }

    return "ok=>" + flag;
}

public String go() {
    RSemaphore park = redisson.getSemaphore("park");
    park.release();     // 释放一个信号量,redis 中的 park 值加一
    return "ok";
}

2-4、闭锁

/**
  * 放假、锁门
  * 1班没人了
  * 5个班,全部走完,我们才可以锁大门
  * 分布式闭锁
  */
public String lockDoor() throws InterruptedException {

    RCountDownLatch door = redisson.getCountDownLatch("door");
    door.trySetCount(5);
    door.await();       //等待闭锁完成

    return "放假了...";
}

public String gogogo(@PathVariable("id") Long id) {
    RCountDownLatch door = redisson.getCountDownLatch("door");
    door.countDown();       //计数-1

    return id + "班的人都走了...";
}

更多关于锁的资料可以参考官方文档:分布式锁和同步器。

你可能感兴趣的:(Redis,分布式,redis)