【SpringBoot】Redission 的使用与介绍

背景:

我想我们用到 Redisson 最多的场景一定是分布式锁,一个基础的分布式锁具有三个特性:

互斥:在分布式高并发的条件下,需要保证,同一时刻只有有一个线程获得锁,这是最基本的一点。

防止死锁:在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来的及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。

可重入:我们知道 ReentrantLock 是可重入锁,那它的特点就是同一个线程可以重复拿到同一个资源的锁。

一、SpringBoot 应用

1、通过 Maven 引入依赖


   org.redisson
   redisson-spring-boot-starter
   3.14.0

2、在 yml 文件中添加配置

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    timeout: 10000
    password:
    lettuce:
      pool:
        #最大连接数据库连接数,设 0 为没有限制
        max-active: 8
        #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
        max-wait: 1000
        #最大等待连接中的数量,设 0 为没有限制
        max-idle: 500
        #最小等待连接中的数量,设 0 为没有限制
        min-idle: 300
    jedis:
      pool:
        max-active: 8
        max-wait: 1000
        max-idle: 500
        min-idle: 300

3、自定义配置类

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private String port;

    /**
     * 对 Redisson 的使用都是通过 RedissonClient 对象
     * @return
     */   
    @Bean(name = "redissonClient", destroyMethod = "shutdown") // 服务停止后调用 shutdown 方法
    public RedissonClient redissonClient() {
        // 1、创建配置
        Config config = new Config();

        // 2、集群模式
        // config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");
        // 根据 Config 创建出 RedissonClient 示例
        config.useSingleServer()
                .setPassword(StringUtils.isEmpty(password) ? null : password)
                .setAddress(host.contains("://") ? "" : "redis://" + host + ":" + port);
        return Redisson.create(config);
    }

}

4、测试配置类

    @Autowired
    private RedissonClient redissonClient;

    @Test
    public void test() {
        System.out.println(redissonClient); // org.redisson.Redisson@40a8a26f
    }

二、使用 Redisson 分布式锁

用锁的一般步骤:

  1. 获取锁实例(只是获得一把锁的引用,并不是占有锁)
  2. 通过锁实例加锁(占有了这把锁)
  3. 通过锁实例释放锁

Redisson 提供很多种类型的锁,其中最常用的就是可重入锁(Reentrant Lock)了。

Redisson 中的可重入锁

1、获取锁实例

RLock lock = redissonClient.getLock(String lockName);

获取的锁实例实现了 RLock 接口,而该接口拓展了 JUC 包中的 Lock 接口,以及异步锁接口 RLockAsync。

2、通过锁实例加锁(Rlock 常用的方法)

同步异步特性来区分,加锁方法可分为同步加锁和异步加锁两类。异步加锁方法的名称一般是在相应的同步加锁方法后加上“Async”后缀。
阻塞非阻塞特性来区分,加锁方法可分为阻塞加锁和非阻塞加锁两类。非阻塞加锁方法的名称一般是“try”开头。

下面以比较常用的同步加锁方法来说明加锁的一些细节。

阻塞加锁的方法:
  • void lock():lock 表示如果当前锁可用,则加锁成功,并立即返回,没有返回值,继续执行下面代码;如果当前锁不可用,则阻塞等待直至锁可用(当前锁失效时间默认30 s),然后返回。
//创建锁
RLock helloLock = redissonClient.getLock("hello");
     
//加锁
helloLock.lock();
try {
    log.info("locked");
    Thread.sleep(1000 * 10);
} finally {
    //释放锁
    helloLock.unlock();
}
log.info("finished");
  • void lock(long leaseTime, TimeUnit unit):加锁机制与 void lock() 相同,只是增加了锁的有效(租赁)时长 leaseTime。加锁成功后,可以在程序中显式调用 unlock() 方法进行释放;如果未显式释放,则经过 leaseTime 时间,该锁会自动释放。如果 leaseTime 传入 -1,则会开启看门狗机制,跟上面 lock() 方法意义一致。
非阻塞加锁的方法:
  • boolean tryLock():(JUC 中 Lock 接口定义的方法)调用该方法会立刻返回。返回值为true则表示锁可用,加锁成功;返回值为false则表示锁不可用,加锁失败。
  • boolean tryLock(long time, TimeUnit unit):尝试去加锁(第一个参数表示 the maximum time to wait for the lock),如果锁可用则立刻返回 true,继续执行 true 下面代码,否则最多等待 time 长的时间(如果 time<=0,则不会等待)。在 time 时间内锁可用则立刻返回 true,time 时间之后返回 false。如果在等待期间线程被其他线程中断,则会抛出  InterruptedException 异常。
String key ="product:001";
RLock lock = redisson.getLock(key);
try {
    boolean res = lock.tryLock(10,TimeUnit.SECONDS);
    if ( res){
        System.out.println("这里是你的业务代码");
    } else {
        System.out.println("系统繁忙");
    }
} catch (Exception e){
    e.printStackTrace();
} finally {
    lock.unlock();
}
  • boolean tryLock(long waitTime, long leaseTime, TimeUnit unit):与 boolean tryLock(long time, TimeUnit unit) 类似,只是增加了锁的使用(租赁)时长 leaseTime。表示尝试去加锁(第一个参数表示等待时间,第二个参数表示 key 的失效时间),加锁成功,返回true,继续执行 true 下面代码。如果返回 false,它会等待第一个参数设置的时间,然后去执行 false 下面的代码。
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

解读:尝试加锁,最多等待 100 秒,上锁以后 10 s自动解锁,没有 watch dog 机制。

3、通过锁实例释放锁

void unlock():释放锁。如果当前线程是锁的持有者(即在该锁实例上加锁成功的线程),则会释放成功,否则会抛出异常。

4、异步执行分布式锁

/**
* 异步锁
*/
lock = redissonClient.getLock("erbadagang-lock");
Future res = null;
try {
    // lock.lockAsync();
    // lock.lockAsync(100, TimeUnit.SECONDS);
    res = lock.tryLockAsync(3, 100, TimeUnit.SECONDS);
    if (res.get()) {
        System.out.println("这里是你的Async业务代码");
    } else {
        System.out.println("系统繁忙Async");
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    if (res.get()) {
        lock.unlock();
    }
}
    log.info("finished");

三、一般编程范式

1、同步阻塞加锁

String lockName = ...
RLock lock = redissonClient.getLock(lockName);
// 阻塞式加锁
lock.lock();
try {
    // 操作受锁保护的资源

} finally {
    // 释放锁
    lock.unlock();
}

2、同步非阻塞加锁

String lockName = ...
RLock lock = redissonClient.getLock(lockName);
if (lock.tryLock()) {
    try {
        // 操作受锁保护的资源
    } finally {
        lock.unlock();
    }
} else {
    // 执行其他业务操作
}

四、分布式锁分析

优秀的分布式锁需要具备以下特性:

  • 互斥性:在任意时刻,只有一个客户端(线程)能持有锁,这是锁的基本要求。
  • 锁的可重入:同一个客户端能多次持有同一把锁。实现上只要检查锁的持有者是否为当前客户端,若是则重入锁成功,并将锁的持有数加1。一般通过给每个客户端分配一个唯一的ID,并在加锁成功时向锁中写入该ID即可。
  • 不会因客户端异常而长久锁住:当客户端在持有锁期间崩溃而未主动解锁时,锁也会在一定时间后自动释放,即锁有超时自动释放的特性。
  • 解锁的安全性:加锁和解锁必须是同一个客户端,客户端不能把别人加的锁给释放了,即不能误解锁。实现上与锁的可重入类似,在释放锁时检查客户端ID与锁中保存的ID是否一致即可。

Redisson的分布式锁除了实现上述几个特性外,还具有锁的自动续期功能。即当我们加锁而未指定锁的有效时长时,Redisson会按一定的周期,定时检查当前线程是否活跃,若是则自动为锁续期,这一特性称为watchdog(看门狗)机制。
有了这个特性,我们就可以不必为设定锁的有效时间而纠结了(设得太长,则会在客户端崩溃后仍长时间占有锁;设得太短,则可能在业务逻辑执行完成前,锁自动释放),Redisson分布式锁可以在客户端崩坏时自动释放,业务逻辑未执行完时自动续期。

五、看门狗机制

// 拿锁失败时,会不停的重试
RLock lock = redissonClient.getLock("guodong");
// 具有 watch Dog 自动延期机制,默认续 30 s
lock.lock();

// 尝试拿锁10s后停止重试,返回false,具有 watch dog 自动延期机制默认续30s
boolean res1 = lock.tryLock(10, TimeUnit.SECONDS); 
// 没有 watch dog,10s后自动释放
lock.lock(10, TimeUnit.SECONDS);
// 尝试拿锁100s后停止重试,返回false,没有 watch Dog,10s后自动释放
boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);
Thread.sleep(40000L);
lock.unlock();

 5.1 Watch Dog 的自动延期机制

如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,加入一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,Redisson 给出了自己的答案,就是 watch dog 自动延期机制。

Redisson 提供了一个监控锁的看门狗,它的作用是在 Redisson实例被关闭前,不断地延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断地延长锁超时时间,锁不会因为超时而被释放。

默认情况下,看门狗的续期时间是30s,也可以通过修改 Config.lockWatchdogTimeout 来另行指定。另外 Redisson 还提供了可以指定 leaseTime 参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。

5.2 结论

  • watch dog 只有在未显示指定加锁时间(leaseTime)时才会生效。(这点很重要)
  • watch dog 如果指定加锁时间(leaseTime = -1),也是会开启看门狗机制的
  • watch dog 在当前节点存活时每 10 s给分布式锁的 key 续期 30s
  • watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断地给锁续期
  • 如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中
  • 要使 watch dog 机制生效,lock 时不要设置过期时间
  • watch dog 的延时时间可以由 lockWatchDogTimeout 指定默认延时时间,但是不要设置太小
  • watch dog 会每 lockWatchDogTimeout / 3 时间去延时
  • watch dog 通过类似 netty 的 Future 功能来实现异步延时
  • watch dog 最终还是通过 lua 脚本来进行延时

五、参考文档

  • Redisson分布式锁在Java应用中的使用
  • Spring Boot中使用Redisson

  • redisson中的看门狗机制总结

你可能感兴趣的:(spring,boot,java)