分布式锁(redisson,看门狗,主从一致性)
目录
- 分布式锁
- 一:基本原理和实现方式
- 二:分布式锁的实现
- 1:分布式锁的误删问题
- 2:解决误删问题
- 三:lua脚本解决多条命令原子性问题
- 调用lua脚本
- 四:Redisson
- 1:redisson入门
- 2:redisson可重入锁的原理
- 3:解决尝试等待,超时释放的问题(看门狗)
- 4:主从一致性问题
分布式锁
一:基本原理和实现方式



使用redis实现分布式锁:

我们实现分布式锁有两个基本方法:
一个是获取锁
一个是删除锁
获取我们使用setnx,只有不存在才会set成功作为互斥锁;
删除直接删除这个锁的键就行了;
但是要考虑一种情况,就是获取锁之后服务宕机了,那么就无法释放锁,也就会出现死锁,服务挂了,为了预防这种情况的发生我们要设置过期时间,就算服务宕机,过了过期时间锁也会释放;
还有一种情况就是设置锁,还没去设置锁的过期时间这个时候服务就宕机了,那么也会出现死锁,我们要保证获取锁和设置过期时间这个操作的原子性,redis种就提供了方法可以同时设置过期时间和互斥锁:
set lock t1 nx ex 10


二:分布式锁的实现

我们去实现这个接口:
public class SimpleRedisLock implements ILock{//锁的名称private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}//锁的前缀是个常量private static final String KEY_VALUE ="lock:";/*** 获取锁* @param timeoutSec* @return*/@Overridepublic boolean trylock(long timeoutSec) {//获取当前线程id作为锁的valuelong id = Thread.currentThread().getId();//设值尝试获取锁,true是成功,false是失败Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_VALUE + name, id + "", timeoutSec, TimeUnit.SECONDS);//因为直接返回b的话jvm会自动装箱拆箱,可能会造成空指针异常,如果b为true返回就是true,如果b为false返回就是false,如果为空返回也是false;return Boolean.TRUE.equals(b);}/*** 释放锁*/@Overridepublic void unlock() {stringRedisTemplate.delete(KEY_VALUE + name);}
}
我们在一人一单的问题使用分布式锁:
Long id = UserHolder.getUser().getId();
SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order" + id, stringRedisTemplate);
boolean trylock = simpleRedisLock.trylock(1200);
if (!trylock){return Result.fail("一人只能下一单");
}
try {IVoucherOrderService orderService = (IVoucherOrderService) AopContext.currentProxy();return orderService.createVoucherOrder(voucherId);
} catch (IllegalStateException e) {throw new RuntimeException(e);
}finally {simpleRedisLock.unlock();
}
1:分布式锁的误删问题

存在的问题是这样的:
线程1去获取锁,但是呢,业务阻塞超时了,锁就自动释放了,这个时候线程2来了,因为锁释放了,线程2可以获取到锁,就在这时,线程1阻塞通过了,完成了业务之后就要释放锁了,这个时候释放的锁是线程2的锁,线程2还在执行,这个时候线程3也去获取锁成功了,那么就出现了线程2,3并发执行的情况,造成并发安全问题;
这里的问题是线程释放锁释放的不是自己的锁,所以我们解决办法就是在线程释放的时候判断一下是不是自己的锁,怎么判断呢,我们之前获取锁的时候存入的value是线程的id,这个就是线程的唯一标识,我们只需要判断当前线程id和存入的value是否一致就行;
2:解决误删问题

//锁的前缀是个常量
private static final String KEY_VALUE ="lock:";
//加上线程的前缀,保证不同集群不同线程的id是唯一
public static final String ID_VALUE= UUID.randomUUID().toString(true);/*** 获取锁* @param timeoutSec* @return*/
@Override
public boolean trylock(long timeoutSec) {//获取当前线程id作为锁的valueString id =ID_VALUE+ Thread.currentThread().getId();//设值尝试获取锁,true是成功,false是失败Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_VALUE + name, id + "", timeoutSec, TimeUnit.SECONDS);//因为直接返回b的话jvm会自动装箱拆箱,可能会造成空指针异常,如果b为true返回就是true,如果b为false返回就是false,如果为空返回也是false;return Boolean.TRUE.equals(b);
}
只要在锁获取的时候给线程加上唯一标识就行,我们原来用的是线程id,这样不能保证唯一,因为线程id是在同一个jvm内部是递增的,不同jvm的线程id可能相同,那么就需要加上前缀保证线程标识的唯一性,那么就可以使用uuid来保证每一台jvm的uuid是不同的,标识就是唯一的;
释放锁:
@Override
public void unlock() {if (stringRedisTemplate.opsForValue().get(KEY_VALUE + name).equals(ID_VALUE+ Thread.currentThread().getId())){stringRedisTemplate.delete(KEY_VALUE + name);}
}
判断锁中的val和当前线程的标识是否一致;
这样就解决了误删的问题;
但是还有一个问题:
假设一个线程1获取锁之后执行业务,执行完之后,要释放锁,判断了当前锁是否是自己的(锁的value),在要执行删除操作的时候堵塞了,锁超时释放了,这个时候线程2获取锁,执行业务,就在这时,线程1阻塞结束,执行的代码在释放锁上,因为同一个业务锁的key都是一样的,所以线程一能够把锁释放,这时线程2还没执行结束,线程3来了,就出现了线程安全问题;
所以说我们要将判断锁和释放锁作为一个原子性事件,要么同时发生要么同时失败
这就要用lua了
三:lua脚本解决多条命令原子性问题



--获取当前key
local key =KEY[1]
--获取当前线程标识
local id =ARGV[1]--获取锁中的标识
local _id=redis.call('get',key)
--判断是否是同一个标识:
if(id==_id) thenreturn redis.call("del",key)
end
return 0

在idea中我们执行lua
脚本我们可以在resouce中创建一个lua文件:

要安装插件才行:
插件名叫emmylua;
调用lua脚本
//声明一个DefaultRedisScript,原来调取lua脚本
public static final DefaultRedisScript UNLOCK_LUA;
static {//初始化UNLOCK_LUA=new DefaultRedisScript<>();//定位到lua脚本的位置UNLOCK_LUA.setLocation(new ClassPathResource("unlock.lua"));//设置lua脚本的返回值UNLOCK_LUA.setResultType(Long.class);
}
@Override
public void unlock() {// Collections.singletonList创建单元素的集合stringRedisTemplate.execute(UNLOCK_LUA,Collections.singletonList(KEY_VALUE + name),(ID_VALUE+ Thread.currentThread().getId()));
}
这样将判断标识和释放锁的命令写在lua脚本中,就能够保证命令执行的原子性,就不会出现之前说的误删的情况
我们实现分布式锁的思路:
获取锁:使用setnx,互斥的特性,获取值相当于设置锁,使用uuid保证线程的唯一性;
释放锁:释放之前判断锁释放是当前线程的锁,并且将判断锁和释放锁的命令写在一个lua脚本中保证执行命令的原子性;

四:Redisson


1:redisson入门


步骤1:
引入依赖:
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.33.0</version>
</dependency>
步骤二:
配置客户端:
@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){Config config = new Config();config.useSingleServer().setAddress("redis://123.249.86.145:6379").setPassword("12345");return Redisson.create(config);}
}
2:redisson可重入锁的原理



可重入锁的逻辑:
获取锁:如果锁不存在就获取锁,然后设置过期时间,如果存在了,就判断锁的线程标识是否是自己,如果是自己,那么就让锁的value加一;
释放锁:释放锁的时候判断锁是否是自己的,如果不是自己的就返回空,如果是自己的就让value-1,然后再判断value是否大于0,如果大于0,就重置锁的时间,等于0就删除锁
3:解决尝试等待,超时释放的问题(看门狗)

redisson内部是如何解决重复尝试的呢?尝试解读一下:首先线程会尝试获取锁,获取锁成功就返回空,获取锁失败就返回ttl,也就是锁的超时释放时间,我们重复尝试肯定是建立在获取锁失败的基础上,当返回的是ttl时,我们会判断当前时间减去我们尝试获取锁的时刻的时间,也就是尝试获取锁使用的时间,是否大于我们设置的等待时间,如果大于我们设置的等待时间,那么就尝试获取锁失败,就返回false,如果还有时间,我们不是直接再去获取锁,而是通过订阅机制,我们等待释放锁的信号,如果等待时间超时就直接返回false,如果没抄时,我们在重新尝试锁,失败在判断是否还有时间然后定义然后重试,就这么循环下去,要么拿到锁,要么超过等待时间获取锁失败;这样就解决我们获取锁失败一次就返回失败,可以一直尝试;
然后再来讲一下超时释放问题,也就是过了设置的超时时间,业务还没执行完,锁就释放了,出现了并发安全问题,怎么解决的呢,我们如果没有设置这个锁的超时释放时间,那么redisson就会自动给我们的超时释放时间设置为-1,这个时候才会开启看门狗机制,具体是什么情况呢,就是内部会开启一个任务,这个任务会一直刷新锁的超时时间,一直无限的刷新,每10秒刷新一次,刷新一次延长30秒,也就会一直存在这个锁,知道完成任务释放锁才会取消这个看门狗任务;
那么释放锁的时候如果释放失败就返回异常,释放成功就要做两件事,一个是发送释放锁的信息给正在订阅尝试获取锁的,还有一件事就是将看门狗任务终止;

4:主从一致性问题

主从一致性问题:在redis集群中有主从关系,一个主节点有多个从节点,当主节点宕机时,会选一个从节点作为主节点;
主节点和从节点为了数据一样会做主从同步操作;有一个问题就是当java应用向主节点发送请求获取锁,获取锁成功了,但是在主从同步还没完成的时候,主节点宕机了,这个时候哨兵就会发现宕机,然后从从节点中选出一个作为主节点,而因为数据未同步,新的主节点中没有锁,也就是锁失效了,那么其他线程发送请求就能获取锁,这就是锁失效的问题

如何解决呢:我们不设置主从关系,只设置节点,每个节点都相当与主节点,而且获取锁的时候,必须要所有的节点都获取锁成功才算获取锁成功,如果有一个节点宕机了,他的从节点成为了新的节点,这个时候有线程来获取锁也是不成功的,因为要在所有节点中获取锁,而其他节点已经持有锁了;这就解决了主从一致性问题;
,获取锁成功了,但是在主从同步还没完成的时候,主节点宕机了,这个时候哨兵就会发现宕机,然后从从节点中选出一个作为主节点,而因为数据未同步,新的主节点中没有锁,也就是锁失效了,那么其他线程发送请求就能获取锁,这就是锁失效的问题
[外链图片转存中…(img-1BgOGPaG-1730426881642)]
如何解决呢:我们不设置主从关系,只设置节点,每个节点都相当与主节点,而且获取锁的时候,必须要所有的节点都获取锁成功才算获取锁成功,如果有一个节点宕机了,他的从节点成为了新的节点,这个时候有线程来获取锁也是不成功的,因为要在所有节点中获取锁,而其他节点已经持有锁了;这就解决了主从一致性问题;

相关文章:
分布式锁(redisson,看门狗,主从一致性)
目录 分布式锁一:基本原理和实现方式二:分布式锁的实现1:分布式锁的误删问题2:解决误删问题 三:lua脚本解决多条命令原子性问题调用lua脚本 四:Redisson1:redisson入门2:redisson可重…...
openEuler 服务器Python自动化安装WEB服务器和文件上传服务(1)
一、系统准备 我们的服务器采用了 openEuler 22.03 (LTS-SP4) 的初始化服务器模式安装 二、安装步骤 (一)安装依赖库 在终端中运行以下命令确保系统安装了必要的依赖: sudo dnf install -y python3上述 Python 脚本中的依赖库会在运行 Py…...
【Python游戏开发】石头剪刀布游戏(附完整Python完整代码)
石头剪刀布游戏:Pygame实现 结果图前言核心函数思考步骤实现原理和公式代码实现结论结果图 前言 石头剪刀布是一种经典的猜拳游戏,简单易玩但却蕴含着一定的策略性。本文将介绍如何使用Python和Pygame库开发一个简单的石头剪刀布游戏,并探讨其中的核心功能实现和思考过程。 …...
ctfshow(94,95)--PHP特性--strpos函数
建议先学习intval函数相关内容 Web94 源代码: include("flag.php"); highlight_file(__FILE__); if(isset($_GET[num])){$num $_GET[num];if($num"4476"){die("no no no!");}if(preg_match("/[a-z]/i", $num)){die(&qu…...
C++ --- 多线程的使用
目录 一.什么是线程? 线程的特点: 线程的组成: 二.什么是进程? 进程的特点: 进程的组成: 三.线程与进程的关系: 四.C的Thread方法的使用: 1.创建线程: 2.join(…...
百度笔试(10.29)
判断s字符串是否可以通过添加某些字符变成t字符串,s是否是t的子序列 s input() n int(input()) st set() res [] for idx in range(n):t input()if t in st or len(t) < len(s):continuest.add(t)i,j 0,0while i < len(s) and j < len(t):if s[i] …...
数据库版本更新后,如何迁移数据?
数据库版本更新后迁移数据是一个需要谨慎处理的过程。以下是一般步骤和最佳实践: 1. **备份数据**: 在进行任何迁移之前,确保对现有数据库进行完整备份。这可以是物理备份(如数据库文件的拷贝)或逻辑备份ÿ…...
Chrome与火狐的安全功能全面评估
在当今数字化时代,网络安全已成为用户最为关注的问题之一。作为两款广受欢迎的浏览器,Chrome和火狐(Firefox)都提供了多种安全功能来保护用户的在线隐私和数据安全。本文将全面评估这两款浏览器的安全功能,帮助用户更好…...
微服务设计模式 - 重试模式(Retry Pattern)
微服务设计模式 - 重试模式(Retry Pattern) 定义 重试模式(Retry Pattern)是一种微服务中的设计模式,用于在临时性失败(如网络故障或暂时不可用的服务)发生时,自动重新尝试请求&…...
DNS配置
1.搭建dns服务器能够对自定义的正向或者反向域完成数据解析查询。 2.配置从DNS服务器,对主dns服务器进行数据备份。 正反向解析 [rootlocalhost ~]# vim /etc/named.conf options {listen-on port 53 { 192.168.111.130; };directory "/var/named&quo…...
【Linux指令】---获取进程的PID
获取进程的PID getpid()函数...
在centos中安装cmake
安装依赖工具: sudo yum install -y epel-release sudo yum groupinstall -y "Development Tools" sudo yum install -y wget wget 下载 CMake 3.24 的压缩包: wget https://github.com/Kitware/CMake/releases/download/v3.24.0/cmake-3.24.0-linux-x86_64.tar.gz …...
【补补漏洞吧 | 02】等保测评ZooKeeperElasticsearch未授权访问漏洞补漏方法
一、项目背景 客户新系统上线,因为行业网络安全要求,需要做等保测评, 通过第三方漏扫工具扫描系统,漏扫报告显示ZooKeeper和 Elasticsearch 服务各拥有一个漏洞,具体结果如下: 1、ZooKeeper 未授权访问【…...
Docker Compose一键部署Spring Boot + Vue项目
目录 前提条件 概述 Compose简介 Compose文件 Compose环境 Compose命令 帮助命令 关键命令 Compose部署项目 初始化环境 查看代码文件 sql数据准备 nginx配置文件准备 创建 compose.yaml 一键启动compose多个容器 浏览器访问虚拟机ip:80(可省略默认的80端口) …...
【maven】idea执行了maven的install命令给本地安装项目依赖包 安装后删除
目录 事件起因环境和工具操作过程解决办法1、找到对应的目录下的文件,手动去删除,比如我的依赖库的路径是D:\qc_code\apache-maven-3.8.2\repository 我只需要找到这个目录下对应的依赖包进行手动删除即可(不推荐,强行删除文件夹文…...
Android中的Handle底层原理
先上结论: 一个小小的Handle,就能看出你对Android底层源码理解的深浅。 老规矩,如果你能看懂我上面画的这张草图,这篇文章你可以直接跳过! 开干! 1、先对Handle在Android系统中是如何发挥作用的做一个小结: 1)每一个线程,包括UI主线程,有一个ThreadLocalMap变量:…...
最简单方式SSH连接局域网中另一台电脑的WSL2
1、首先确认一下WSL2中的SSH服务是否开启: 先安装更新一下,再安装一下ssh服务模块,这里很多人都每安装过。 sudo apt update sudo apt upgrade sudo apt install openssh-server 2、配置SSH服务器: 打开配置文件,这…...
热点聚焦:AI在医疗保健领域的深度渗透与变革
内容概要 随着人工智能技术的不断进步,我们正在见证一个充满奇迹的转变,尤其是在医疗保健领域。这种转变不仅仅涉及到提高效率,更在于重新定义我们对疾病诊断和治疗方案的理解。通过智能算法,AI能够在早期识别潜在的健康问题&…...
模板方法模式:定义算法框架的设计模式
1. 引言 在软件开发中,算法的实现通常是可变的,但其基本步骤往往是相对固定的。模板方法模式(Template Method Pattern)正是为了解决这一问题而设计的,它通过定义一个操作中的算法框架,将一些步骤的实现延…...
[Redis] Redis事务
🌸个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 🏵️热门专栏: 🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 🍕 Collection与…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...
JDK 17 新特性
#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持,不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的ÿ…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
C# 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
Java 二维码
Java 二维码 **技术:**谷歌 ZXing 实现 首先添加依赖 <!-- 二维码依赖 --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.1</version></dependency><de…...
云原生玩法三问:构建自定义开发环境
云原生玩法三问:构建自定义开发环境 引言 临时运维一个古董项目,无文档,无环境,无交接人,俗称三无。 运行设备的环境老,本地环境版本高,ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...
VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP
编辑-虚拟网络编辑器-更改设置 选择桥接模式,然后找到相应的网卡(可以查看自己本机的网络连接) windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置,选择刚才配置的桥接模式 静态ip设置: 我用的ubuntu24桌…...
Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
【JVM】Java虚拟机(二)——垃圾回收
目录 一、如何判断对象可以回收 (一)引用计数法 (二)可达性分析算法 二、垃圾回收算法 (一)标记清除 (二)标记整理 (三)复制 (四ÿ…...
HubSpot推出与ChatGPT的深度集成引发兴奋与担忧
上周三,HubSpot宣布已构建与ChatGPT的深度集成,这一消息在HubSpot用户和营销技术观察者中引发了极大的兴奋,但同时也存在一些关于数据安全的担忧。 许多网络声音声称,这对SaaS应用程序和人工智能而言是一场范式转变。 但向任何技…...
