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. 示例 一. 前言 字节码指令集的特点是数据量短小精干,便于传…...
Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...
Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...
【JVM】- 内存结构
引言 JVM:Java Virtual Machine 定义:Java虚拟机,Java二进制字节码的运行环境好处: 一次编写,到处运行自动内存管理,垃圾回收的功能数组下标越界检查(会抛异常,不会覆盖到其他代码…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...
【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...
有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...
逻辑回归暴力训练预测金融欺诈
简述 「使用逻辑回归暴力预测金融欺诈,并不断增加特征维度持续测试」的做法,体现了一种逐步建模与迭代验证的实验思路,在金融欺诈检测中非常有价值,本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...
Elastic 获得 AWS 教育 ISV 合作伙伴资质,进一步增强教育解决方案产品组合
作者:来自 Elastic Udayasimha Theepireddy (Uday), Brian Bergholm, Marianna Jonsdottir 通过搜索 AI 和云创新推动教育领域的数字化转型。 我们非常高兴地宣布,Elastic 已获得 AWS 教育 ISV 合作伙伴资质。这一重要认证表明,Elastic 作为 …...
阿里云Ubuntu 22.04 64位搭建Flask流程(亲测)
cd /home 进入home盘 安装虚拟环境: 1、安装virtualenv pip install virtualenv 2.创建新的虚拟环境: virtualenv myenv 3、激活虚拟环境(激活环境可以在当前环境下安装包) source myenv/bin/activate 此时,终端…...
【WebSocket】SpringBoot项目中使用WebSocket
1. 导入坐标 如果springboot父工程没有加入websocket的起步依赖,添加它的坐标的时候需要带上版本号。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dep…...
