一文看懂Redisson分布式锁的Watchdog机制源码实现
wptr33 2024-12-22 21:12 54 浏览
一、 分布式锁简介
分布式锁是一种常见的协调分布式系统的机制,在分布式环境下保证数据的一致性和可用性。分布式锁的实现有很多种方式,其中较为常见的方式是利用Redis实现分布式锁。
在使用 Redis 实现分布式锁时,我们通常使用 SET key value [EX seconds] [NX] 命令来给某个 key 设置一个具有过期时间的值作为锁。其中 EX 参数表示设置过期时间,当 Redis 客户端连接断开或者达到过期时间时,锁会自动失效。但是,在一些特殊情况下,由于网络波动等原因,我们可能无法及时续期更新锁的过期时间,这会导致锁在没有被显式释放的情况下过期,从而引发并发问题。
Redisson 的看门狗就是为了解决这个问题而设计的。它会在获取锁之后启动一个后台任务定期地对锁进行“续期”,即更新锁的过期时间。具体来说,每次启动续期任务时,会通过 set(key, value, XX, PX, ttl) 命令更新锁的过期时间,同时记录该任务与锁的对应关系。在锁释放或过期时,会取消相应的续期任务,从而保证锁的有效性。
二、 Redisson分布式锁的看门狗源码分析
Redisson 的 RedissonLock 类主要通过下面五个方法实现Watchdog机制:
- tryAcquireAsync
- scheduleExpirationRenewal
- renewExpiration
- renewExpirationAsync
- cancelExpirationRenewal
下面讲解各个方法的源码片段
1. tryAcquireAsync
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
// 如果设置了超时时间,调用 tryLockInnerAsync 方法尝试加锁并设置过期时间。
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
// 否则仅获取锁,并返回剩余时间。
// 计算剩余过期时间
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
// 当获取剩余过期时间的异步操作完成后,判断是否获取到锁,如果获取到,开始定时任务自动续期
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// 如果返回值为 null 表示已经拥有锁
if (ttlRemaining == null) {
// 开始定期检查锁是否过期,如果没有过期则续期
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
Redisson在lock方法执行时,会调用tryAcquireAsync方法获取锁,在获取到锁时,上面示例代码种会调用scheduleExpirationRenewal(threadId)方法,开启定时检查是否过期和自动续期的定时任务,这里其实就是看门狗机制的创建点。
2. scheduleExpirationRenewal
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
// 在 EXPIRATION_RENEWAL_MAP 中添加一个新的键值对,如果该键名已存在,则将 threadId 添加到该键名的队列中。
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else { // 如果该键名不存在,则添加一个新的键值对,并执行 renewExpiration() 方法续约该锁的过期时间。
entry.addThreadId(threadId);
renewExpiration();
}
}
当一个线程获取到锁时,会调用 scheduleExpirationRenewal() 方法向 EXPIRATION_RENEWAL_MAP (ConcurrentHashMap 对象)中添加一个新的键值对,如果该键名已存在,则将 threadId 添加到该键名的队列中;否则,添加一个新的键值对,并调用 renewExpiration() 方法续约该锁的过期时间。
3. renewExpiration
private void renewExpiration() {
// 从 EXPIRATION_RENEWAL_MAP 中查找当前锁实例,并获取相应的 ExpirationEntry 对象。
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 创建一个定时任务(Timeout),在线程持有该锁时执行续期操作。
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<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
// 如果续期成功,则重新安排定时任务
if (res) {
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 定时任务的间隔为锁到期时间的三分之一
// 将该定时任务存储在 ExpirationEntry 实体中,方便后续处理。
ee.setTimeout(task);
}
在该方法内部,首先通过 getEntryName() 获取当前锁实例的名称,然后从 EXPIRATION_RENEWAL_MAP 中查找该名称对应的 ExpirationEntry 对象。如果对象不存在,则直接返回即可;否则,创建一个新的定时任务,该任务会在 internalLockLeaseTime / 3 毫秒后执行,并尝试异步更新锁的过期时间。如果更新成功,则会再次调用 renewExpiration() 方法,以便持续延长锁的过期时间。这个定时任务通俗的讲就是所谓的看门狗。当然,这里更新过期时间的操作是通过调用 renewExpirationAsync() 实现的,它仍然是一个异步操作。
4. renewExpirationAsync
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
// 使用 evalWriteAsync 方法执行 EVAL 命令
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// EVAL 命令的脚本,根据给定的键和参数进行判断和更新
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 如果指定的键和参数都存在,则续约该键的过期时间
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
// 并返回操作成功的标志
"return 1; " +
"end; " +
// 如果指定的键和参数不匹配,则返回操作失败的标志
"return 0;",
// EVAL 命令中用到的键
Collections.<Object>singletonList(getName()),
// 续约的过期时间,在 Redisson 中为 internalLockLeaseTime
internalLockLeaseTime,
// 获取锁的名称,由线程 ID 和当前 Redisson 实例 ID 组成
getLockName(threadId)
);
}
此方法主要是用于在获取分布式锁的情况下,对锁的过期时间进行续约的操作。其中,RedisCommands.EVAL_BOOLEAN 代表执行 EVAL 命令后返回的数据类型为 Boolean 类型;getKey() 方法用于获取锁的名称,该名称由锁的前缀和锁的 ID 组成;getLockName(threadId) 方法用于获取当前线程获取锁后的锁名称,之所以要使用当前 Redisson 实例 ID 与线程 ID 组合作为锁名称,是为了确保在多个 Redisson 实例下,所有线程都能够正确地获取到锁名并正确地执行续约操作。
5.cancelExpirationRenewal
void cancelExpirationRenewal(Long threadId) {
// 从 EXPIRATION_RENEWAL_MAP 中查找当前锁实例,并获取相应的 ExpirationEntry 对象。
ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (task == null) {
return;
}
// 如果传入了调用者线程 ID,则从任务中移除该线程 ID。
if (threadId != null) {
task.removeThreadId(threadId);
}
// 如果线程 ID 为空,或者任务已经没有任何线程在持有它了,则取消任务并从 EXPIRATION_RENEWAL_MAP 中删除该实体。
if (threadId == null || task.hasNoThreads()) {
Timeout timeout = task.getTimeout();
if (timeout != null) {
timeout.cancel(); // 取消定时任务
}
EXPIRATION_RENEWAL_MAP.remove(getEntryName()); // 从 EXPIRATION_RENEWAL_MAP 中删除实体
}
}
这个方法主要是用于取消延长锁过期时间的定时任务。当一个线程在unlock释放锁时,便会调用这个方法。在该方法内部,首先通过 getEntryName() 获取当前锁实例的名称,然后从 EXPIRATION_RENEWAL_MAP 中查找该名称对应的 ExpirationEntry 对象。如果对象不存在,则直接返回即可;否则,如果传入了调用者线程 ID,则从任务中移除该线程 ID。接着,如果线程 ID 为空,或者任务已经没有任何线程在持有它了,则取消任务并从 EXPIRATION_RENEWAL_MAP 中删除该实体。需要注意的是,取消定时任务的操作是通过调用 timeout.cancel() 实现的,它会将定时任务从时间轮中移除。由于 Redisson 是基于 Netty 的,所以它使用的是 HashedWheelTimer,这个定时器底层是基于时间轮实现的,并且支持动态添加和删除定时任务。
三、 使用注意事项
使用Redisson分布式锁的看门狗应注意以下几个问题:
- 设置合理的锁超时时间:如果锁的超时时间过短,则会导致频繁续命和多次加锁解锁,影响程序性能;如果锁的超时时间过长,则可能会因为某些异常原因使得锁无法释放,从而导致死锁。
- 在业务逻辑完成后及时启动看门狗:如果业务逻辑执行时间过长,则有可能导致锁的过期,从而使看门狗失去续命的意义。
- 合理配置看门狗的参数:看门狗的续命时间间隔应该在锁的过期时间内,且重试次数不宜过多,以免影响程序性能。
- 避免锁的嵌套使用:锁的嵌套使用有可能导致死锁或者其他并发问题,应避免使用。
相关推荐
- Linux文件系统操作常用命令(linux文件内容操作命令)
-
在Linux系统中,有一些常用的文件系统操作命令,以下是这些命令的介绍和作用:#切换目录,其中./代表当前目录,../代表上一级目录cd#查看当前目录里的文件和文件夹ls#...
- 别小看tail 命令,它难倒了技术总监
-
我把自己以往的文章汇总成为了Github,欢迎各位大佬star...
- lnav:基于 Linux 的高级控制台日志文件查看器
-
lnav是一款开源的控制台日志文件查看器,专为Linux和Unix-like系统设计。它通过自动检测日志文件的格式,提取时间戳、日志级别等关键信息,并将多个日志文件的内容按时间顺序合并显示,...
- 声明式与命令式代码(声明模式和命令模式)
-
编程范式中的术语和差异信不信由你,你可能已经以开发人员的身份使用了多种编程范例。因为没有什么比用编程理论招待朋友更有趣的了,所以这篇文章可以帮助您认识代码中的流行范例。命令式编程命令式编程是我们从As...
- linux中的常用命令(linux常用命令和作用)
-
linux中的常用命令linux中的命令统称shell命令shell是一个命令行解释器,将用户命令解析为操作系统所能理解的指令,实现用户与操作系统的交互shell终端:我们平时输入命令,执行程序的那个...
- 提高工作效率的--Linux常用命令,能够决解95%以上的问题
-
点击上方关注,第一时间接受干货转发,点赞,收藏,不如一次关注评论区第一条注意查看回复:Linux命令获取linux常用命令大全pdf+Linux命令行大全pdf...
- 如何限制他人操作自己的电脑?(如何控制别人的电脑不让发现)
-
这段时间,小猪罗志祥正处于风口浪尖,具体是为啥?还不知道的小伙伴赶紧去补一下最近的娱乐圈八卦~简单来说,就是我们的小罗同事,以自己超强的体力,以及超强的时间管理能力,重新定义了「多人运动」的含义,重新...
- 最通俗易懂的命令模式讲解(命令模式百科)
-
我们先不讲什么是命令模式,先通过一个场景来引出命令模式,看看命令模式能解决什么样的问题。现在有一个渣男张三,他有还几个女朋友,你现在是不是还是单身狗,你就说你气不气?然后他需要每天分别叫几个女朋友起床...
- 互联网大厂后端必看!Spring Boot 中Runtime执行与停止命令?
-
你是否曾在使用SpringBoot开发项目时,遇到需要执行系统命令的场景?比如调用脚本进行文件处理,又或是启动外部程序?很多后端开发人员会使用Processexec=Runtime.get...
- Linux 常用命令(linux常用的20个命令面试)
-
日志排查类操作命令...
- Java字节码指令:if_icmpgt(0xA3)(java字节码使用的汇编语言)
-
if_icmpgt是Java字节码中的一条条件跳转指令,其全称是"IfIntegerCompareGreaterThan"。它用于比较两个整数值的大小。如果栈顶的第一个...
- 外贸干货|如何增加领英的曝光量和询盘
-
#跨境电商#...
- golang执行linux命令(golang调用shell脚本)
-
需求需要通过openssl生成rsa秘钥,然后保存该秘钥。代码实例packagemainimport("io/ioutil""bytes"&...
- LINUX磁盘挂载(linux磁盘挂载到windows)
-
1、使用root用户查看磁盘挂载情况:fdisk-l2、使用df查看当前磁盘挂载情况,根据和fdisk-l的结果进行对比,查看还有那些磁盘未使用3、挂载:mount磁盘挂载路径...
- Linux命令学习——nl命令(linux ln命令的使用)
-
nl命令主要功能为每一个文件添加行号,每一个输入的文件添加行号后发送到标准输出。当没有文件或文件为-时,读取标准输入...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
面试官:git pull是哪两个指令的组合?
-
git 执行pull错误如何撤销 git pull fail
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)