当前位置: 首页 > news >正文

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搞好先

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

相关文章:

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;…...

Spring Boot 笔记 021 项目部署

1.1 引入坐标&#xff0c;并双击package打包成jar包 1.2 在服务器上运行jar包 1.3 使用postman测试 2.1 运行配置 2.1.1 命令更改端口 java -jar big-event-1.0-SNAPSHOT.jar --server.port7777 2.1.2 环境变量更新&#xff08;略&#xff09; 2.1.3 外部配置文件&#xff0c…...

新技术革命开始了,Sora一出,所有的视频人、电影人都下岗

Sora一出&#xff0c;所有的视频人、电影人都下岗&#xff01; Sora直接用文本制作长达60秒的视频长镜头&#xff0c;也就是说&#xff0c;将来&#xff0c;只需要输入分镜脚本&#xff0c;电影就可以制作出来&#xff0c;不再需要几十人几百人声势浩大地去“拍”了&#xff0c…...

【FPGA开发】Modelsim和Vivado的使用

本篇文章包含的内容 一、FPGA工程文件结构二、Modelsim的使用三、Vivado的使用3.1 建立工程3.2 分析 RTL ANALYSIS3.2.1 .xdc约束&#xff08;Constraints&#xff09;文件的产生 3.3 综合 SYNTHESIS3.4 执行 IMPLEMENTATION3.5 烧录程序3.6 程序固化3.6.1 SPI约束3.6.2 .bin文…...

现代浏览器对 es模块 【esm】原生支持

现代浏览器对 ES&#xff08;ECMAScript&#xff09;模块的原生支持是指浏览器可以直接解析和执行 JavaScript 文件中的 ES 模块语法&#xff0c;无需额外的工具或转换。 具体来说&#xff0c;当浏览器遇到 import 和 export 关键字时&#xff0c;会将其识别为 ES 模块语法&…...

修改SpringBoot中默认依赖版本

例如SpringBoot2.7.2中ElasticSearch版本是7.17.4 我希望把它变成7.6.1...

网络安全最典型基础靶场-DVWA-本地搭建与初始化

写在前面&#xff1a; 之前也打过这个 DVWA 靶场&#xff0c;但是是在虚拟机环境下的一个小块分区靶场&#xff1b; 本篇博客主要介绍在本地搭建 DVWA 靶场以及靶场的初始化&#xff0c;后续会陆续更新通关教程。 由于我们是在本地搭建&#xff0c;则需要基于你已经装好 phpstu…...

算法-----高精度2(高精度乘法,高精度除法,高精度斐波那锲数列)

高精度乘法 对于高精度乘法来说似乎不像高精度加减法那样简单了&#xff0c;我们似乎得一个一个加了&#xff0c;因为我们都知道 abaaaaa…a(b个a)。如果真要这要的话那1e9*1e9不得超时啊&#xff0c;所以不能这样&#xff0c;我们还是得从乘法竖式入手 这样看似乎看不出来什…...

windows vs 自己编译源码 leveldb 然后使用自己编译的文件

1 准备源码文件 1.1 第一种方法 git下载源码 vs项目中git leveldb源码和git third_party googletest-CSDN博客 1.2 第二种方法 手动下载 然后把第三方的源码下载 复制到 third_party 对应的文件夹中 没有文件夹 third_party -> powershell mkdir third_party 2 编译lev…...

基于GPT一键完成数据分析全流程的AI Agent: Streamline Analyst

大型语言模型&#xff08;LLM&#xff09;的兴起不仅为获取知识和解决问题开辟了新的可能性&#xff0c;而且催生了一些新型智能系统&#xff0c;例如旨在辅助用户完成特定任务的AI Copilot以及旨在自动化和自主执行复杂任务的AI Agent&#xff0c;使得编程、创作等任务变得高效…...

C语言-----习题

1.通过这个例题&#xff0c;我们可以知道*p.a是无法打印99的&#xff0c;因为.的优先级比解引用*高&#xff1b; ​ struct S {int a;int b; }; int main() {struct S a, * p &a;//可以分为两部分理解//struct S a;//struct S *p &a;a.a 99;printf("%d\n"…...

Java学习笔记(五)

目录 一、控制结构 1.1 顺序控制 1.2 分支控制 &#xff08;一&#xff09;单分支 &#xff08;二&#xff09;双分支 &#xff08;三&#xff09;多分支 &#xff08;四&#xff09;嵌套分支 &#xff08;五&#xff09;switch分支 1.3 循环控制 &#xff08;一&…...

4.【Linux】进程控制(进程终止||进程等待||程序替换)

一.进程创建fork 见上篇文章 二.进程的终止 1.进程退出场景 1.代码运行完毕&#xff0c;结果正确&#xff0c;通过main函数退出码返回一般为0。 2.代码运行完毕&#xff0c;结果不正确&#xff0c;通过不同的退出码标识不同的错误原因。 3.代码异常终止&#xff08;信号&am…...

微服务设计:Spring Cloud 链路追踪概述

Spring Cloud 链路追踪是指在分布式系统中追踪请求路径的技术。它可以帮助开发者了解请求在各个微服务之间是如何流转的&#xff0c;以及每个微服务处理请求所花费的时间。链路追踪可以用于解决以下问题&#xff1a; 性能分析: 识别性能瓶颈&#xff0c;优化微服务性能。故障排…...

【MySQL/Redis】如何实现缓存一致

目录 不实用的方案 1. 先写 MySQL , 再写 Redis 2. 先写 Redis &#xff0c; 再写MySQL 3. 先删 Redis&#xff0c;再写 MySQL 实用的方案 1. 先删 Redis&#xff0c;再写 MySQL, 再删 Redis 2. 先写 MySQL , 再删 Redis 3. 先写MySQL&#xff0c;通过BinLog&#xff0…...

Socket.D 开源输传协议 v2.4.0 发布

Socket.D 协议 是基于"事件"和"语义消息""流"的网络应用层传输协议。有用户说&#xff0c;“Socket.D 之于 Socket&#xff0c;尤如 Vue 之于 Js、Mvc 之于 Http”。支持 tcp, udp, ws, kcp 传输。协议特点可参考《官网介绍》。 pyton 已开发完…...

单片机学习笔记---AT24C02数据存储

目录 AT24C02数据存储 准备工作 代码讲解 I2C.c 模拟起始位置的时序 模拟发送一个字节的时序 模拟接收应答的时序 模拟接收一个字节的时序 模拟发送应答的时序 模拟结束位置的时序 I2C.h AT24C02.c 字节写&#xff1a;在WORD ADDRESS&#xff08;字地址&#xff…...

首次安装Mysql数据库

1、在mysql官网下载自己需要的版本 2、选择安装类型 3、 检查一下需求版本 4、 这里可能会弹出如下信息,先不用管这一步,点击Yes继续即可 5、 安装需要的环境,点击执行就可以,此过程会比较慢 如下就是全面安装完成了,点击next即可...

2024 前端面试题(GPT回答 + 示例代码 + 解释)No.1 - No.20

本文题目来源于全网收集&#xff0c;答案来源于 ChatGPT 和 博主&#xff08;的小部分……&#xff09; 格式&#xff1a;题目 h3 回答 text 参考大佬博客补充 text 示例代码 code 解释 quote 补充 quote 目录 No.1 - No.20 本文题目来源于全网收集&#xff0c;答案来源于…...

通过`ssh`同步`tmux`剪贴板内容

通过ssh同步tmux剪贴板内容 通过ssh连接远程服务器时&#xff0c;可以通过xclip同步tmux剪贴板内容。这需要在服务器上安装xclip&#xff0c;且需要在ssh远程连接时开启X11。 此处附tmux剪贴板调用xclip的配置&#xff1a; # Copy the current buffer to the system clipboa…...

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…...

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

java_网络服务相关_gateway_nacos_feign区别联系

1. spring-cloud-starter-gateway 作用&#xff1a;作为微服务架构的网关&#xff0c;统一入口&#xff0c;处理所有外部请求。 核心能力&#xff1a; 路由转发&#xff08;基于路径、服务名等&#xff09;过滤器&#xff08;鉴权、限流、日志、Header 处理&#xff09;支持负…...

label-studio的使用教程(导入本地路径)

文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

边缘计算医疗风险自查APP开发方案

核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

(二)原型模式

原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...

相机从app启动流程

一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...

Linux云原生安全:零信任架构与机密计算

Linux云原生安全&#xff1a;零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言&#xff1a;云原生安全的范式革命 随着云原生技术的普及&#xff0c;安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测&#xff0c;到2025年&#xff0c;零信任架构将成为超…...

LLM基础1_语言模型如何处理文本

基于GitHub项目&#xff1a;https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken&#xff1a;OpenAI开发的专业"分词器" torch&#xff1a;Facebook开发的强力计算引擎&#xff0c;相当于超级计算器 理解词嵌入&#xff1a;给词语画"…...

Android15默认授权浮窗权限

我们经常有那种需求&#xff0c;客户需要定制的apk集成在ROM中&#xff0c;并且默认授予其【显示在其他应用的上层】权限&#xff0c;也就是我们常说的浮窗权限&#xff0c;那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...