当前位置:

首页 > 编程开发 > springboot基于redisson如何实现redis分布式可重入锁源码解析

springboot基于redisson如何实现redis分布式可重入锁源码解析

一、前言我们在实现使用Redis实现分布式锁,最开始一般使用SETresource-nameanystringNXEXmax-lock-time进行加锁,使用Lua脚本保证原子性进行实现释放锁。这样手动实现比较麻烦,对此Redis官网也明确说Java版使用Redisson来实现。小编也是看了官网慢慢的摸索清楚,特写此记录一下。从官网到整合Springboot到源码解读,以单节点为例。二、为什么使用Redisson1.我们打开官网redis中文官网2.我们可以看到官方让我们去使用其他3.打开官方推荐4.找到

      一、前言

      我们在实现使用Redis实现分布式锁,最开始一般使用SET resource-name anystring NX EX max-lock-time进行加锁,使用Lua脚本保证原子性进行实现释放锁。这样手动实现比较麻烦,对此Redis官网也明确说Java版使用Redisson来实现。小编也是看了官网慢慢的摸索清楚,特写此记录一下。从官网到整合Springboot到源码解读,以单节点为例。

      二、为什么使用Redisson

      1. 我们打开官网

      redis中文官网

      2. 我们可以看到官方让我们去使用其他

      springboot基于redisson如何实现redis分布式可重入锁源码解析

      3. 打开官方推荐

      springboot基于redisson如何实现redis分布式可重入锁源码解析

      4. 找到文档

      Redisson地址

      springboot基于redisson如何实现redis分布式可重入锁源码解析

      5. Redisson结构

      springboot基于redisson如何实现redis分布式可重入锁源码解析

      三、Springboot整合Redisson

      1. 导入依赖

      
          org.springframework.boot
          spring-boot-starter-data-redis
      
      
          redis.clients
          jedis
      
      
      
          org.redisson
          redisson
          3.12.0
      

      2. 以官网为例查看如何配置

      springboot基于redisson如何实现redis分布式可重入锁源码解析

      3. 编写配置类

      import org.redisson.Redisson;
      import org.redisson.api.RedissonClient;
      import org.redisson.config.Config;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * @author wangzhenjun
       * @date 2022/2/9 9:57
       */
      @Configuration
      public class MyRedissonConfig {
      
          /**
           * 所有对redisson的使用都是通过RedissonClient来操作的
           * @return
           */
          @Bean(destroyMethod="shutdown")
          public RedissonClient redisson(){
              // 1. 创建配置
              Config config = new Config();
              // 一定要加redis://
              config.useSingleServer().setAddress("redis://192.168.17.130:6379");
              // 2. 根据config创建出redissonClient实例
              RedissonClient redissonClient = Redisson.create(config);
              return redissonClient;
          }
      }

      4. 官网测试加锁例子

      springboot基于redisson如何实现redis分布式可重入锁源码解析

      5. 根据官网简单Controller接口编写

      @ResponseBody
      @GetMapping("/hello")
      public String hello(){
          // 1.获取一把锁,只要锁名字一样,就是同一把锁
          RLock lock = redisson.getLock("my-lock");
          // 2. 加锁
          lock.lock();// 阻塞试等待  默认加的都是30s
          // 带参数情况
          // lock.lock(10, TimeUnit.SECONDS);// 10s自动解锁,自动解锁时间一定要大于业务的执行时间。
          try {
              System.out.println("加锁成功" + Thread.currentThread().getId());
              Thread.sleep(30000);
          } catch (InterruptedException e) {
              e.printStackTrace();
          } finally {
              // 3. 解锁
              System.out.println("解锁成功:" + Thread.currentThread().getId());
              lock.unlock();
          }
          return "hello";
      }

      6. 测试

      springboot基于redisson如何实现redis分布式可重入锁源码解析

      四、lock.lock()源码分析

      1. 打开RedissonLock实现类

      springboot基于redisson如何实现redis分布式可重入锁源码解析

      2. 找到实现方法

      @Override
      public void lock() {
          try {
          	// 我们发现不穿过期时间源码默认过期时间为-1
              lock(-1, null, false);
          } catch (InterruptedException e) {
              throw new IllegalStateException();
          }
      }

      3. 按住Ctrl进去lock方法

      private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
      	// 获取线程的id,占有锁的时候field的值为UUID:线程号id
          long threadId = Thread.currentThread().getId();
          // 尝试获得锁
          Long ttl = tryAcquire(leaseTime, unit, threadId);
          // lock acquired 获得锁,返回
          if (ttl == null) {
              return;
          }
      	// 这里说明获取锁失败,就通过线程id订阅这个锁
          RFuture future = subscribe(threadId);
          if (interruptibly) {
              commandExecutor.syncSubscriptionInterrupted(future);
          } else {
              commandExecutor.syncSubscription(future);
          }
      
          try {
          	// 这里进行自旋,不断尝试获取锁
              while (true) {
              	// 继续尝试获取锁
                  ttl = tryAcquire(leaseTime, unit, threadId);
                  // lock acquired 获取成功
                  if (ttl == null) {
                  	// 直接返回,挑出自旋
                      break;
                  }
      
                  // waiting for message 继续等待获得锁
                  if (ttl >= 0) {
                      try {
                          future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                      } catch (InterruptedException e) {
                          if (interruptibly) {
                              throw e;
                          }
                          future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                      }
                  } else {
                      if (interruptibly) {
                          future.getNow().getLatch().acquire();
                      } else {
                          future.getNow().getLatch().acquireUninterruptibly();
                      }
                  }
              }
          } finally {
           	// 取消订阅
              unsubscribe(future, threadId);
          }
      //        get(lockAsync(leaseTime, unit));
      }

      4. 进去尝试获取锁方法

      springboot基于redisson如何实现redis分布式可重入锁源码解析

      private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
      	// 直接进入异步方法
          return get(tryAcquireAsync(leaseTime, unit, threadId));
      }
      
      private  RFuture tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
          // 这里进行判断如果没有设置参数leaseTime = -1
          if (leaseTime != -1) {
              return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
          }
          // 此方法进行获得锁,过期时间为看门狗的默认时间
          // private long lockWatchdogTimeout = 30 * 1000;看门狗默认过期时间为30s
          // 加锁和过期时间要保证原子性,这个方法后面肯定调用执行了Lua脚本,我们下面在看
          RFuture ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
          // 开启一个定时任务进行不断刷新过期时间
          ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
              if (e != null) {
                  return;
              }
              // lock acquired 获得锁
              if (ttlRemaining == null) {
              	// 刷新过期时间方法,我们下一步详细说一下
                  scheduleExpirationRenewal(threadId);
          });
          return ttlRemainingFuture;

      5. 查看tryLockInnerAsync()方法

       RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
          internalLockLeaseTime = unit.toMillis(leaseTime);
      
          return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
          		  // 首先判断锁是否存在
                    "if (redis.call('exists', KEYS[1]) == 0) then " +
                    		// 存在则获取锁
                        "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                        // 然后设置过期时间
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                    "end; " +
                    // hexists查看哈希表的指定字段是否存在,存在锁并且是当前线程持有锁
                    "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    		// hincrby自增一
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        	// 锁的值大于1,说明是可重入锁,重置过期时间
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                    "end; " +
                    // 锁已存在,且不是本线程,则返回过期时间ttl
                    "return redis.call('pttl', KEYS[1]);",
                      Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
      }

      6. 进入4留下的定时任务scheduleExpirationRenewal()方法

      一步步往下找源码:scheduleExpirationRenewal --->renewExpiration

      根据下面源码,定时任务刷新时间为:internalLockLeaseTime / 3,是看门狗的1/3,即为10s刷新一次

      private void renewExpiration() {
          ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
          if (ee == null) {
              return;
          }
          
          Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
              @Override
              public void run(Timeout timeout) throws Exception {
                  ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
                  if (ent == null) {
                      return;
                  }
                  Long threadId = ent.getFirstThreadId();
                  if (threadId == null) {
                      return;
                  }
                  
                  RFuture future = renewExpirationAsync(threadId);
                  future.onComplete((res, e) -> {
                      if (e != null) {
                          log.error("Can't update lock " + getName() + " expiration", e);
                          return;
                      }
                      
                      if (res) {
                          // reschedule itself
                          renewExpiration();
                      }
                  });
              }
          }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
          
          ee.setTimeout(task);
      }

      五、lock.lock(10, TimeUnit.SECONDS)源码分析

      1. 打开实现类

      @Override
      public void lock(long leaseTime, TimeUnit unit) {
          try {
          	// 这里的过期时间为我们输入的10
              lock(leaseTime, unit, false);
          } catch (InterruptedException e) {
              throw new IllegalStateException();
          }
      }

      2. 方法lock()实现展示,同三.3源码

      3. 直接来到尝试获得锁tryAcquireAsync()方法

      private  RFuture tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
          // 这里进行判断如果没有设置参数leaseTime = -1,此时我们为10
          if (leaseTime != -1) {
          	// 来到此方法
              return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
          }
          // 此处省略后面内容,前面以详细说明。。。。
      }

      4. 打开tryLockInnerAsync()方法

      我们不难发现和没有传过期时间的方法一样,只不过leaseTime的值变了。

       RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
          internalLockLeaseTime = unit.toMillis(leaseTime);
      
          return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
          		  // 首先判断锁是否存在
                    "if (redis.call('exists', KEYS[1]) == 0) then " +
                    		// 存在则获取锁
                        "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                        // 然后设置过期时间
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                    "end; " +
                    // hexists查看哈希表的指定字段是否存在,存在锁并且是当前线程持有锁
                    "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    		// hincrby自增一
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        	// 锁的值大于1,说明是可重入锁,重置过期时间
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                    "end; " +
                    // 锁已存在,且不是本线程,则返回过期时间ttl
                    "return redis.call('pttl', KEYS[1]);",
                      Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
      }

      六、lock.unlock()源码分析

      1. 打开方法实现

      @Override
      public void unlock() {
          try {
          	// 点击进入释放锁方法
              get(unlockAsync(Thread.currentThread().getId()));
          } catch (RedisException e) {
              if (e.getCause() instanceof IllegalMonitorStateException) {
                  throw (IllegalMonitorStateException) e.getCause();
              } else {
                  throw e;
              }
          }
          
      //        Future future = unlockAsync();
      //        future.awaitUninterruptibly();
      //        if (future.isSuccess()) {
      //            return;
      //        }
      //        if (future.cause() instanceof IllegalMonitorStateException) {
      //            throw (IllegalMonitorStateException)future.cause();
      //        }
      //        throw commandExecutor.convertException(future);
      }

      2. 打开unlockAsync()方法

      @Override
      public RFuture unlockAsync(long threadId) {
          RPromise result = new RedissonPromise();
          // 解锁方法,后面展开说
          RFuture future = unlockInnerAsync(threadId);
      	// 完成
          future.onComplete((opStatus, e) -> {
              if (e != null) {
              	// 取消到期续订
                  cancelExpirationRenewal(threadId);
                  // 将这个未来标记为失败并通知所有人
                  result.tryFailure(e);
                  return;
              }
      		// 状态为空,说明解锁的线程和当前锁不是同一个线程
              if (opStatus == null) {
                  IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                          + id + " thread-id: " + threadId);
                  result.tryFailure(cause);
                  return;
              }
              
              cancelExpirationRenewal(threadId);
              result.trySuccess(null);
          });
      
          return result;
      }

      3. 打开unlockInnerAsync()方法

      protected RFuture unlockInnerAsync(long threadId) {
          return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
          		// 判断释放锁的线程和已存在锁的线程是不是同一个线程,不是返回空
                  "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                      "return nil;" +
                  "end; " +
                  // 释放锁后,加锁次数减一
                  "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                  // 判断剩余数量是否大于0
                  "if (counter > 0) then " +
                  	// 大于0 ,则刷新过期时间
                      "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                      "return 0; " +
                  "else " +
                  	// 释放锁,删除key并发布锁释放的消息
                      "redis.call('del', KEYS[1]); " +
                      "redis.call('publish', KEYS[2], ARGV[1]); " +
                      "return 1; "+
                  "end; " +
                  "return nil;",
                  Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
      
      }                                    
                                                      本文内容来源于互联网,如有侵权请联系删除。
                                                      
      作者最新文章
      编程开发
      相关文章 更多
      using namespace 使用中遇到的问题怎么解决
      using namespace 使用中遇到的问题怎么解决

      命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

      c语言函数递归 实操经验总结:这些技巧很实用
      c语言函数递归 实操经验总结:这些技巧很实用

      理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

      c语言函数递归 怎么选?常见方案对比分析
      c语言函数递归 怎么选?常见方案对比分析

      递归函数的基本概念与适用场景在C语言编程中,递归是一种函数调用自身的编程技巧。它并非适用于所有问题,但在处理某些具有自相似结构的问题时,能提供极其清晰和优雅的解决方案。递归的核心思想是将一个大规模问题分解为一个或多个同类型但规模更小的子问题,直到子问题简单到可以直接求解。典型的适用场景包括树形结构的

      Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解
      Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解

      理解内存管理的基石在Objective-C的编程世界中,内存管理是开发者必须掌握的核心技能之一。它直接关系到应用的性能、稳定性与资源利用效率。与一些采用自动垃圾回收机制的语言不同,Objective-C在很长一段时间里,依赖一套基于引用计数的、需要开发者部分介入的管理规则。这套规则的核心思想是明确的

      如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏
      如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏

      理解 dealloc 的角色与时机在 iOS 应用开发中,内存管理是保障应用性能与稳定性的基石。dealloc 方法是 Objective-C 中对象生命周期结束时的关键回调,它标志着对象即将被系统回收内存。正确理解其触发时机至关重要:当一个对象的引用计数降为零时,运行时系统会自动调用该对象的 de

      深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制
      深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制

      内存管理的基石在Objective-C的世界里,内存管理是开发者必须掌握的核心技能之一。作为一门在手动引用计数(MRC)时代诞生的语言,Objective-C要求程序员对对象的生命周期有清晰的认识。dealloc方法正是这一生命周期中至关重要的终点站。它是一个实例方法,当对象的引用计数降为零时,系统

      理解 native2ascii:Java 国际化开发中的字符编码工具
      理解 native2ascii:Java 国际化开发中的字符编码工具

      native2ascii 工具的基本定位在Ja va应用程序的国际化与本地化开发过程中,处理非拉丁字符集是一个常见且关键的环节。Ja va内部使用Unicode字符集来统一表示全球各种语言的文字,但其属性文件(.properties)在历史上要求使用ASCII编码,或者更准确地说,要求非ASCII字

      如何使用 native2ascii 转换中文字符为 Unicode 转义序列
      如何使用 native2ascii 转换中文字符为 Unicode 转义序列

      理解 native2ascii 工具的基本用途在软件开发,特别是涉及国际化处理的场景中,开发者常常需要处理不同编码的文本资源。native2ascii 是 Ja va 开发工具包(JDK)中提供的一个命令行实用程序,其主要功能是将包含本地字符编码(非ASCII字符)的文件,转换为包含 Unicode

      Java native2ascii 命令详解:解决属性文件乱码问题
      Java native2ascii 命令详解:解决属性文件乱码问题

      native2ascii 命令的由来与作用在Ja va开发中,处理国际化资源文件是一个常见需求。资源文件通常以.properties格式存储,用于支持多语言界面。然而,Ja va属性文件默认采用ISO-8859-1字符集编码,这导致了一个直接的问题:当文件中包含非拉丁字符(如中文、日文、韩文等)时,

      一个 memwatch 实战案例:定位野指针问题
      一个 memwatch 实战案例:定位野指针问题

      内存监控工具的价值与挑战在软件开发,尤其是使用C/C++这类手动管理内存的语言时,内存错误是程序员最常遭遇的难题之一。其中,野指针问题因其隐蔽性和破坏性,往往成为最难定位的“幽灵”缺陷。它可能潜伏在代码中,在特定条件下才被触发,导致程序崩溃、数据损坏或难以预测的行为。传统的调试手段,如打印日志或使用

      查看更多
      精品专题 更多
      装机必备
      装机必备

      正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

      Windows
      Windows

      正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

      macOS软件
      macOS软件

      正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

      Mac软件 更多
      灵活计算器
      灵活计算器
      macOS/iOS/Android

      灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

      赤友清理大师
      赤友清理大师
      macOS

      赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

      极度公式
      极度公式
      Windows/macOS/Linux

      极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

      WINDOWS 更多
      Windows 10
      Windows 10
      Windows

      Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

      极度公式
      极度公式
      Windows/macOS/Linux

      极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

      密码键盘
      密码键盘
      Windows/macOS/iOS/Android

      密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

      网站备案号:苏ICP备2026018738号-1 联系邮箱:bd@zhengruan.com 网站地图

      Copyright ©2018-2026