• 【SpringBoot】Redission 的使用与介绍


    背景:

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

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

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

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

    一、SpringBoot 应用

    1、通过 Maven 引入依赖

    1. <dependency>
    2. <groupId>org.redissongroupId>
    3. <artifactId>redisson-spring-boot-starterartifactId>
    4. <version>3.14.0version>
    5. dependency>

    2、在 yml 文件中添加配置

    1. spring:
    2. redis:
    3. host: 127.0.0.1
    4. port: 6379
    5. timeout: 10000
    6. password:
    7. lettuce:
    8. pool:
    9. #最大连接数据库连接数,设 0 为没有限制
    10. max-active: 8
    11. #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
    12. max-wait: 1000
    13. #最大等待连接中的数量,设 0 为没有限制
    14. max-idle: 500
    15. #最小等待连接中的数量,设 0 为没有限制
    16. min-idle: 300
    17. jedis:
    18. pool:
    19. max-active: 8
    20. max-wait: 1000
    21. max-idle: 500
    22. min-idle: 300

    3、自定义配置类

    1. @Configuration
    2. public class RedissonConfig {
    3. @Value("${spring.redis.password}")
    4. private String password;
    5. @Value("${spring.redis.host}")
    6. private String host;
    7. @Value("${spring.redis.port}")
    8. private String port;
    9. /**
    10. * 对 Redisson 的使用都是通过 RedissonClient 对象
    11. * @return
    12. */
    13. @Bean(name = "redissonClient", destroyMethod = "shutdown") // 服务停止后调用 shutdown 方法
    14. public RedissonClient redissonClient() {
    15. // 1、创建配置
    16. Config config = new Config();
    17. // 2、集群模式
    18. // config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");
    19. // 根据 Config 创建出 RedissonClient 示例
    20. config.useSingleServer()
    21. .setPassword(StringUtils.isEmpty(password) ? null : password)
    22. .setAddress(host.contains("://") ? "" : "redis://" + host + ":" + port);
    23. return Redisson.create(config);
    24. }
    25. }

    4、测试配置类

    1. @Autowired
    2. private RedissonClient redissonClient;
    3. @Test
    4. public void test() {
    5. System.out.println(redissonClient); // org.redisson.Redisson@40a8a26f
    6. }

    二、使用 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),然后返回。
    1. //创建锁
    2. RLock helloLock = redissonClient.getLock("hello");
    3. //加锁
    4. helloLock.lock();
    5. try {
    6. log.info("locked");
    7. Thread.sleep(1000 * 10);
    8. } finally {
    9. //释放锁
    10. helloLock.unlock();
    11. }
    12. 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 异常。
    1. String key ="product:001";
    2. RLock lock = redisson.getLock(key);
    3. try {
    4. boolean res = lock.tryLock(10,TimeUnit.SECONDS);
    5. if ( res){
    6. System.out.println("这里是你的业务代码");
    7. } else {
    8. System.out.println("系统繁忙");
    9. }
    10. } catch (Exception e){
    11. e.printStackTrace();
    12. } finally {
    13. lock.unlock();
    14. }
    • 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、异步执行分布式锁

    1. /**
    2. * 异步锁
    3. */
    4. lock = redissonClient.getLock("erbadagang-lock");
    5. Future res = null;
    6. try {
    7. // lock.lockAsync();
    8. // lock.lockAsync(100, TimeUnit.SECONDS);
    9. res = lock.tryLockAsync(3, 100, TimeUnit.SECONDS);
    10. if (res.get()) {
    11. System.out.println("这里是你的Async业务代码");
    12. } else {
    13. System.out.println("系统繁忙Async");
    14. }
    15. } catch (Exception e) {
    16. e.printStackTrace();
    17. } finally {
    18. if (res.get()) {
    19. lock.unlock();
    20. }
    21. }
    22. log.info("finished");

    三、一般编程范式

    1、同步阻塞加锁

    1. String lockName = ...
    2. RLock lock = redissonClient.getLock(lockName);
    3. // 阻塞式加锁
    4. lock.lock();
    5. try {
    6. // 操作受锁保护的资源
    7. } finally {
    8. // 释放锁
    9. lock.unlock();
    10. }

    2、同步非阻塞加锁

    1. String lockName = ...
    2. RLock lock = redissonClient.getLock(lockName);
    3. if (lock.tryLock()) {
    4. try {
    5. // 操作受锁保护的资源
    6. } finally {
    7. lock.unlock();
    8. }
    9. } else {
    10. // 执行其他业务操作
    11. }

    四、分布式锁分析

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

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

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

    五、看门狗机制

    1. // 拿锁失败时,会不停的重试
    2. RLock lock = redissonClient.getLock("guodong");
    3. // 具有 watch Dog 自动延期机制,默认续 30 s
    4. lock.lock();
    5. // 尝试拿锁10s后停止重试,返回false,具有 watch dog 自动延期机制默认续30s
    6. boolean res1 = lock.tryLock(10, TimeUnit.SECONDS);
    7. // 没有 watch dog,10s后自动释放
    8. lock.lock(10, TimeUnit.SECONDS);
    9. // 尝试拿锁100s后停止重试,返回false,没有 watch Dog,10s后自动释放
    10. boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);
    11. Thread.sleep(40000L);
    12. 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 脚本来进行延时

    五、参考文档

  • 相关阅读:
    基于PHP+MySQL毕业生档案管理系统
    IDEA 2022创建Spring Boot项目
    Springcloud实战之自研分布式id生成器
    Android简易音乐重构MVVM Java版-BottomNavigationView+viewpager主界面结构(十一)
    项目实战(依旧还是登录认证,JWT解析异常处理,授权信息处理)
    MQ消息队列(二)——RabbitMQ进阶,保证消息的可靠性
    vue导出功能实现
    闭眼推荐,9 个不能错过的机器学习数据集
    mysql中的全文索引
    WebRTC源码之音频设备的录制流程源码分析
  • 原文地址:https://blog.csdn.net/yuxiangdeming/article/details/134037685