详细分析Redisson分布式锁中的renewExpiration()方法

server/2024/10/18 15:50:16/

目录

Redisson%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E7%9A%84%E7%BB%AD%E6%9C%9F-toc" style="margin-left:0px;">一、Redisson分布式的续期

整体分析

具体步骤和逻辑分析

为什么需要递归调用?

定时任务的生命周期?


Redisson%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E7%9A%84%E7%BB%AD%E6%9C%9F">一、Redisson分布式的续期

Redisson是一个基于Redis的Java分布式实现。它允许多个进程或线程之间安全地共享资源。为了实现这一点,Redisson使用了一种基于分布式系统的机制,其中的持有者在操作过程中需要维护的有效性。

关于Redisson分布式的详细介绍,可移步到我的另一篇博客Redisson分布式-CSDN博客

Redisson中,的续期是一个关键特性,用于确保在的持有者仍在执行任务期间,不会被意外释放。

整体分析

的续期机制在Redisson中是自动管理的,的续期是基于一个定时任务的机制,定期检查的状态并决定是否需要续期。具体实现为:

java">private void renewExpiration() {// 1、首先会从EXPIRATION_RENEWAL_MAP中获取一个值,如果为null说明可能已经被释放或过期,因此不需要进行续期,直接返回ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}// 2、基于TimerTask实现一个定时任务,设置internalLockLeaseTime / 3的时长进行一次续期,也就是每10s进行一次续期。Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {// 从EXPIRATION_RENEWAL_MAP里获取一个值,检查是否被释放ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());// 如果为null则说明也被释放了,不需要续期if (ent == null) {return;}// 如果不为null,则获取第一个thread(也就是持有的线程)Long threadId = ent.getFirstThreadId();if (threadId == null) {return;}// 如果threadId 不为null,说明需要续期,它会异步调用renewExpirationAsync(threadId)方法来实现续期RFuture<Boolean> future = renewExpirationAsync(threadId);// 处理结果future.onComplete((res, e) -> {// 如果有异常if (e != null) {log.error("Can't update lock " + getName() + " expiration", e);return;}// 如果续期成功,则会重新调用renewExpiration()方法进行下一次续期if (res) {// reschedule itselfrenewExpiration();}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}

具体步骤和逻辑分析

java">ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}

首先,从 EXPIRATION_RENEWAL_MAP 中获取当前的 ExpirationEntry 对象。如果该对象为null,说明可能已经被释放或过期,因此不需要进行续期,直接返回。

java">Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {...}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

如果当前的 ExpirationEntry 对象不是null,就会继续往下执行,创建一个定时任务。这个定时任务的代码实现了一个的续期机制,具体步骤和逻辑分析如下:

在代码中,定时任务是通过 commandExecutor.getConnectionManager().newTimeout(...) 方法创建的,该任务的延迟时间设置为 internalLockLeaseTime / 3 毫秒,即每次续期的时间间隔。

java">ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {return;
}

在定时任务的 run 方法中,首先尝试从 EXPIRATION_RENEWAL_MAP 中获取与当前对应的 ExpirationEntry 实例。如果获取到的 ExpirationEntry 为 null,则说明已经被释放,此时无需续期,直接返回。

java">Long threadId = ent.getFirstThreadId();
if (threadId == null) {return;
}

如果获取到的 ExpirationEntry 不为 null,说明如果仍然有效,继续往下走,接下来获取持有该的线程 ID。如果 threadId 为 null,也说明可能已经被释放,直接返回。

java">RFuture<Boolean> future = renewExpirationAsync(threadId);

如果持有的线程 ID 不为 null,继续往下走,则调用 renewExpirationAsync(threadId) 方法异步续期的有效期。

继续进入这个renewExpirationAsync()方法,可以看到,方法的主要功能是延长的有效期。下面是对这段代码的详细分析:

java">protected RFuture<Boolean> renewExpirationAsync(long threadId) {return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return 1; " +"end; " +"return 0;",Collections.singletonList(getName()),internalLockLeaseTime, getLockName(threadId));}

renewExpiration()函数内部的RFuture<Boolean> future = renewExpirationAsync(threadId);又是一个关键的函数,跳入renewExpirationAsync(threadId)内部一探究竟。

  • 返回类型RFuture<Boolean> 表示该方法返回一个表示异步操作结果的未来对象,最终会得到一个布尔值,指示续期操作是否成功。
  • 参数long threadId 是持有的线程 ID,用于标识当前续期操作是否适用于该线程。

这个renewExpirationAsync()是一个异步刷新有效期的函数,它主要是用evaLWriteAsync()方法来异步执行一段Lua脚本,重置当前threadId线程持有的的有效期。也就是说该方法负责执行给定的Lua脚本,以实现分布式的续期

  • KEYS[1]:代表的名称,即 Redis 键。
  • ARGV[1]:引用传入的第一个非键参数,表示希望设置的新过期时间(毫秒),的默认租约时间为internalLockLeaseTime。
  • ARGV[2]:引用传入的第二个非键参数,表示通过getLockName(threadId)根据线程ID生成特定的标识符,确保操作的是特定线程的。简单说就是持有的线程id
  • getName():获取当前的名称,用于作为Redis中的键。
  • LongCodec.INSTANCE:编码器,指示如何处理数据的序列化与反序列化。
  • RedisCommands.EVAL_BOOLEAN:表示执行的命令类型,这里是执行一个返回布尔值的Lua脚本。

Lua脚本中,首先执行redis.call('hexists', KEYS[1], ARGV[2]) == 1,该命令检查的名称KEYS[1]下是否存在持有该的线程ID(ARGV[1])。如果存在,说明该线程仍然是的持有者,则调用pexpire命令redis.call('pexpire', KEYS[1], ARGV[1])更新的过期时间。如果续期成功,返回1,否则返回0。

因此,Lua脚本中的整体逻辑是如果当前key存在,说明当前还被该线程持有,那么就重置过期时间为30s,并返回true表示续期成功,反之返回false。

这段代码的设计充分利用了Redis的Lua脚本特性,实现了高效且原子化的续期逻辑,减少了并发操作中的 race condition 问题,同时提供了异步执行的能力,提升了系统的响应性和性能。

然后,我们退回到renewExpiration()方法中,继续往下走,

java">future.onComplete((res, e) -> {if (e != null) {log.error("Can't update lock " + getName() + " expiration", e);return;}if (res) {renewExpiration();}
});

通过 onComplete 方法处理续期操作的结果,如果e 不为 null,说明有异常则记录错误日志。如果res 为 true,说明续期成功则调用 renewExpiration() 方法,安排下一次的续期操作。

总结一下,整体流程就是,在代码中,定时任务是通过 commandExecutor.getConnectionManager().newTimeout(...) 方法创建的。该任务会在指定的时间(internalLockLeaseTime / 3 毫秒)后执行一次。每当任务执行时,都会检查当前的状态,并尝试续期。如果需要续期(即仍然有效),则会调用 renewExpiration() 方法。

为什么需要递归调用?

的实现中,为了确保在持有者处理任务期间保持有效,通常会设置一个有效期(lease time)。在有效期内,如果持有的线程仍然在执行任务,那么它需要定期续期,以防止在任务完成前过期,从而导致其他线程获取

递归调用的机制:在 run 方法的最后,如果续期成功,调用 renewExpiration() 方法。这通常意味着该方法会重新安排另一个定时任务,相当于在每次续期后再次创建一个新的定时任务,使得续期操作可以持续进行。这种递归调用的方式确保了只要仍然被持有,续期操作就会不断地被调度,从而保持的有效性。

定时任务的生命周期?

每个定时任务的生命周期是短暂的,完成一次 run 方法的执行后,该任务就结束了。然后,通过递归调用,可能会创建新的定时任务,从而继续续期。

(1)任务通过 newTimeout 被创建,并且首次执行会在 internalLockLeaseTime / 3 毫秒后触发。这个时间间隔确保了任务在的生命周期的早期进行检查和续期。此时,任务进入其生命周期,准备执行。

(2)当定时任务第一次执行时,run() 方法被调用。它主要的任务是:

  1. 从 EXPIRATION_RENEWAL_MAP 获取的状态。
  2. 如果被释放(ent == null),任务直接返回,不再进行续期。
  3. 如果仍然存在并且当前线程持有threadId != null),则异步调用 renewExpirationAsync(threadId) 来续期
  4. 在续期的异步任务完成后,如果续期成功(res == true),会重新调用 renewExpiration() 进行下一次续期。

(3)续期条件:如果任务成功续期,它会在异步任务的 onComplete 回调中再次调用 renewExpiration() 方法。renewExpiration() 负责创建一个新的定时任务,这意味着每次任务续期成功后,系统会重新调度一个新的定时任务,以确保的有效期能够持续。

这个 renewExpiration() 方法的调用实际上是递归调用新的定时任务,续期继续进行下去。每次任务执行后,都可能会创建一个新的任务,直到被释放。

(3)定时任务的生命周期可能在以下情况下终止:

  • 被释放:当 EXPIRATION_RENEWAL_MAP.get(getEntryName()) 返回 null,表示已经被释放,定时任务会停止续期,不再创建新的定时任务。
  • 无持有的线程:如果没有线程持有(即 threadId == null),任务也会停止续期。
  • 异步任务失败:如果续期的异步任务失败(例如网络问题、数据库问题等),则可能无法继续续期。不过在代码中,如果发生异常,它只会记录错误,并不会立即停止整个续期机制,但最终续期将会失败并终止。

定时任务的生命周期从它的创建开始,通过定期执行检查和续期,直到被释放或没有线程持有时,任务才会停止。每次续期成功后,新的定时任务会继续执行,确保的有效期在持线程存在时不会过期。

因此,虽然定时任务会被创建并执行,但它的执行是基于持状态的,只有在有效且持有者仍在执行任务的情况下才会持续进行续期。这个设计确保了资源的有效管理,避免不必要的续期操作。


http://www.ppmy.cn/server/131689.html

相关文章

网站服务器监控:Apache指标解读

监控易是一款专业的IT监控软件&#xff0c;能够实时监控各类IT资源和应用的状态&#xff0c;确保系统的稳定运行。在网站服务器监控中&#xff0c;Apache作为广泛使用的Web服务器软件&#xff0c;其性能和稳定性对于网站的正常运行至关重要。下面&#xff0c;我们将对监控易中A…

【自动化】Java Access Bridge 使用说明

【自动化】Java Access Bridge 使用说明 Java Access Bridge是一项在Microsoft Windows动态链接库(DLL)中公开Java Accessibility API的技术,使实现Java Accessibility API的 Java应用程序对Microsoft Windows系统上的辅助技术可见。 开启jab服务 1 、首先获取java版本信…

Python速成笔记——知识:GUI自动化控制鼠标

图形用户界面自动化(GUI自动化):通过程序控制其他应用,向它们发送虚拟的按键和鼠标点击事件,模拟用户在计算机前进行操作。 功能:自动化解决需要大量机械式点击鼠标或填写表格的任务。 模块:pyautogui安装:pip install --user pyautoguiGUI自动化程序中断 将鼠标指针滑…

python数据分析与可视化介绍

本文主要讲述了数据可视化的基础知识&#xff0c;包括什么是数据可视化&#xff0c;数据可视化应用以及Python可视化工具库。 什么是数据可视化 可视化是一种通过视觉的方式有效传达信息的技术。数据可视化旨在借助于图形化手段&#xff0c;将数据以视觉形式来呈现&#xff0c…

VScode连接服务器配置c、c++编程环境

在 VS Code 中配置远程服务器的 C/C 编程环境&#xff0c;可以使用 VS Code 的 Remote-SSH 扩展来通过 SSH 连接到远程服务器&#xff0c;并在服务器上编写、编译和调试 C/C 代码。 以下是详细的配置步骤&#xff1a; 1. 在本地机器上安装 VS Code 和扩展 安装 VS Code&#…

蓝桥杯备赛(c/c++)

排序 9. 实现选择排序 10. 实现插入排序 11. 实现快速排序 12. 实现归并排序 13. 实现基数排序 14. 合并排序数组

SketchUp Pro 2024 for Mac 3D建模 草图设计大师软件安装【保姆级教程,简单小白轻松上手】

Mac分享吧 文章目录 SketchUp Pro 3D建模 草图设计大师软件 安装完成&#xff0c;软件打开效果一、Mac中安装SketchUp Pro 3D建模 草图设计大师软件——v241️⃣&#xff1a;下载软件2️⃣&#xff1a;安装软件&#xff0c;将安装包从左侧拖入右侧文件夹中3️⃣&#xff1a;应…

文件工具类 - C#小函数类推荐

此文记录的是文件工具类。 /***文件工具类Austin Liu 刘恒辉Project Manager and Software DesignerE-Mail: lzhdim163.comBlog: http://lzhdim.cnblogs.comDate: 2024-01-15 15:18:00***/namespace Lzhdim.LPF.Utility {using System.Collections;using System.IO;/// <…