当前位置:   article > 正文

【SpringBoot】Redission 的使用与介绍_springboot redission

springboot redission

背景:

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

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

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

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

一、SpringBoot 应用

1、通过 Maven 引入依赖

  1. <dependency>
  2. <groupId>org.redisson</groupId>
  3. <artifactId>redisson-spring-boot-starter</artifactId>
  4. <version>3.14.0</version>
  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<Boolean> 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 脚本来进行延时

五、参考文档

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/在线问答5/article/detail/965605
推荐阅读
相关标签
  

闽ICP备14008679号