Redission 分布式锁原理分析

一、前言
我们先来说说分布式锁,为啥要有分布式锁呢? 像 JDK 提供的 synchronized、Lock 等实现锁不香吗?这是因为在单进程情况下,多个线程访问同一资源,可以使用 synchronized 和 Lock 实现;在多进程情况下,也就是分布式情况,对同一资源的并发请求,需要使用分布式锁实现。而 Redisson 组件可以实现 Redis 的分布式锁,同样 Redisson 也是 Redis 官方推荐分布式锁实现方案,封装好了让用户实现分布式锁更加的方便与简洁。

二、分布式锁的特性
互斥性

任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。

同一性

锁只能被持有该锁的客户端删除,不能由其它客户端删除。

可重入性

持有某个锁的客户端可继续对该锁加锁,实现锁的续租。

容错性

锁失效后(超过生命周期)自动释放锁(key失效),其他客户端可以继续获得该锁,防止死锁。

三、Redisson 分布式锁原理
下面我们从加锁机制、锁互斥机制、锁续期机制、可重入加锁机制、锁释放机制等五个方面对 Redisson 分布式锁原理进行分析。

3.0 整体分析

注:redisson 版本 3.24.4-SNAPSHOT

public class RedissonLockTest {public static void main(String[] args) {Config config = new Config();config.useSingleServer().setPassword("admin").setAddress("redis://127.0.0.1:6379");RedissonClient redisson = Redisson.create(config);RLock lock = redisson.getLock("myLock");try {lock.lock();// 业务逻辑} finally {lock.unlock();}}
}

初始化 RedissonLock
在这里插入图片描述

/*** 加锁方法** @param leaseTime 加锁到期时间(-1:使用默认值 30 秒)* @param unit 时间单位* @param interruptibly 是否可被中断标识* @throws InterruptedException*/
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {// 获取当前线程IDlong threadId = Thread.currentThread().getId();// 尝试获取锁(重点)Long ttl = tryAcquire(-1, leaseTime, unit, threadId);// lock acquired// 成功获取锁, 过期时间为空。if (ttl == null) {return;}// 订阅分布式锁, 解锁时进行通知。CompletableFuture<RedissonLockEntry> future = subscribe(threadId);pubSub.timeout(future);RedissonLockEntry entry;if (interruptibly) {entry = commandExecutor.getInterrupted(future);} else {entry = commandExecutor.get(future);}try {while (true) {// 再次尝试获取锁ttl = tryAcquire(-1, leaseTime, unit, threadId);// lock acquired// 成功获取锁, 过期时间为空, 成功返回。if (ttl == null) {break;}// waiting for message// 锁过期时间如果大于零, 则进行带过期时间的阻塞获取。if (ttl >= 0) {try {// 获取不到锁会在这里进行阻塞, Semaphore, 解锁时释放信号量通知。entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {if (interruptibly) {throw e;}entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);}} else {// 锁过期时间小于零, 则死等, 区分可中断及不可中断。if (interruptibly) {entry.getLatch().acquire();} else {entry.getLatch().acquireUninterruptibly();}}}} finally {// 取消订阅unsubscribe(entry, threadId);}
}

在这里插入图片描述
当锁超时时间为 -1 时,而且获取锁成功时,会启动看门狗定时任务自动续锁:
在这里插入图片描述
在这里插入图片描述
每次续锁都要判断锁是否已经被释放,如果锁续期成功,自己再次调度自己,持续续锁操作。

为了保证原子性,用 lua 实现的原子性加锁操作,见 3.1 加锁机制。

3.1 加锁机制

加锁机制的核心就是这段,将 Lua 脚本被 Redisoon 包装最后通过 Netty 进行传输。

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {/*** // 1* KEYS[1] 代表上面的 myLock* 判断 KEYS[1] 是否存在, 存在返回 1, 不存在返回 0。* 当 KEYS[1] == 0 时代表当前没有锁* // 2* 查找 KEYS[1] 中 key ARGV[2] 是否存在, 存在回返回 1* // 3* 使用 hincrby 命令发现 KEYS[1] 不存在并新建一个 hash* ARGV[2] 就作为 hash 的第一个key, val 为 1* 相当于执行了 hincrby myLock 91089b45... 1* // 4* 设置 KEYS[1] 过期时间, 单位毫秒* // 5* 返回 KEYS[1] 过期时间, 单位毫秒*/return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,"if ((redis.call('exists', KEYS[1]) == 0) " + // 1"or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " + // 2"redis.call('hincrby', KEYS[1], ARGV[2], 1); " + // 3"redis.call('pexpire', KEYS[1], ARGV[1]); " + // 4"return nil; " +"end; " +"return redis.call('pttl', KEYS[1]);", // 5Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

断点走一波就很清晰了:
在这里插入图片描述
KEYS[1]) :加锁的key ARGV[1] :key的生存时间,默认为30秒 ARGV[2] :加锁的客户端ID (UUID.randomUUID()) + “:” + threadId)
在这里插入图片描述
上面这一段加锁的 lua 脚本的作用是:第一段 if 判断语句,就是用 exists myLock 命令判断一下,如果你要加锁的那个锁 key 不存在的话(第一次加锁)或者该 key 的 field 存在(可重入锁),你就进行加锁。如何加锁呢?使用 hincrby 命令设置一个 hash 结构,类似于在 Redis 中使用下面的操作:
在这里插入图片描述
整个 Lua 脚本加锁的流程画图如下:
在这里插入图片描述
可以看出,最新版本的逻辑比之前的版本更简单清晰了。

3.2 锁互斥机制

此时,如果客户端 2 来尝试加锁,会如何呢?首先,第一个 if 判断会执行 exists myLock,发现 myLock 这个锁 key 已经存在了。接着第二个 if 判断,判断一下,myLock 锁 key 的 hash 数据结构中,是否包含客户端 2 的 ID,这里明显不是,因为那里包含的是客户端 1 的 ID。所以,客户端 2 会执行:

return redis.call('pttl', KEYS[1]);

返回的一个数字,这个数字代表了 myLock 这个锁 key 的剩余生存时间。

锁互斥机制主流程其实在 3.0 整体分析 里有讲,具体可以看这个 org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit, boolean) 方法。

3.3 锁续期机制

客户端 1 加锁的锁 key 默认生存时间是 30 秒,如果超过了 30 秒,客户端 1 还想一直持有这把锁,怎么办呢?

Redisson 提供了一个续期机制, 只要客户端 1 一旦加锁成功,就会启动一个 Watch Dog。

3.4 可重入加锁机制
在这里插入图片描述
在这里插入图片描述
Watch Dog 机制其实就是一个后台定时任务线程,获取锁成功之后,会将持有锁的线程放入到一个 RedissonBaseLock.EXPIRATION_RENEWAL_MAP 里面,然后每隔 10 秒 (internalLockLeaseTime / 3) 检查一下,如果客户端 1 还持有锁 key(判断客户端是否还持有 key,其实就是遍历 EXPIRATION_RENEWAL_MAP 里面线程 id 然后根据线程 id 去 Redis 中查,如果存在就会延长 key 的时间),那么就会不断的延长锁 key 的生存时间。

注:

如果服务宕机了,Watch Dog 机制线程也就没有了,此时就不会延长 key 的过期时间,到了 30s 之后就会自动过期了,其他线程就可以获取到锁。
如果调用带过期时间的 lock 方法,则不会启动看门狗任务去自动续期。

3.5 锁释放机制
在这里插入图片描述

// 判断 KEYS[1] 中是否存在 ARGV[3]
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +"return nil;" +
"end; " +
// 将 KEYS[1] 中 ARGV[3] Val - 1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 如果返回大于0 证明是一把重入锁
"if (counter > 0) then " +// 重置过期时间"redis.call('pexpire', KEYS[1], ARGV[2]); " +"return 0; " +
"else " +// 删除 KEYS[1]"redis.call('del', KEYS[1]); " +// 通知阻塞等待线程或进程资源可用"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1; " +
"end; " +
"return nil;"

KEYS[1]: myLock KEYS[2]: redisson_lock_channel:{myLock} ARGV[1]: 0 ARGV[2]: 30000 (过期时间) ARGV[3]: 66a84a47-3960-4f3e-8ed7-ea2c1061e4cf:1 (Hash 中的锁 field)

同理,锁释放断点走一波:
在这里插入图片描述
锁释放机制小结一下:

删除锁(这里注意可重入锁)
广播释放锁的消息,通知阻塞等待的进程(向通道名为 redisson_lock__channel:{myLock} publish 一条 UNLOCK_MESSAGE 信息)
取消 Watch Dog 机制,即将 RedissonLock.EXPIRATION_RENEWAL_MAP 里面的线程 id 删除,并且 cancel 掉 Netty 的那个定时任务线程。
四、主从 Redis 架构中分布式锁存在的问题
线程A从主redis中请求一个分布式锁,获取锁成功;
从redis准备从主redis同步锁相关信息时,主redis突然发生宕机,锁丢失了;
触发从redis升级为新的主redis;
线程B从继任主redis的从redis上申请一个分布式锁,此时也能获取锁成功;
导致,同一个分布式锁,被两个客户端同时获取,没有保证独占使用特性;
为了解决这个问题,redis引入了红锁的概念。

需要准备多台redis实例,这些redis实例指的是完全互相独立的Redis节点,这些节点之间既没有主从,也没有集群关系。客户端申请分布式锁的时候,需要向所有的redis实例发出申请,只有超过半数的redis实例报告获取锁成功,才能算真正获取到锁。跟大多数保证一致性的算法类似,就是多数原理。

public static void main(String[] args) {String lockKey = "myLock";Config config = new Config();config.useSingleServer().setPassword("123456").setAddress("redis://127.0.0.1:6379");Config config2 = new Config();config.useSingleServer().setPassword("123456").setAddress("redis://127.0.0.1:6380");Config config3 = new Config();config.useSingleServer().setPassword("123456").setAddress("redis://127.0.0.1:6381");RLock lock = Redisson.create(config).getLock(lockKey);RLock lock2 = Redisson.create(config2).getLock(lockKey);RLock lock3 = Redisson.create(config3).getLock(lockKey);RedissonRedLock redLock = new RedissonRedLock(lock, lock2, lock3);try {redLock.lock();} finally {redLock.unlock();}
}

当然, 对于 Redlock 算法不是没有质疑声,两位大神前几年吵的沸沸腾腾,大家感兴趣的可以去 Redis 官网查看Martin Kleppmann 与 Redis 作者Antirez 的辩论。

额,想收一收了,再讲下去感觉要绕不开分布式经典问题 CAP了。

五、分布式锁选型
鱼和熊掌不可兼得,如果你想强一致性的话可以选择 ZK 的分布式锁,但 ZK 的话性能就会有一定的下降,如果项目没有用到 ZK 的话,那就选择 Redis 的分布式锁吧,比较你为了那极小的概率而丢去性能以及引入一个组件很不划算,如果无法忍受 Redis 的红锁缺陷,那自己在业务中自己保证吧。

下面是常见的几种分布式锁选型对比:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/571727.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

电容笔可以用什么替代?五款性价比高的手写笔推荐,入手不亏!

作为一位拥有多年数码产品测评经验的专业评测师&#xff0c;我不仅使用过原装笔&#xff0c;还尝试了许多平替电容笔&#xff0c;其数量不下十款。在这个过程中&#xff0c;我见过许多朋友由于选购不当而遭遇书写效率低下、体验感差等问题。这并非言过其实&#xff0c;因为非专…

【收集】好玩的技术

文章目录 IP-Adapter-FaceID IP-Adapter-FaceID https://huggingface.co/h94/IP-Adapter-FaceID

文献速递:基于SAM的医学图像分割--SAMUS:适应临床友好型和泛化的超声图像分割的Segment Anything模型

Title 题目 SAMUS: Adapting Segment Anything Model for Clinically-Friendly and Generalizable Ultrasound Image Segmentation SAMUS&#xff1a;适应临床友好型和泛化的超声图像分割的Segment Anything模型 01 文献速递介绍 医学图像分割是一项关键技术&#xff0c;用…

【Java程序设计】【C00343】基于Springboot的船运物流管理系统(有论文)

基于Springboot的船运物流管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 项目获取 &#x1f345;文末点击卡片获取源码&#x1f345; 开发环境 运行环境&#xff1a;推荐jdk1.8&#xff1b; 开发工具&#xff1a;eclipse以及i…

新手如何去做性能测试?

1、性能测试是什么&#xff1f; 一句话概括&#xff1a;不断的通过不同场景的系统表现去探究系统设计与资源消耗之间的平衡。 具体一点&#xff1a;通过在测试环境下对系统或构件的性能进行探测&#xff0c;用以验证在生产环境下系统性能是否达到预估的性能需求&#xff0c;发…

渗透测试——分享几个我常用渗透网站

前言 经常有小伙伴问我常用的渗透网站有哪些&#xff0c;花点时间整理出来&#xff0c;废话不多说&#xff0c;直接上网站。 一、雨苁 雨苁 这个网站从我接触网络安全一直在用&#xff0c;里面有几个很有用的工具如&#xff1a;在线地图情报搜集&#xff0c;开源情报信息收集…

Redis修改开源协议,6大备胎重见天日

背景&#xff1a;Redis2018年以来修改了多次开源协议&#xff0c;以前是把一些高级功能收费&#xff0c;这次彻底怒了&#xff0c;把核心代码的协议修改为RSALv2和SSPL双重协议&#xff0c;这个修改对普通用户不受影响&#xff0c;是向所有云厂商开炮&#xff0c;以后云厂商将不…

Linux 系统Centos7.0记录安装Docker和安装jdk环境完整教程(建议收藏备用)

Linux 系统Centos7.0记录安装Docker和安装jdk环境完整教程&#xff08;建议收藏备用&#xff09; 一、安装前准备工作 1.1 查看服务器系统版本以及内核版本 cat /etc/redhat-release1.2 查看服务器内核版本 uname -r这里我们使用的是CentOS 7.9 系统&#xff0c;内核版本为…

详解 WebWorker 的概念、使用场景、示例

前言 提到 WebWorker&#xff0c;可能有些小伙伴比较陌生&#xff0c;不知道是做什么的&#xff0c;甚至不知道使用场景&#xff0c;今天这篇文章就带大家一起简单了解一下什么是 webworker&#xff01; 概念 WebWorker 实际上是运行在浏览器后台的一个单独的线程&#xff0…

eNSP ppp验证实验

1、R1和R2使用PPP链路直连&#xff0c;R2和R3把2条PPP链路捆绑为PPP MP直连 2、按照图示配置IP地址 3、R2对R1的PPP进行单向chap验证 4、R2和R3的PPP进行双向chap验证 实验步骤&#xff1a; R1配置&#xff1a; #修改名称 <Huawei>sys Enter system view, return u…

javascript基础代码练习之超速罚款

<!DOCTYPE html> <html> <head><meta charset"UTF-8" /><title>Nauji</title> </head> <body><script>function punish(speed) {const limit 120;if (speed < limit) {return 未超速;} else if (speed &l…

【二叉树】Leetcode 104. 二叉树的最大深度【简单】

二叉树的最大深度 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例1 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;3 解题思路 二叉树的最大深度可以通过递归方式来求解…