Redisson分布式锁源码解析、集群环境存在的问题
一、使用Redisson步骤
Redisson各个锁基本所用Redisson各个锁基本所用Redisson各个锁基本所用
二、源码解析
lock锁
1) 基本思想:
lock有两种方法 一种是空参 另一种是带参
* 空参方法:会默认调用看门狗的过期时间30*1000(30秒)
* 然后在正常运行的时候,会启用定时任务调用重置时间的方法(间隔为开门看配置的默认过期时间的三分之一,也就是10秒)
* 当出现错误的时候就会停止续期,直到到期释放锁或手动释放锁
* 带参方法:手动设置解锁时间,到期后自动解锁,或者业务完成后手动解锁,不会自动续期
源码:
Lock
调用lockInterruptibly()方法会默认传入lease 为-1,该值再后面起作用
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {long threadId = Thread.currentThread().getId();//获取该锁的过期时间,如果该锁没被持有,会返回一个null,如果被持有 会返回一个过期时间Long ttl = this.tryAcquire(leaseTime, unit, threadId);if (ttl != null) {//ttl不为null,说明锁已经被抢占了RFuture<RedissonLockEntry> future = this.subscribe(threadId);this.commandExecutor.syncSubscription(future);try {//开始循环获取锁while(true) {//刚进如循环先尝试获取锁,获取成功返回null,跳出循环,获取失败,则继续往下走ttl = this.tryAcquire(leaseTime, unit, threadId);if (ttl == null) {return;}if (ttl >= 0L) {//如果过期时间大于0,则调用getLatch// 返回一个信号量,开始进入阻塞,阻塞时长为上一次锁的剩余过期时长,并且让出cup//有阻塞必然有唤醒,位于解锁操作中this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {this.getEntry(threadId).getLatch().acquire();}}} finally {this.unsubscribe(future, threadId);}}}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {//如果leaseTime != -1,即不等于默认值,则表示手动设置了过期时间if (leaseTime != -1L) {return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {//如果leaseTime = -1,表示使用默认方式,即使用看门狗默认实现自动续期RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);ttlRemainingFuture.addListener(new FutureListener<Long>() {public void operationComplete(Future<Long> future) throws Exception {//如果tryLockInnerAsync执行成功if (future.isSuccess()) {//获取过期时间Long ttlRemaining = (Long)future.getNow();//过期时间为空,表示加锁成功if (ttlRemaining == null) {//开启刷新重置过期时间步骤RedissonLock.this.scheduleExpirationRenewal(threadId);}}}});return ttlRemainingFuture;}}
// lua脚本尝试抢占锁,失败返回锁过期时间<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {this.internalLockLeaseTime = unit.toMillis(leaseTime);//直接使用lua脚本发起命令//通过lua脚本可以看出,redisson加锁除了使用自定义的名字以外,还要使用uuid// 加上当前线程的threadId组合,以自定义名字作hash的key,使用return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command,//如果该锁未被占有,则设置锁,设置过期时间,过期时间为 internalLockLeaseTime ,然后返回null"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; " +//如果锁已经被占有,判断是否是重入锁,如果是重入锁,则将value增加1 ,代表重入,并且设置过期时间,返回null。"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; " +//如果已经被站有所,且不是重入锁,则返回过期时间"return redis.call('pttl', KEYS[1]);",Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});}
看门狗续命
//看门狗续命机制private void scheduleExpirationRenewal(final long threadId) {//首先会判断该线程是否已经再重置时间的map中,仅仅第一次进来是空的。if (!expirationRenewalMap.containsKey(this.getEntryName())) {//使用了看门狗默认的时间(30秒) 除以3 ,也就是延迟10秒后执行Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {public void run(Timeout timeout) throws Exception {//判断是否该线程是否还持有锁,如果持有,返回1,并且设置过期时间,如果没持有,返回0RFuture<Boolean> future = RedissonLock.this.commandExecutor.evalWriteAsync(RedissonLock.this.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(RedissonLock.this.getName()), new Object[]{RedissonLock.this.internalLockLeaseTime, RedissonLock.this.getLockName(threadId)});future.addListener(new FutureListener<Boolean>() {public void operationComplete(Future<Boolean> future) throws Exception {//从map中移除该线程,这样下次再调用该方法仍然可以执行RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());if (!future.isSuccess()) {RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());} else {if ((Boolean)future.getNow()) {//当lua脚本返回1表是true,也就是仍然持有锁,则递归调用该方法,RedissonLock.this.scheduleExpirationRenewal(threadId);}}}});}}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);if (expirationRenewalMap.putIfAbsent(this.getEntryName(), task) != null) {task.cancel();}}}
2、unlock
源码
public RFuture<Void> unlockAsync(final long threadId) {final RPromise<Void> result = new RedissonPromise();//调用lua脚本释放锁RFuture<Boolean> future = this.unlockInnerAsync(threadId);future.addListener(new FutureListener<Boolean>() {public void operationComplete(Future<Boolean> future) throws Exception {if (!future.isSuccess()) {result.tryFailure(future.cause());} else {Boolean opStatus = (Boolean)future.getNow();//如果锁状态为null,表示存在异常,为正常释放锁之前,被别人占领锁了if (opStatus == null) {IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + RedissonLock.this.id + " thread-id: " + threadId);result.tryFailure(cause);} else {//如果返回0.为false 表示可重入锁,不取消重置过期时间,//返回1 为true,表示已解锁,取消重置过期时间if (opStatus) {RedissonLock.this.cancelExpirationRenewal();}//解锁result.trySuccess((Object)null);}}}});return result;}
protected RFuture<Boolean> unlockInnerAsync(long threadId) {return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,//当key不存在,表示锁未被持有,说明不用解锁了,返回1 ,1在后续表示取消重置过期时间"if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); return 1; end;" +//key存在,但是持有锁的线程不是当前线程,返回null,后面会提出一个异常"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; " +//锁状态-1后仍然大于0,表示可重入锁,仍处于锁定状态,返回0,0在后续表示 不做处理,仍然重置过期时间"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; " +//返回锁状态不大于0,正常解锁,返回1,1在后续表示取消重置过期时间"else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; " +"return nil;", Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)});}
三、集群环境下潜在问题
在Redis主从架构+哨兵模式的环境下,业务系统已经成功获取了锁,redis写入数据,但是正要往从库上存数据时,发生主库宕机的情况,从库在哨兵的选举下成为了主库,而另外一个业务请求再次需要获取锁,会直接访问到新的主库,而此时新主库是没有锁信息的,此时就会出现业务重复的情况。
相关文章:

Redisson分布式锁源码解析、集群环境存在的问题
一、使用Redisson步骤 Redisson各个锁基本所用Redisson各个锁基本所用Redisson各个锁基本所用 二、源码解析 lock锁 1) 基本思想: lock有两种方法 一种是空参 另一种是带参 * 空参方法:会默认调用看门狗的过期时间30*1000&…...

2016年10月4日 Go生态洞察:HTTP追踪介绍
🌷🍁 博主猫头虎(🐅🐾)带您 Go to New World✨🍁 🦄 博客首页——🐅🐾猫头虎的博客🎐 🐳 《面试题大全专栏》 🦕 文章图文…...

分布式篇---第四篇
系列文章目录 文章目录 系列文章目录前言一、分布式ID生成有几种方案?二、幂等解决方法有哪些?三、常见负载均衡算法有哪些?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给…...

从零开始的C++(十九)
红黑树: 一种接近平衡的二叉树,平衡程度低于搜索二叉树。 特点: 1.根节点为黑 2.黑色结点的子结点可以是红色结点或黑色结点。 3.红色结点的子结点只能是黑色结点。 4.每个结点到其所有叶子结点的路径的黑色结点个数相同。 5.指向空的…...

opencv-使用 Haar 分类器进行面部检测
Haar 分类器是一种用于对象检测的方法,最常见的应用之一是面部检测。Haar 分类器基于Haar-like 特征,这些特征可以通过计算图像中的积分图来高效地计算。 在OpenCV中,Haar 分类器被广泛用于面部检测。以下是一个简单的使用OpenCV进行面部检测…...

C++纯虚函数和抽象类 制作饮品案例(涉及知识点:继承,多态,实例化继承抽象类的子类,多文件实现项目)
一.纯虚函数的由来 在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。例如: #include<iostream>using namespace std;class AbstractCalculator { public:int m_Num1;int m_Num2;virtual int getResult(){r…...
什么是网关和链路追踪,以及怎么使用?
在云计算中,网关和链路追踪也是非常重要的。云服务提供商通常会提供网关和链路追踪等相关服务来帮助用户管理和优化云计算网络。 云计算中的网关通常包括以下几种: 虚拟网络网关:用于将云中不同虚拟网络之间相互连接,实现云内不同…...
git 文件被莫名其妙的或略且无论如何都查不到哪个.gitignore文件忽略的
先说解决办法:git check-ignore -v [文件路径] 这个命令会返回一个忽略规则,以及该规则在哪个文件中定义的,该规则使得指定的文件被忽略。 1.遇到的问题 同项目组,其他同学都可以正常的提交.meta文件,我的提交就出现以…...

nova组件简介
目录 组件关系图 controller节点 openstack-nova-api.service: openstack-nova-conductor.service: openstack-nova-consoleauth.service: openstack-nova-novncproxy.service: openstack-nova-scheduler.service: openstack-nova-conductor.service详解 作用和功能&…...
【Vue】响应式与数据劫持
Vue.js 是一个渐进式的 JavaScript 框架,其中最重要的一个特性就是响应式(Reactivity)。Vue 借助数据劫持(Data Observation)技术实现了对数据的响应式更新,当数据发生变化时,它会自动重新渲染视…...

Modbus RTU转Profinet网关连接PLC与变频器通讯在机床上应用案例
背景:以前在机床加工车间里,工人们忙碌地操作着各种机床设备。为了使整个生产过程更加高效、流畅,进行智能化改造。 方案:在机床上,PLC通过Modbus RTU转Profinet网关连接变频器进行通讯:PLC作为整个生产线…...
Autoware 整体架构
Autoware 整体架构 测试...
【maven】手动指定jar推送
说明 为了推送第三方的jar,有时需要指定对应的jar推送到私有仓库。 示例 mvn deploy:deploy-file --settings /home/xxx/.m2/settings.xml -DgroupIdgroupId的值 -DartifactIdartifactId的值 -Dversionjar包的版本号 -Dpackagingjar -Dfilejar的路径 -Durlhttp:/…...
算法---定长子串中元音的最大数目
题目 给你字符串 s 和整数 k 。 请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。 英文中的 元音字母 为(a, e, i, o, u)。 示例 1: 输入:s “abciiidef”, k 3 输出:3 解释:…...

美国汽车零部件巨头 AutoZone 遭遇网络攻击
Security Affairs 网站披露,美国汽车配件零售商巨头 AutoZone 称其成为了 Clop MOVEit 文件传输网络攻击的受害者,导致大量数据泄露。 AutoZone 是美国最大的汽车零配件售后市场经销商之一,在美国、墨西哥、波多黎各、巴西和美属维尔京群岛经…...
WPF面试题入门篇
入门篇[2] 1. 谈谈什么是WPF? WPF(Windows Presentation Foundation)是微软公司开发的一种用于创建Windows应用程序的用户界面框架。它是.NET Framework的一部分,提供了一种基于XAML(可扩展应用程序标记语言…...

opencv-ORB检测
ORB(Oriented FAST and Rotated BRIEF)是一种图像特征检测和描述算法,结合了 FAST 关键点检测器和 BRIEF 描述子的优点。ORB 算法具有良好的性能,特别适用于实时应用,如目标追踪、相机定位等。 以下是 ORB 算法的一般…...

please upgrade numpy version to >=1.20
升级 upgrade numpy_升级numpy-CSDN博客 pip install numpy --upgrade 没有pip conda install numpy --upgrade 会报错 conda list numpy来查看numpy版本 似乎这个numpy要看numpy-base这个 似乎没有pip...

关于进制的转化
二进制转十进制: 🔰 方法一:二进制转十进制,用各数的码位与位权的乘积之和,说白了就是用从右到左的每个数去乘以2的幂次方(最右边是0),然后就所有的数相加。 补充:位权是…...
JVM 之 字节码指令
目录 一. 前言 二. 指令集 2.1. 支持的数据类型 2.2. 指令分类 三. 指令手册 3.1. 操作数栈 3.2. 运算与转换 3.3. 条件转移 3.4. 类与数组 3.5. 调度与返回加 finally 3.6. 指令手册汇总 3.7. 示例 一. 前言 字节码指令集的特点是数据量短小精干,便于传…...
Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信
文章目录 Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket(服务端和客户端都要)2. 绑定本地地址和端口&#x…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...

push [特殊字符] present
push 🆚 present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中,push 和 present 是两种不同的视图控制器切换方式,它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...

Web后端基础(基础知识)
BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。 优点:维护方便缺点:体验一般 CS架构:Client/Server,客户端/服务器架构模式。需要单独…...
区块链技术概述
区块链技术是一种去中心化、分布式账本技术,通过密码学、共识机制和智能合约等核心组件,实现数据不可篡改、透明可追溯的系统。 一、核心技术 1. 去中心化 特点:数据存储在网络中的多个节点(计算机),而非…...
xmind转换为markdown
文章目录 解锁思维导图新姿势:将XMind转为结构化Markdown 一、认识Xmind结构二、核心转换流程详解1.解压XMind文件(ZIP处理)2.解析JSON数据结构3:递归转换树形结构4:Markdown层级生成逻辑 三、完整代码 解锁思维导图新…...
用递归算法解锁「子集」问题 —— LeetCode 78题解析
文章目录 一、题目介绍二、递归思路详解:从决策树开始理解三、解法一:二叉决策树 DFS四、解法二:组合式回溯写法(推荐)五、解法对比 递归算法是编程中一种非常强大且常见的思想,它能够优雅地解决很多复杂的…...
TJCTF 2025
还以为是天津的。这个比较容易,虽然绕了点弯,可还是把CP AK了,不过我会的别人也会,还是没啥名次。记录一下吧。 Crypto bacon-bits with open(flag.txt) as f: flag f.read().strip() with open(text.txt) as t: text t.read…...

【51单片机】4. 模块化编程与LCD1602Debug
1. 什么是模块化编程 传统编程会将所有函数放在main.c中,如果使用的模块多,一个文件内会有很多代码,不利于组织和管理 模块化编程则是将各个模块的代码放在不同的.c文件里,在.h文件里提供外部可调用函数声明,其他.c文…...

结构化文件管理实战:实现目录自动创建与归类
手动操作容易因疲劳或疏忽导致命名错误、路径混乱等问题,进而引发后续程序异常。使用工具进行标准化操作,能有效降低出错概率。 需要快速整理大量文件的技术用户而言,这款工具提供了一种轻便高效的解决方案。程序体积仅有 156KB,…...