Redis 篇-深入了解分布式锁 Redisson 原理(可重入原理、可重试原理、主从一致性原理、解决超时锁失效)

ops/2024/11/13 9:24:00/

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

本章目录

        1.0 基于 Redis 实现的分布式锁存在的问题

        2.0 Redisson 功能概述

        3.0 Redisson 具体使用

        4.0 Redisson 可重入锁原理

        5.0 Redisson 锁重试原理

        6.0 Redisson WatchDog 机制

        6.1 Redisson 是如何解决超时释放问题的呢?

        7.0 Redisson MultiLock 原理

        7.1 Redisson 分布式锁是如何解决主从一致性问题的呢?


        1.0 基于 Redis 实现的分布式锁存在的问题

        首先,在之前基于 setnx 实现的分布式锁存在以下问题:

        1)不可重入:同一个线程无法多次获取同一把锁。

        2)不可重试:获取锁只尝试一次就返回 false ,没有重试机制。

        当然这个机制是可以自己在判断完有无获取锁之后,再来根据业务的需求进行手动添加代码。比如说,当业务需求是:需要重复尝试获取锁。则可以在判断获取锁失败之后,等待一段时间,再去获取锁即可。

        3)超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患。

        比如说,当业务阻塞时间较久,锁到了超时时间则会自动释放,那么其他线程就会有可能获取锁成功,这就出现了多个线程获取锁成功,从而导致线程安全问题。

        4)主从一致性:如果 Redis 提供了主从集群,主从同步延迟,当主机宕机时,如果未来得及同步到其他机器上,则就会出现多线程获取锁成功情况,从而导致线程安全问题。

        那么 Java 实现了解决以上问题的 Redisson 分布式服务类。

        2.0 Redisson 功能概述

        Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网络。它不仅提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务,其中包含了各种分布式锁的实现。

        Redisson 解决了不可重入问题、不可重试问题、超时释放问题、主从一致性问题。

        比如说,分布式锁的可重入锁、公平锁、联锁、红锁等等。

        3.0 Redisson 具体使用

        1)引入依赖

        <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version></dependency>

        2)配置 RedissonClient类

java">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;@Configuration
public class RedissonConfig {@Beanpublic RedissonClient client(){//配置类Config config = new Config();//添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址config.useSingleServer().setAddress("redis://8.152.162.159:6379").setPassword("****");//创建客户端return Redisson.create(config);}
}

        3)使用 RedissonClient类

java">    @AutowiredRedissonClient redissonClient;@Testvoid contextLoads() throws InterruptedException {//先获取锁对象,根据业务来锁定资源RLock lock = redissonClient.getLock("lock");//尝试获取锁//tryLock() 进行了重写,有无参、只有两个参数、有三个参数boolean b = lock.tryLock(1, TimeUnit.SECONDS);if (b){System.out.println("成功获取锁!");}else {System.out.println("获取锁失败!");}}

        先注入 RedissonClient 对象,根据 getLock("锁") 方法获取 RLock lock 锁对象,根据业务需要对资源进行锁定。  

        调用 lock 对象中的 tryLock() 方法来尝试获取锁,该方法进行了重写:

        1)boolean tryLock():当获取锁失败时,默认不等待,就是不重试获取锁,默认锁的超时时间为 30 秒。

        2)boolean tryLock(long time, TimeUnit unit):在 time 时间内会进行重试尝试获取锁,unit 为时间单位。默认锁的超时时间为 30 秒。

        3)boolean tryLock(long waitTime, long leaseTime, TimeUnit unit):在获取锁失败时,在 waitTime 时间内进行重试尝试获取锁,锁的超时时间为 leaseTime 秒,unit 为时间单位。

        最后,调用 lock 对象中的方法 unlock() 来释放锁。

具体代码:

java">    @AutowiredRedissonClient redissonClient;@Testvoid contextLoads() throws InterruptedException {//先获取锁对象,根据业务来锁定资源RLock lock = redissonClient.getLock("lock");//尝试获取锁//tryLock() 进行了重写,有无参、只有两个参数、有三个参数boolean b = lock.tryLock(1, TimeUnit.SECONDS);if (!b){System.out.println("获取锁失败!");}try {System.out.println("获取锁成功!");} catch (Exception e) {throw new RuntimeException(e);} finally {//释放锁lock.unlock();}}

        4.0 Redisson 可重入锁原理

        在之前的基于 setnx 实现的分布式锁是不支持可重入锁,举个例子:线程一来获取锁,使用 setnx 来设置,当设置成功,则获取锁成功了,线程一在获取锁成功之后,再想来获取相同的锁时,则再次执行 setnx 命令,那一定是不可能成功获取,因为 setxn 已经存在了,这就是基于 setnx 来实现分布式锁不可重入锁的核心原因。

        而对于 Redisson 可以实现可重入锁,这是如何实现的呢?

        其核心原因是基于 Redis 中的哈希结构实现的分布式锁,利用 key 来锁定资源,对于 field 来标识唯一成功获取锁的对象,而对于 value 来累计同一个线程成功获取相同的锁的次数。

        具体实现思路:

        1)尝试获取锁:

        先判断缓存中是否存在 key 字段,如果存在,则说明锁已经被成功获取,这时候需要继续判断成功获取锁的对象是否为当前线程,如果根据 key field 来判断是当前线程,则 value += 1 且还需要重置锁的超时时间;如果根据 key field 判断不是当前线程,则直接返回 null。如果缓存中不存在 key 字段,则说明锁还没有被其他线程获取,则获取锁成功。

        2)释放锁:

        当业务完成之后,在释放锁之前,先判断获取锁的对象是不是当前线程,如果不是当前线程,则说明可能由于超时,锁已经被自动释放了,这时候直接返回 null;如果是当前线程,则进行 value -= 1 ,最后再来判断 value 是否大于 0 ,当大于 0 时,则不能直接释放锁,需要重置锁的超时时间;当 value = 0 时,则可以真正的释放锁。

如图:

 

        又因为使用 Java 实现不能保证原子性,所以需要借助 Lua 脚本实现多条 Redis 命令来保证原则性。

尝试获取锁的 Lua 脚本:

释放锁的 Lua 脚本:

        5.0 Redisson 锁重试原理

        在之前基于 setnx 实现的分布式锁,获取锁只尝试一次就返回 false ,没有重试机制。

        而 Redisson 是如何实现锁重试的呢?

实现锁重试

        追踪源代码:

得到该类:

        首先,将等待时间转换为毫秒,接着获取当前时间和获取当前线程 ID ,再接着第一个尝试去获取锁,将参数 waitTime 最大等待时间,leaseTime 锁的超时时间,unit 时间单位,threadId 当前线程 ID 传进去 tryAcquire 方法中。

        紧接着来查看 tryAcquire 方法:

         再查看调用的 tryAcquireAsync 方法:

        当指定了 leaseTime 锁的超时时间,则会调用 tryLockInnerAsync 方法;当没有指定 leaseTime 锁的超时时间,则会调用 getLockWatchdogTimeout 方法,默认超时时间为 30 秒。

        接着查看 tryLockInnerAsync 方法:

         可以看到,这就是尝试获取是的 Lua 脚本执行多条 Redis 命令。

        细心可以发现,如果正常获取锁,则返回 null ;如果获取锁失败,则返回当前锁的 TTL ,锁的剩余时间。

        因此最后将当前锁的 TTL 返回赋值给 Long ttl 变量。

        再接着往下:

        当 ttl == null ,则说明当前线程成功获取锁,因此就不需要接着往下再次尝试去获取锁了。相反,当 ttl != null ,则需要接着往下走,重新尝试去获取锁。

        判断 time 等于当前时间减去在第一次获取锁之前的时间,time 也就是最大的等待时间还剩多少。判断 time 是否小于 0 ,若小于 0 则已经到了最大等待时间了,所以不需要再继续等下去了,直接返回 false 即可。

        若 time 还是大于 0 ,则接着往下走:

        调用 subscribe 方法,该方法可以理解成订阅锁,一旦锁被释放之后,该方法就会收到通知,然后再去尝试获取锁。

回顾在释放锁的时候,使用 Redis 命令中的 redis.call('publish', KEYS[2], ARGV[1]) 来发布消息,通知锁已经被释放,一旦锁被释放,那么就可以成功订阅。

        因此,在订阅锁的过程中,并不是一直死等下去,而是在 time 剩余最大等待时间之内,如果可以订阅锁成功,才会去尝试获取锁。如果在 time 时间内,订阅锁失败,则会取消订阅,再返回 false 。

        接着往下走,当在 time 时间内订阅锁成功,会更新 time 时间,也就是更新最大的等待时间,判断 time 小于 0 ,则返回 false ,如果 time 还是大于 0 ,则到了真正尝试第二次获取锁,调用 tryAcquire(waitTime, leaseTime, unit, threadId) 方法,将返回值再次赋值给变量 ttl ,判断 ttl == null ,则说明成功获取锁了,直接返回 true ;判断 ttl != null ,则第二次获取锁还是失败,由需要更新 time 了,因为在调用尝试获取锁的过程中,消耗时间还是挺大的,同理,判断更新完之后的 time 是否大于 0,如果 time 小于 0,则超过了剩余最大锁的超时时间,返回 false ;

        如果判断 time 仍旧大于 0 :

        那么先判断锁的过期时间 ttl 与 剩余时间 time ,如果 ttl < time ,则类似订阅方法一样的思路,选择等待 ttl 锁的过期时间,当 ttl 过期之后,就会订阅该锁;如果 time < ttl ,则 ttl 还没有释放,就不需要等 ttl 了,等到 time 结束还没有订阅到锁,则 time 也就小于 0 了,如果在 time 时间内获取到锁,再次尝试去获取锁,同样的,当在 ttl 时间内,成功订阅了,而且 time > 0 ,则会第三次去尝试获取锁。之后的步骤都是如此,这里使用了 do whlie 循环,判断循环成立为 time > 0,当 time < 0 ,则会退出循环。

        总结,在解决可重试锁过程中,并不是循环不断的调用 tryAcquire(waitTime, leaseTime, unit, threadId) 方法来获取锁,这样容易造成 CPU 的浪费,而是通过等待锁释放,再去获取锁的方式来实现的可重试锁,利用信号量(Semaphore)和发布/订阅(PubSub)模式实现等待、唤醒、获取锁失败的重试机制。

        6.0 Redisson WatchDog 机制

        在之前基于 setnx 实现的分布式锁,锁超时释放虽然可以避免死锁,但是如果是业务执行耗时较长,也会导致锁释放,存在安全隐患。

        6.1 Redisson 是如何解决超时释放问题的呢?

        解决超时释放的核心是:当 leaseTime == -1 时,为了保证当前业务执行完毕才能释放锁,而不是业务还没有执行完毕,锁就被自动释放了。

追踪源代码:

        当 leaseTime == -1 时,默认锁的最大超时时间为 30 秒,会执行以下代码。

        接着点进去:

        WatchDog 会在锁的过期时间到期之前,定期向 Redis 发送续约请求,更新锁的过期时间。这通常是通过设置一个较短的过期时间和一个续约间隔来实现的。

        如果持有锁的线程正常释放锁,WatchDog 会停止续约操作。如果持有锁的线程崩溃或失去响应,WatchDog 会在锁的过期时间到达后自动释放锁。

        简单概述一下 WatchDog 机制:在获取锁成功之后,就会调用 scheduleExpirationRenewal(threadId) 方法开启自动续约,具体是由在 map 中添加业务名称和任务定时器,这个定时器会在一定时间内执行,比如说 10 秒就会自动开启任务,而该定时器中的任务就是不断的重置锁的最大超时时间,使用递归,不断的调用重置锁的时间,这就保证了锁是永久被当前线程持有。 

        这样就可以保证执行业务之后,才会释放锁。释放锁之后,会取消定时任务。

        7.0 Redisson MultiLock 原理

        7.1 Redisson 分布式锁是如何解决主从一致性问题的呢?

        先搞清楚什么是主从一致性问题,在集群的 Redis 中会区分出主力机和一般机器,在写 Redis 命令会放到主力机中运行,而主力机和一般机器需要保证数据都是一样的,也就是主从同步数据,在主力机中执行写命令时,突然发生宕机,未来得及将数据同步到其他一般机器中,而且当主力机宕机之后,会选出一台一般机器充当主力机,这时候的主力机没有同步之前的数据,那么其他线程再来写命名的时候就会出现问题了,这出现了主从不一致性。

        那么 Redisson 是如何来解决该问题呢?

        在多主架构中,每台主机都可以接收写请求,这样即使某一台主机宕机,其他主机仍然可以继续处理写请求。

        当某一台主机宕机后,如果在它恢复之前有新的写操作发生,可能会导致数据不一致。通过比较不同主机的数据状态,可以很容易地发现这些不一致的问题。

        当宕机的主机恢复后,可以通过与其他主机的数据进行比较,找出差异并进行数据同步,确保所有主机的数据一致。

        简单来说,设置多台主力机,每一次写命令都是一式多份,当某一台主力机出现宕机了,主从未来得及同步时,再写命令,同样一式多份,这样充当主力机出现了跟其他主力机不同的结果时,就很容易的发现问题了。

        通过设置多台主力机并进行写操作的多份复制,可以有效提高系统的可靠性,并在出现问题时快速发现和解决数据不一致的问题。

具体使用:


http://www.ppmy.cn/ops/110103.html

相关文章

MinIO自动化下载及部署脚本(Windows)

提前准备事项 直接上脚本代码&#xff0c;需要保存为Power shell脚本文件&#xff0c;然后在执行 脚本执行策略 注意&#xff1a;Windows默认是禁止脚本运行的&#xff0c;需要放开一下脚本执行策略 临时更改执行策略&#xff08;仅对当前会话有效&#xff09;&#xff1a;…

安防监控/视频汇聚平台EasyCVR无法启动并报错“error while loading shared libraries”,如何解决?

安防监控/视频汇聚平台EasyCVR视频管理系统以其强大的拓展性、灵活的部署方式、高性能的视频能力和智能化的分析能力&#xff0c;为各行各业的视频监控需求提供了优秀的解决方案。通过简单的配置和操作&#xff0c;用户可以轻松地进行远程视频监控、存储和查看&#xff0c;满足…

HP电脑如何启动硬件检测

许多人都在使用HP电脑&#xff0c;但是当出现问题时候&#xff0c;不知道该如何测试&#xff0c;本文来分享一下&#xff0c;如何在电脑能开机但是有问题时进行检测。 使用F2键进行组件测试 步骤&#xff1a;开机后不停敲击键盘上【F2】的按键&#xff0c;进入BIOS设置界面&am…

git 合并分支并解决冲突

git 合并分支并解决冲突 切换分支 git checkout <branch-name> 首先切换到要合并的目标分支 合并分支 git merge <source-branch> //将源分支代码合并到当前分支中&#xff0c;源分支的各项新增的提交都会按时间点插入到当前分支的提交记录中 git merge …

Django+Vue基于OpenCV的人脸识别系统的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 需要的环境3.2 Django接口层3.3 实体类3.4 config.ini3.5 启动类3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平台Java领域优质创作者&…

信息安全新趋势:黑龙江等保测评全解析,助力企业安全升级!

随着信息技术的迅猛发展&#xff0c;信息安全问题日益凸显&#xff0c;成为各行业亟待解决的关键挑战。特别是在黑龙江省&#xff0c;信息安全等级保护&#xff08;等保&#xff09;测评逐渐成为企业提升安全防护能力的重要手段。本文将对黑龙江等保测评的新趋势进行全面解析&a…

浅谈网络安全的认识与学习规划

一、网络安全的认识 1&#xff09;计算机网络安全定义 网络安全&#xff08;Network Security&#xff09;&#xff0c;它是指网络系统的硬件&#xff0c;软件及其系统中的数据受到安全保护&#xff0c;不因恶意的或者偶然的问题遭到更改、破坏、泄露&#xff0c;系统连续可靠…

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化&#xff0c;使其看起来更清晰&#xff0c;同时保持尺寸不变&#xff0c;通常涉及到图像处理技术如锐化、降噪、对比度增强…