从Redis实现分布式锁的问题延伸到Redisson的利用入门

打印 上一主题 下一主题

主题 890|帖子 890|积分 2670

 部分文章泉源:二哥Java,JavaGuide
本文从Redis的setNx实现分布式锁的问题,以及Redi自己简朴实现锁的锁误删,锁提前释放,不可重入锁,死锁问题然后延伸到开源的框架Redisson的入门利用



前提知识-什么是可重入锁?

也就是我们的同一个线程可以多次获取一把锁
为什么要有可重入锁?

多次锁定

如果一个线程已经获得了可重入锁,它可以在没有释放锁的环境下,再次获取该锁。每次获取锁时,锁的内部计数器会递增
当线程调用unlock()释放锁时,计数器递减。只有当计数器减为零时,锁才会真正被释放,其他线程才有时机获得该锁
递归调用

在递归调用中,方法可能会多次尝试获取同一把锁。可重入锁允许这种行为,使得递归方法调用不会因为再次获取已经持有的锁而导致死锁。
因为有些环境,比方递归,他会重复获取同一个锁
但是递归的时间,它自己等候自己释放,那不就造成了死锁的征象?
所以我们就要有可重入锁
嵌套锁定

如果一个方法调用另一个需要同一把锁的方法,可重入锁允许这种嵌套调用,而不会导致线程壅闭

Redis实现分布式锁会有什么问题?

问题地点

默认环境下,如果利用 setnx lock true 实现分布式锁会存在以下问题:

  • 死锁问题:setnx 如未设置逾期时间,锁忘记删了或加锁线程宕机都会导致死锁,也就是分布式锁一直被占用的环境。
  • 锁误删问题:setnx 设置了超时时间,但因为实验时间太长,所以程序没运行完,锁已经被自动释放了,但线程不知道,因此在线程实验竣事之后,会把其他线程的锁误删的问题。
  • 不可重入问题:也就是说同一线程在已经获取了某个锁的环境下,如果再次请求获取该锁,则请求会失败(因为只有在第一次能加锁成功)。也就是说,一个线程不能对自己已持有的锁进行重复锁定。
  • 无法自动续期:线程在持有锁期间,任务未能实验完成,锁可能会因为超时而自动释放。SETNX 无法自动根据任务的实验环境,设置新的超时实现,以延长锁的时间。
而这些问题的解决方案也是不同的。
① 解决死锁问题

死锁问题可以通过设置超时时间来解决,如果凌驾了超时时间,分布锁会自动释放,这样就不会存在死锁问题了。也就是 setnx 和 expire 配合利用,在 Redis 2.6.12 版本之后,新增了一个强大的功能,我们可以利用一个原子操作也就是一条命令来实验 setnx 和 expire 操作了,实现示比方下:
  1. 127.0.0.1:6379> set lock true ex 30 nx
  2. OK #创建锁成功
  3. 127.0.0.1:6379> set lock true ex 30 nx
  4. (nil) #在锁被占用的时候,企图获取锁失败
复制代码
其中 ex 为设置超时时间, nx 为元素非空判定,用来判定是否能正常利用锁的
② 解决锁误删问题

锁误删可以通过将锁标识存储到 Redis 中来解决,删除之前先判定锁归属(也就是将线程 id 存储到分布式的 value 值内,删除之前先判定锁 value 值是否等于当前线程 id),如果属于你的锁再删除,否则不删除就可以,这就解决了锁误删的问题。
但这样解决因为判定和删除是非原子操作,所以仍旧有问题,这个问题可以通过编写 lua 脚本或利用 Redisson 框架来解决,因为他们两都能保证判定和删除的原子性。
③ 通用解决方案

以上问题有一个通用的解决方案,那就是利用 Redisson 框架来实现 Redis 分布式锁,这样既可以解决死锁问题,也可以解决锁误删、不可重入和无法自动续期的问题了。
简朴来说就是锁误删,锁续期,不可重入,死锁这4个问题

怎样解决分布式锁可靠性问题(红锁,联锁)

利用Redisson


Redisson实现了多种分布式锁
Redisson分布式锁种另有一个Watch Dog(看门狗)机制,当操作共享资源还没完成的时间,可以或许实现自动续期
 



一样平常分布式锁我们是利用Redisson来做的
 



Redisson怎样解决集群环境下分布式锁的可靠性


红锁


如果我们的Redis主节点,拿到锁之后宕机了
然后没有同步到其他节点,然后就推举出了一个新的Redis主节点
因为锁没有同步,所以这个新的Redis主节点还可以获取锁
我们会出现这种问题
这个时间就是Redlock算法,红锁,来解决这个问题了

如果我们可以或许和半数以上的实例成功加锁,那么就认为客户端成功获取到了锁
 




 





实在红锁,是联锁的一种实现
 




联锁



redisson.getMultiLock(锁1,锁2,锁3)
 




怎样利用Redisson实现分布式锁
 


什么是Redisson

Redisson 是一个开源的用于操作 Redis 的 Java 框架。与 Jedis 和 Lettuce 等轻量级的 Redis 框架不同,它提供了更高级且功能丰富的 Redis 客户端。它提供了很多简化 Redis 操作的高级 API,并支持分布式对象、分布式锁、分布式集合等特性。
Redisson 官网地址:Redisson: Valkey and Redis Java client. Complete Real-Time Data Platform
源码地址:GitHub - redisson/redisson: Redisson - Valkey and Redis Java client. Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache..
特性


  • Redisson 可以设置分布式锁的逾期时间,从而避免锁一直被占用而导致的死锁问题。
  • Redisson 在为每个锁关联一个线程 ID(保证当前线程只能释放当前线程的锁) 和重入次数(递增计数器)作为分布锁 value 的一部分存储在 Redis 中,这样就避免了锁误删和不可重入的问题。
  • Redisson 还提供了自动续期的功能,通过定时任务(看门狗)定期延长锁的有效期,确保在业务未完成前,锁不会被其他线程获取。
Redisson 实现分布锁

① 添加 Redisson 框架支持
如果是 Spring Boot 项目,直接添加 Redisson 为 Spring Boot 写的如下依赖:
  1. <!-- Redisson -->
  2. <!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
  3. <dependency>
  4. <groupId>org.redisson</groupId>
  5. <artifactId>redisson-spring-boot-starter</artifactId>
  6. <version>3.25.2</version><!-- 请根据实际情况使用最新版本 -->
  7. </dependency>
复制代码
其他项目,访问 https://mvnrepository.com/search?q=Redisson 获取详细依赖设置。
② 设置 RedissonClient 对象
将 RedissonClient 重写,存放到 IoC 容器,并且设置连接的 Redis 服务器信息。
 
  1. import org.redisson.Redisson;
  2. import org.redisson.api.RedissonClient;
  3. import org.redisson.config.Config;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. @Configuration
  7. publicclassRedissonConfig {
  8. @Bean
  9. public RedissonClient redissonClient() {
  10. Configconfig=newConfig();
  11. // 也可以将 redis 配置信息保存到配置文件
  12. config.useSingleServer().setAddress("redis://127.0.0.1:6379");
  13. return Redisson.create(config);
  14. }
  15. }
复制代码
实现公平锁


Redisson 默认创建的分布式锁是非公平锁(出于性能的思量),想要把它变成公平锁可利用以下代码实现:
 
  1. RLocklock= redissonClient.getFairLock(lockKey); // 获取公平锁
复制代码
实现读写锁


Redisson 还可以创建读写锁,如下代码所示:
  1. RReadWriteLocklock= redissonClient.getReadWriteLock(lockKey); // 获取读写锁
  2. lock.readLock(); // 读锁
  3. lock.writeLock(); // 写锁
复制代码
读写锁的特点就是并发性能高(读读共享、读写/写写不共享),它是允很多个线程同时获取读锁进行读操作的,也就是说在没有写锁的环境下,读取操作可以并发实验,提高了体系的并行度。但写锁则是独占式的,同一时间只有一个线程可以获得写锁,无论是读还是写都无法与写锁并存,这样就确保了数据修改时的数据一致性。

实现联锁

Redisson 也支持联锁,也叫分布式多锁 MultiLock,它允许客户端一次性获取多个独立资源(RLock)上的锁,这些资源可能是不同的键或同一键的不同锁。当所有指定的锁都被成功获取后,才会认为整个操作成功锁定。这样可以或许确保在分布式环境下进行跨资源的并发控制。
联锁的实现示比方下:
  1. // 获取需要加锁的资源
  2. RLocklock1= redisson.getLock("lock1");
  3. RLocklock2= redisson.getLock("lock2");
  4. // 联锁
  5. RedissonMultiLockmultiLock=newRedissonMultiLock(lock1, lock2);
  6. try {
  7. // 一次性尝试获取所有锁
  8. if (multiLock.tryLock()) {
  9. // 获取锁成功...
  10. }
  11. } finally {
  12. // 释放所有锁
  13. multiLock.unlock();
  14. }
复制代码

说一下Redisson的看门狗机制

什么是看门狗机制

Redisson 看门狗(Watchdog)机制是一种用于延长分布式锁的有效期的机制。它通过定时续租锁的方式,防止持有锁的线程在实验操作时凌驾了锁的有效期而导致锁被自动释放。
看门狗(Watchdog)的实验过程大致如下:

  • 获取锁并设置超时时间:当客户端通过 Redisson 尝试获取一个分布式锁时,会利用 Redis 命令将锁存入 Redis,并设置一个初始的有效时间(即超时时间)。
  • 启动看门狗线程:如果开启了看门狗的功能(默认开启),在成功获取锁后,Redisson 会在客户端内部启动一个背景守护线程,也就是所谓的“看门狗”定时任务定时去实验并续期。
  • 定时检查与续期:看门狗按照预设的时间隔断(默认为锁有效时间的三分之一)周期性地检查锁是否仍然被当前客户端持有。如果客户端仍然持有锁,看门狗会调用 Redis 的相关命令或者 Lua 脚原来延长锁的有效期,确保在业务处理期间锁不会因超时而失效。
  • 循环监控和更新:这个过程会一直持续到客户端显式地释放锁,或者由于其他原因(比方客户端崩溃、网络中断等)导致无法继续实验看门狗任务为止。
  • 制止看门狗任务:客户端在完成业务逻辑后,会主动调用解锁方法释放锁,此时 Redisson 不仅会解除对 Redis 中对应键的锁定状态,还会同步制止看门狗的任务。
通过看门狗机制,即使在长时间运行的业务场景下,也能有效地避免由于锁超时而导致的数据不一致或其他并发控制问题,提高了体系的稳定性和可靠性。

知识扩展

看门狗实现原理

Redisson 看门狗的底层实现就是一个定时任务,它的看门狗默认的超时时间是 30s,不过超时时间可以利用 Config.lockWatchdogTimeout 来进行设置。
看门狗每隔 lockWatchdogTimeout/3L 会实验一次检查和续期分布式锁,它的焦点实现源码如下:
  1. privatevoidrenewExpiration() {
  2.         // 拿到需要延期的锁信息
  3.         RedissonBaseLock.ExpirationEntryee= (RedissonBaseLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
  4.         // 如果锁信息不为空
  5.         if (ee != null) {
  6.                 // 构建一个定时任务,周期为 this.internalLockLeaseTime / 3L
  7.                 // 而this.internalLockLeaseTime就是上面提到的默认超时时间30000L,也就是30s
  8.                 // 30000L / 3L,得出周期时间为 10秒,也就是说,10秒检查一次,每次都会将锁的过期时间延长至30秒
  9.                 Timeouttask=this.commandExecutor.getConnectionManager().newTimeout(newTimerTask() {
  10.                         publicvoidrun(Timeout timeout)throws Exception {
  11.                                 // 获取要延期的锁信息
  12.                                 RedissonBaseLock.ExpirationEntryent= (RedissonBaseLock.ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
  13.                                 // 锁存在才继续,否则退出
  14.                                 if (ent != null) {
  15.                                         LongthreadId= ent.getFirstThreadId();
  16.                                         // 线程id不为空才继续,否则退出
  17.                                         if (threadId != null) {
  18.                                                 // 通过lua脚本进行延期
  19.                                                 RFuture<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
  20.                                                 future.onComplete((res, e) -> {
  21.                                                         // 如果e不为空,代表有异常,从待延期锁信息集合中删除当前锁,并退出
  22.                                                         if (e != null) {
  23.                                                                 RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
  24.                                                                 RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
  25.                                                         } else {
  26.                                                                 // 延期成功,递归调度,进入下一次延期
  27.                                                                 if (res) {
  28.                                                                         RedissonBaseLock.this.renewExpiration();
  29.                                                                 }
  30.                                                         }
  31.                                                 });
  32.                                         }
  33.                                 }
  34.                         }
  35.                 }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
  36.                 ee.setTimeout(task);
  37.         }
  38. }
复制代码

看门狗制止续期

Redisson 看门狗当遇到以下环境会制止自动续期:

  • 锁被主动释放(调用 unlock 方法):当持有锁的线程主动释放锁时,Redisson 的看门狗会制止续期。这是因为锁已经不再被持有,没有必要进行续租操作。
  • 线程意外制止:在持有锁的线程意外制止时,Redisson 的看门狗会制止续期。这是为了避免已经不再活动的线程持有锁,并防止续租请求的无效实验。
  • 锁被其他线程抢占:当其他线程成功获取到同一把锁时,Redisson 的看门狗会制止续期。这是因为锁的持有线程发生了变化,原先持有锁的线程失去了锁的所有权,不再需要进行续租操作。
  • Redisson 客户端连接断开:如果 Redisson 与 Redis 服务器端之间的连接断开,看门狗会制止续期。这是为了保证续租请求的可靠性,如果无法与 Redis 创建连接,就无法实验续期操作。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

杀鸡焉用牛刀

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表