Redisson分布式锁 原理 + 运用 记录

Redisson 分布式锁

简单入门

pom

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

配置类

package com.hmdp.config;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;/*** redisson的配置类* @author jjking* @date 2023-11-06 20:21*/
@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient() {//配置Config config = new Config();config.useSingleServer().setAddress("redis://8.140.54.97:6379");//创建RedissonClient对象return Redisson.create(config);}
}

简单的使用
在这里插入图片描述

获取锁的方法的参数

看官网

RLock lock = redisson.getLock("myLock");//默认锁 
lock.lock();//锁的过期时间为10s
lock.lock(10, TimeUnit.SECONDS);//获取锁失败,重试时间为100s,获取锁成功,锁过期时间为10s
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {try {...} finally {lock.unlock();}
}

可重入锁原理

redissson的可重入原理和Reentranlock很像,都是用计数器来实现的

因为这里既要锁的名字 又要锁的标识 还要计数器,这里不能单纯的用key value了,要用hashmap
在这里插入图片描述

我们先来看整体的锁重入的流程
在这里插入图片描述
整个的流程都十分严谨,而且比较合理

我们来简单的捋一下

  • 首先,判断锁是否存在
    - 锁存在,此时判断锁上的标识是否是自己,如果是自己,那么计数器 + 1,代表又多一个人拥有此锁
    - 锁不存在,那么此时就是新锁,我们应该创建一个新锁,并且置计数器为1
    - 不管锁是否存在,都要设置锁的有效期,和我们自己实现不同的是,如果我们不写有效期,他会有一个默认的看门狗有效期,为30s,这里可以看我下面的源码分析
  • 执行完业务了,就该到释放锁的流程了
    - 先判断这个锁是不是自己,如果不是自己,那么锁就已经释放了
    - 如果是自己,就让锁计数 - 1,此时再去判断此时的计数器是否到了0,如果是的化,释放锁

不管是获取锁 还是 释放锁,他们底层都是用lua脚本来实现的,使用lua脚本来实现这些命令,也是为了操作redis时命令的原子性,避免线程安全问题

lua脚本

所以我们先来看获取锁 + 释放锁的lua脚本,这样我们再看源码的时候就会有所准备

获取锁

在这里插入图片描述
和我们的流程一致

先是判断锁是否存在,不存在获取锁,并且设置有效期 返回1
存在的化,就去判断锁标识是不是自己,如果不是自己,获取锁失败 返回0

如果是自己,计数器 + 1,设置有效期,返回1

这个lua脚本还是很好理解的,如果你不懂lua脚本,就把这个看成是js代码,差不多的

释放锁

在这里插入图片描述
先是判断锁是否是自己,如果是不是自己,说明锁的主人换了,返回nil,这里也是为了解决锁误删问题

是自己锁的化,就去计数器-1,然后判断计数器是否为0,如果是0,释放锁,不是的化,就正常往下执行

源码查看

我这里的源码解读是十分粗浅的,我就是看的是一个大概,并没有非常仔细,我的想法是想大概搞懂这个原理 + 流程就ok了

在这里插入图片描述
先点开tryLock的代码,点到如下的是是实现类RedissonLock就是我们普遍使用的lock
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
点这个方法里边
在这里插入图片描述

在这里插入图片描述
这里的lua脚本是简略版的,直接用参数,但是中心思想还是一样的,所以这就是锁重入的原理

总结

我们总结一下,锁重入的原理

我们应该搞清楚一个关键的问题,如何知道是同一个人再拿锁,答案就是设置锁标识,锁上面写了你的名字相当于

所以,当我们获取锁的时候,就应该判断锁锁是不是自己的,如果是自己的,已经有锁了,我们就再锁的计数器 + 1

然后我们再释放锁的时候,也要去判断锁是不是自己,如果是自己的化,计数器 - 1
然后判断此时要不要去释放锁,如果计数器 为0了,那么就是释放速,反之就不用

锁超时 + 锁重试的原理

因为锁超时和锁重试有着千丝万缕的关系,所以一起来看是最好的

锁重试

    @Overridepublic boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {long time = unit.toMillis(waitTime);//这个是获得当前时间的long current = System.currentTimeMillis();long threadId = Thread.currentThread().getId();Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);//如果返回的是null的话,就拿到了锁返回true// lock acquiredif (ttl == null) {return true;}//此时是没有获得锁,接下来要进行重试的阶段time -= System.currentTimeMillis() - current;//如果此时重试的时间已经到了,就返回falseif (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}//再获取一遍current = System.currentTimeMillis();//订阅,订阅他人释放锁的信号RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);//等待time的时间,如果这个时间内,都没有人过来发信号的话,就会直接失败if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {if (!subscribeFuture.cancel(false)) {subscribeFuture.onComplete((res, e) -> {if (e == null) {//取消这个订阅unsubscribe(subscribeFuture, threadId);}});}acquireFailed(waitTime, unit, threadId);return false;}//到了这个地方,说明订阅来人通知了,也就是有人释放锁了,并且是在重试的剩余//时间内来通知的try {//再看一次是否超时了time -= System.currentTimeMillis() - current;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}while (true) {long currentTime = System.currentTimeMillis();//重试第一次ttl = tryAcquire(waitTime, leaseTime, unit, threadId);// lock acquiredif (ttl == null) {return true;}//判断是否超时time -= System.currentTimeMillis() - currentTime;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}// waiting for messagecurrentTime = System.currentTimeMillis();if (ttl >= 0 && ttl < time) {subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);}time -= System.currentTimeMillis() - currentTime;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}}} finally {unsubscribe(subscribeFuture, threadId);}
//        return get(tryLockAsync(waitTime, leaseTime, unit));}

需要注意是,再获取锁的一开始就开始计时了
在这里插入图片描述

比较特殊的就是这里的订阅机制,redisson如果锁失败不会一直忙等的,而是等待一段时间,这里的时间是不一定的,因为是订阅释放锁的信号,也就是当别人释放锁的时候,会发出这样的信号,这样这里就会进行重试

在这里插入图片描述

然后就是while true循环,去重试了,再循环的过程中,也会去维持此时的等待的剩余时间,也是基于订阅机制的

总结

总结来说, 锁重试的原理的关键在于订阅机制,订阅释放锁信号,这样很大程度上减少cpu的消耗,
然后就是比较正常的重试了,这我们都差不多能懂

锁超时

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {if (leaseTime != -1) {return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);}RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);ttlRemainingFuture.onComplete((ttlRemaining, e) -> {if (e != null) {return;}// lock acquired 获取锁成功if (ttlRemaining == null) {scheduleExpirationRenewal(threadId);}});return ttlRemainingFuture;
}

先是这里的 ttlRemainingFuture.onComplete方法,类似于回调函数,当我们尝试去获取锁的时候,会有回调,e是异常,

如果ttlRemaining == null的化,说明获取成功了,获取锁成功之后就去更新有效期,或者说更新租约,下面就是更新的代码

private void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry = new ExpirationEntry();//第一个参数当前锁的名称ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);//如果有重入锁的话,那么这里的oldEntry就不是空值if (oldEntry != null) {oldEntry.addThreadId(threadId);} else {//如果是第一次来的话,oldEntry他返回的是nullentry.addThreadId(threadId);//更新有效期,因为是第一次来嘛renewExpiration();}
}

这里的EXPIRATION_RENEWAL_MAP,类似于房东一样,管理着所有的key的过期时间
如果我们是重入锁的化,这里的oldEntry就不会是空值,我们将线程id设置进去

如果是第一次来的化,就会去更新租约renewExpiration

private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}//定时任务,这里是个延时任务,这里的延迟时间就是internalLockLeaseTime / 3//如果我们不写释放时间的话,internalLocakLeaseTime就是30s,那么这里的释放时间就是10sTimeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic 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) {// reschedule itselfrenewExpiration();}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}

这个方法就比较重要了,是看门狗的逻辑
Timeout 是定时任务,定时的时间是(看门狗的时间) / 3
也就是这个代码 internalLockLeaseTime / 3, 那么默认来说看门狗的时间是30s,那么这里默认就是10s一次,开始续约

我们还需要提前注意,只要当我们的过期时间是空的时候,才会有看门狗的存在,如果我们去看设置了过期时间的化,是不会到这里来的,我们看这里代码就懂了
在这里插入图片描述

回到这里的看门狗的逻辑,再timeout中的run方法就是核心代码
我们要先着眼于这个方法
在这里插入图片描述
在这里插入图片描述
这个方法就是最最最核心的方法了,已经不能再底层了,逻辑就是判断这个锁是不是自己的,然后更新有效期
再之后,回到看门狗代码

就会有一个递归的代码

在这里插入图片描述
意思是,不断的去进行重置

总结

按我们来看,只有当我们没有设置过期时间的时候,就会有看门狗的机制,看门狗的机制还是为了解决线程安全问题,实现了一个自动化,并且比较巧妙的是,就算我们拿到锁突然宕机了,那这里的重新去重置有效期,也不会去进行,也就是说,他会自动停下来,不用我们手动去搞

总结下来的化,看门狗这个名字挺贴切的,他会帮我们把手好过期时间的大门,然后如果我们的房子着火了的化,他还会自动的将门打开,自动化~

在这里插入图片描述
在这里插入图片描述
对于释放锁而言就会发送示范锁的消息然后去取消看门狗

主从一致性问题

在这里插入图片描述

正常来说,java给主机发一个锁的操作,也就是想要新开一个锁

在这里插入图片描述
同步还没完成,主机就宕机了,然后由于从机由哨兵模式,就会重新整一个为主机在这里插入图片描述

这个问题一定是会存在的,如果说我们这里的锁再主机上设置了,还没同步到从机上,突然宕机,锁就会失效

解决办法

解决办法也很简答,我们要把获取锁成功的条件改为,所有节点都获得到这个锁才算成功
在这里插入图片描述
这样完全就可以根治这个问题,除非所有节点全都宕机了,那就不用先关心这个锁的问题,先把redis搞好先

但是我想了一下,不应该所有都有,为了性能 的化,我认为还是一个主节点 + 从节点都收到了这个锁的化,就算成功!

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

【Spring MVC篇】参数的传递及json数据传参

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【Spring MVC】 本专栏旨在分享学习Spring MVC的一点学习心得&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 一、普通参数的传…

CVE-2012-1823 漏洞复现

CVE-2012-1823 PHP SAPI 与运行模式 首先&#xff0c;介绍一下PHP的运行模式。 下载PHP源码&#xff0c;可以看到其中有个目录叫sapi。sapi在PHP中的作用&#xff0c;类似于一个消息的“传递者”&#xff0c;比如在《Fastcgi协议分析 && PHP-FPM未授权访问漏洞 &…

【算法与数据结构】496、503、LeetCode下一个更大元素I II

文章目录 一、496、下一个更大元素 I二、503、下一个更大元素II三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、496、下一个更大元素 I 思路分析&#xff1a;本题思路和【算法与数据结构】739、LeetCode每日温度类似…

论文笔记:相似感知的多模态假新闻检测

整理了RecSys2020 Progressive Layered Extraction : A Novel Multi-Task Learning Model for Personalized Recommendations&#xff09;论文的阅读笔记 背景模型实验 论文地址&#xff1a;SAFE 背景 在此之前&#xff0c;对利用新闻文章中文本信息和视觉信息之间的关系(相似…

在Ubuntu22.04上部署ComfyUI

ComfyUI 是 一个基于节点流程的 Stable Diffusion 操作界面&#xff0c;可以通过流程&#xff0c;实现了更加精准的工作流定制和完善的可复现性。每一个模块都有特定的的功能&#xff0c;我们可以通过调整模块连接达到不同的出图效果&#xff0c;特点如下&#xff1a; 1.对显存…

二十、K8S-1-权限管理RBAC详解

目录 k8s RBAC 权限管理详解 一、简介 二、用户分类 1、普通用户 2、ServiceAccount 三、k8s角色&角色绑定 1、授权介绍&#xff1a; 1.1 定义角色&#xff1a; 1.2 绑定角色&#xff1a; 1.3主体&#xff08;subject&#xff09; 2、角色&#xff08;Role和Cluster…

【正在更新】从零开始认识语音识别:DNN-HMM混合系统语音识别(ASR)原理

摘要 | Abstract TO-BE-FILLED 1.前言 | Introduction 近期想深入了解语音识别(ASR)中隐马尔可夫模型(HMM)和深度神经网络-隐马尔可夫(DNN-HMM)混合模型&#xff0c;但是尽管网络上有许多关于DNN-HMM的介绍&#xff0c;如李宏毅教授的《深度学习人类语言处理》[1]&#xff0c;…

Hive-架构与设计

架构与设计 一、背景和起源二、框架概述1.设计特点 三、架构图1.UI交互层2.Driver驱动层3.Compiler4.Metastore5.Execution Engine 四、执行流程1.发起请求2.获取执行计划3.获取元数据4.返回元数据5.返回执行计划6.运行执行计划7.运行结果获取 五、数据模型1.DataBase数据库2.T…

Elasticsearch 通信模块的分析

Elasticsearch 通信模块的分析 - 知乎 Elasticsearch是一个基于Lucene的分布式实时搜索框架&#xff0c;它本身能够接受用户发来的http 请求&#xff0c; 集群节点之间也会有相关的通信。 通信模块的简介 Elasticsearch 中的通信相关的配置都是由NetworkModule 这个类完成的…

【深度学习】:实验6布置,图像自然语言描述生成(让计算机“看图说话”)

清华大学驭风计划 因为篇幅原因实验答案分开上传&#xff0c;深度学习专栏持续更新中&#xff0c;期待的小伙伴敬请关注 实验答案链接http://t.csdnimg.cn/bA48U 有任何疑问或者问题&#xff0c;也欢迎私信博主&#xff0c;大家可以相互讨论交流哟~~ 案例 6 &#xff1a;图像自…

【RabbitMQ(二)】:Exchange 详解 | Message Convert 消息转换器

文章目录 03. 使用 Java 代码去操控 RabbitMQ3.1 快速入门3.1.1 创建父子项目3.1.2 编写代码 3.2 Work 模型3.3 RabbitMQ 中的三类交换机3.3.1 Fanout 扇出交换机3.3.2 Direct 交换机3.3.3 Topic 交换机 3.4 声明队列交换机3.4.1 方式一&#xff1a;书写 Config 类3.4.2 方式二…

如何将 Hexo 部署到 GitHub Pages

引言 在数字时代&#xff0c;拥有个人博客是展示自己想法、分享知识和技能的绝佳方式。Hexo 是一个基于 Node.js 的静态博客生成器&#xff0c;它结合了简洁性和功能性&#xff0c;让我们可以轻松地建立并维护一个博客。而 GitHub Pages 提供了一个免费的平台来托管这些静态网站…

推荐几个Python爬虫接单渠道

前言 平时工作有闲的家人们&#xff0c;今天给大家推荐一些用Python爬虫做私活的渠道&#xff01; 【Python爬虫学习资料】 先给各位还不熟悉Python爬虫的朋友介绍一下&#xff01; 可以短时间获得大量资料~ 可以进一步数据分析 当然也可以获得收益&#xff01; 学会Python…

物资捐赠管理系统

文章目录 物资捐赠管理系统一、项目演示二、项目介绍三、系统部分功能截图四、部分代码展示五、底部获取项目&#xff08;9.9&#xffe5;带走&#xff09; 物资捐赠管理系统 一、项目演示 爱心捐赠系统 二、项目介绍 基于springboot的爱心捐赠管理系统 开发语言&#xff1a…

Spring基础 - SpringMVC请求流程和案例

Spring基础 - SpringMVC请求流程和案例 什么是MVC 用一种业务逻辑、数据、界面显示分离的方法&#xff0c;将业务逻辑聚集到一个部件里面&#xff0c;在改进和个性化定制界面及用户交互的同时&#xff0c;不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理…

MYSQL学习笔记:mysql运算符

MYSQL学习笔记&#xff1a;mysql运算符 select * from user where score in (99,100); select * from user where name like zhang%;通配符放到后面或者中间是可以利用索引的&#xff0c;但是通配符放到开头没法用到索引

社区店营销新趋势:如何吸引并留住顾客?

作为一名资深的鲜奶吧创业者&#xff0c;我已经在这个行业摸爬滚打了五年。 这五年的时间&#xff0c;我见证了社区店营销的变迁&#xff0c;也积累了一些关于如何吸引并留住顾客的经验。今天&#xff0c;我想和大家分享一些留住顾客的核心干货。&#xff08;可以点赞收藏&…

统一数据格式返回,统一异常处理

目录 1.统一数据格式返回 2.统一异常处理 3.接口返回String类型问题 1.统一数据格式返回 添加ControllerAdvice注解实现ResponseBodyAdvice接口重写supports方法&#xff0c;beforeBodyWrite方法 /*** 统一数据格式返回的保底类 对于一些非对象的数据的再统一 即非对象的封…

Idea Git Review插件

idea git plugin 添加了一些常用的小插件 可以右键打开git bash窗口 可以右键选中文字点击baidu fanyi 可以通过搜索git用户名 指定开始时间查询某个版本自己提交的所有代码文件 可以通过点击蓝色行数&#xff0c;跳转到指定的改动代码块 资源地址&#xff1a; git-pl…

Python贝尔多项式

文章目录 Bell数和Bell多项式第二类Bell多项式 Bell数和Bell多项式 Bell&#xff0c;即所有包含 n n n个对象的有限集合的子集数之和&#xff0c;可通过递推式进行定义 B n ∑ k 0 n − 1 ( n − 1 k ) B k , B 0 1 B_n\sum^{n-1}_{k0}\begin{pmatrix} n-1\\k \end{pmatrix…