【SpringBoot】Redission 的使用与介绍

背景:

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

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

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

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

一、SpringBoot 应用

1、通过 Maven 引入依赖

XML 复制代码
<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson-spring-boot-starter</artifactId>
   <version>3.14.0</version>
</dependency>

2、在 yml 文件中添加配置

XML 复制代码
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、自定义配置类

java 复制代码
@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、测试配置类

java 复制代码
    @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、获取锁实例

java 复制代码
RLock lock = redissonClient.getLock(String lockName);

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

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

同步异步 特性来区分,加锁方法可分为同步加锁和异步加锁两类。异步加锁方法的名称一般是在相应的同步加锁方法后加上"Async"后缀。

阻塞非阻塞特性来区分,加锁方法可分为阻塞加锁和非阻塞加锁两类。非阻塞加锁方法的名称一般是"try"开头。

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

阻塞加锁的方法:
  • void lock():lock 表示如果当前锁可用,则加锁成功,并立即返回,没有返回值,继续执行下面代码;如果当前锁不可用,则阻塞等待直至锁可用(当前锁失效时间默认30 s),然后返回。
java 复制代码
//创建锁
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 异常。
java 复制代码
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 下面的代码。
java 复制代码
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

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

3、通过锁实例释放锁

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

4、异步执行分布式锁

java 复制代码
/**
* 异步锁
*/
lock = redissonClient.getLock("erbadagang-lock");
Future<Boolean> 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、同步阻塞加锁

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

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

2、同步非阻塞加锁

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

四、分布式锁分析

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

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

Redisson的分布式锁除了实现上述几个特性外,还具有锁的自动续期功能。即当我们加锁而未指定锁的有效时长时,Redisson会按一定的周期,定时检查当前线程是否活跃,若是则自动为锁续期,这一特性称为watchdog(看门狗)机制。

有了这个特性,我们就可以不必为设定锁的有效时间而纠结了(设得太长,则会在客户端崩溃后仍长时间占有锁;设得太短,则可能在业务逻辑执行完成前,锁自动释放),Redisson分布式锁可以在客户端崩坏时自动释放,业务逻辑未执行完时自动续期。

五、看门狗机制

java 复制代码
// 拿锁失败时,会不停的重试
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 脚本来进行延时

五、参考文档

相关推荐
泉城老铁1 天前
Spring Boot中实现多线程分片下载
java·spring boot·后端
Issie71 天前
ThreadLocal 内存泄漏详解
java
泉城老铁1 天前
Spring Boot中实现多文件打包下载
spring boot·后端·架构
泉城老铁1 天前
Spring Boot中实现大文件分片下载和断点续传功能
java·spring boot·后端
友莘居士1 天前
长流程、复杂业务流程分布式事务管理实战
spring boot·rocketmq·saga·复杂流程分布式事务·长流程
master-dragon1 天前
java log相关:Log4J、Log4J2、LogBack,SLF4J
java·log4j·logback
奔跑吧邓邓子1 天前
【Java实战㉖】深入Java单元测试:JUnit 5实战指南
java·junit·单元测试·实战·junit5
百思可瑞教育1 天前
Spring Boot 参数校验全攻略:从基础到进阶
运维·服务器·spring boot·后端·百思可瑞教育·北京百思教育
SheldonChang1 天前
Onlyoffice集成与AI交互操作指引(Iframe版)
java·人工智能·ai·vue·onlyoffice·postmessage
数据爬坡ing1 天前
C++ 类库管理系统的分析与设计:面向对象开发全流程实践
java·运维·开发语言·c++·软件工程·软件构建·运维开发