Redis分布式锁--java实现

文章目录

  • Redis分布式锁
    • 方案:SETNX + EXPIRE
      • 基本原理
      • 比较好的实现
      • 会产生四个问题
    • 几种解决原子性的方案
      • 方案:SETNX + value值是(系统时间+过期时间)
      • 方案:使用Lua脚本(包含SETNX + EXPIRE两条指令)
      • 方案:SET的扩展命令(SET EX PX NX)
      • 会出现的问题
    • 方案: 开源框架:Redisson
    • 方案:多机实现的分布式锁Redlock
    • 总结
    • 参考链接:

Redis分布式锁

方案:SETNX + EXPIRE

基本原理

SETNX 是SET IF NOT EXISTS的简写.日常命令格式是SETNX key value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。
redis语法:
使用 SETNX 命令设置键,值为锁的标识
键名为 lock_key,值为锁标识 lock_value
SETNX 返回 1 表示成功设置,0 表示键已存在
SETNX lock_key lock_value
使用 EXPIRE 命令设置键的过期时间(例如,设置为 10 秒)
EXPIRE lock_key 10

用java来实现:

 ValueOperations<String, String> valueOps = redisTemplate.opsForValue();// 使用 setIfAbsent 方法设置键值对,仅当键不存在时才进行设置boolean isSet = valueOps.setIfAbsent(key, value);if (isSet) {// 如果成功设置,再使用 expire 方法设置过期时间redisTemplate.expire(key, ttlInSeconds, java.util.concurrent.TimeUnit.SECONDS);System.out.println("Key set successfully with TTL!");} else {System.out.println("Key already exists, not set.");}

比较好的实现

  private static final String LOCK_SUCCESS = "OK";private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "PX";/*** 尝试获取分布式锁* @param jedis Redis客户端* @param lockKey 锁* @param requestId 请求标识* @param expireTime 超期时间* @return 是否获取成功*/public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);if (LOCK_SUCCESS.equals(result)) {return true;}return false;}

第一个为key,我们使用key来当锁,因为key是唯一的。
第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,代表key的过期时间。

会产生四个问题

1、setnx和expire两个命令分开了,不是原子操作。如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash或者要重启维护了,那么这个锁就“长生不老”了,别的线程永远获取不到锁啦。
2、超时解锁会导致并发问题,如果两个线程同时要获取锁,此时所恰好过期,此时这两个线程能够同时进入,产生并发问题
3、不可重入,同一个线程想要同时获得这个锁两次实际是是不支持的
4、无法等待锁释放,在没有获取锁的时候会直接返回,没有等待时间。

其中原子性问题是一个最重要的问题,因此有了下面的解决方案。

几种解决原子性的方案

方案:SETNX + value值是(系统时间+过期时间)

实际上是一种逻辑过期时间,将过期的时间作为value用setnx的value值里,如果加锁失败则拿出value校验一下即可。
java实现

long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {return true;
}
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)String oldValueStr = jedis.getSet(key_resource_id, expiresStr);if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁return true;}
}
//其他情况,均返回加锁失败
return false;
}

方案:使用Lua脚本(包含SETNX + EXPIRE两条指令)

实现原理,调用lua代码,一个lua脚本是原子的。

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 thenredis.call('expire',KEYS[1],ARGV[2])
elsereturn 0
end;

java代码:

 String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +" redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";   
Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
//判断是否成功
return result.equals(1L);

方案:SET的扩展命令(SET EX PX NX)

保证SETNX + EXPIRE两条指令的原子性,我们还可以巧用Redis的SET指令扩展参数
例如:SET key value[EX seconds][PX milliseconds][NX|XX],它也是原子性的!
语法如下:

SET key value[EX seconds][PX milliseconds][NX|XX]NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
EX seconds :设定key的过期时间,时间单位是秒。
PX milliseconds: 设定key的过期时间,单位为毫秒
XX: 仅当key存在时设置值

java代码:

if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁try {do something  //业务处理}catch(){}finally {jedis.del(key_resource_id); //释放锁}
}

会出现的问题

这种方案能解决方案一的原子性问题,但是依然会存在很大的问题,如下所示:
1、时钟不同步:如果不同的节点的系统时钟不同步,可能导致锁的过期时间计算不准确。
解决方案:使用相对时间而非绝对时间,或者使用时钟同步工具确保系统时钟同步。
2、死锁:在某些情况下,可能出现死锁,例如由于网络问题导致锁的释放操作未能执行。
解决方案:使用带有超时和重试的锁获取和释放机制,确保在一定时间内能够正常操作。
3、锁过期与业务未完成:如果业务逻辑执行时间超过了设置的过期时间,锁可能在业务未完成时自动过期,导致其他客户端获取到锁。
解决方案:可以设置更长的过期时间,确保业务有足够的时间完成。或者在业务未完成时,通过更新锁的过期时间来延长锁的生命周期。
4、锁的争用:多个客户端同时尝试获取锁,可能导致锁的频繁争用。
解决方案:可以使用带有重试机制的获取锁操作,或者采用更复杂的锁实现,如 Redlock 算法。
5、锁的释放问题:客户端获取锁后发生异常或未能正常释放锁,可能导致其他客户端无法获取锁。
解决方案:使用 SET 命令设置锁的值,并在释放锁时检查当前值是否匹配。只有匹配时才执行释放锁的操作。
6、锁被别的线程误删:假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完。
解决方案:SET EX PX NX + 校验唯一随机值,给value值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下,需要用乱

方案: 开源框架:Redisson

总结一下上面的解决问题的历程和问题,用SETNX+EXPIRE可以解决分布式锁的问题,但是这种方式不是原子性操作。因此,在提出的三种原子性操作解决方法,但是依然会出现几个问题,在会出现的问题中简单罗列了几种问题与解决方法,其中问题3中有锁过期与业务未完成有一个系统的解决方案,即接下来介绍的Redison。
Redisson 是一个基于 Redis 的 Java 驱动库,提供了分布式、高性能的 Java 对象操作服务,这里只探讨分布式锁的原理:
在这里插入图片描述

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。

Watchdog 定期续期锁:
当客户端成功获取锁后,Redisson 启动一个 Watchdog 线程,该线程会定期(通常是锁过期时间的一半)检查锁是否过期,并在过期前对锁进行续期。
Watchdog 使用 Lua 脚本确保原子性:
为了确保 Watchdog 操作的原子性,Redisson 使用 Lua 脚本执行 Watchdog 操作。这样在 Watchdog 检查和续期锁的过程中,可以保证整个操作是原子的,防止出现竞争条件。
Watchdog 续期锁的过期时间:
Watchdog 线程会通过使用 PEXPIRE 或者 EXPIRE 命令来续期锁的过期时间。这样在业务未完成时,锁的过期时间会不断延长,直到业务完成释放锁。

方案:多机实现的分布式锁Redlock

Redisson分布式锁会有个缺陷,就是在Redis哨兵模式下:客户端1 对某个 master节点 写入了redisson锁,此时会异步复制给对应的 slave节点。但是这个过程中一旦发生master节点宕机,主备切换,slave节点从变为了 master节点。
这时 客户端2 来尝试加锁的时候,在新的master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题, 导致各种脏数据的产生 。
因此有了Redlock
Redlock:全名叫做 Redis Distributed Lock;即使用redis实现的分布式锁
使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击);
基本原理:
按顺序向5个master节点请求加锁(注意这五个节点不是哨兵和master-slaver)
根据设置的超时时间来判断,是不是要跳过该master节点。
如果大于等于三个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
如果获取锁失败,解锁!
1、时钟漂移问题:
问题描述: 不同的服务器上的系统时钟可能存在一定的漂移,导致在不同节点上计算锁的过期时间不一致。
解决方案: 使用 NTP 等工具同步服务器时钟,确保各个节点时钟同步。此外,可以选择使用更精确的锁实现,如 Redisson 的 Redlock 的改进版本,可以在获取锁时计算一个时钟偏移量,使得各个节点的时钟更一致。
2. 网络分区和节点故障:
问题描述: 网络分区或节点故障可能导致锁的不一致状态。
解决方案: 针对网络分区,可以采用心跳机制定期检测节点的健康状态。对于节点故障,可以使用更多的节点(超过一半)以确保容错性。另外,可以考虑使用 Redis Sentinel 或 Redis Cluster 等 Redis 提供的高可用性方案,以降低节点故障的影响。
3. 性能代价问题:
问题描述: Redlock 的性能代价相对较高,因为需要在多个节点上执行锁的获取和释放操作。
解决方案: 对于一些对性能要求较高的场景,可以考虑使用更轻量级的锁算法,例如基于单一节点的分布式锁实现。在一些场景下,性能可能更为重要。
4. 容错性和安全性:
问题描述: 由于 Redis 是基于内存的数据库,节点故障可能导致数据丢失。另外,需要确保所有的 Redis 节点都是可信任的。
解决方案: 在容错性方面,可以通过配置 Redis Sentinel 或 Redis Cluster 来提高 Redis 的高可用性。在安全性方面,需要确保 Redis 部署在受信任的网络中,并采取相应的网络安全措施,例如使用密码保护 Redis。

因此可以看出,实际上这种方案也有很大的问题,需要谨慎的去使用,总之系统服务不能假定所有的客户端都表现的符合预期。从安全角度讲,服务端必须防范这种来自客户端的滥用。

总结

在单体redis中通过SETNX + EXPIRE方式可以为多个JVM加一个分布式锁,但是由于操作的非原子性会导致并发问题,因此出现了几种原子性解决方法,包括SETNX+时间value、lua脚本和SET扩展命令的方式解决,但是,依然会出现事务还没完成时间就失效,产生了新一轮并发,因此,通过添加一个看门狗线程定期检查能够解决这个问题,对于一个Java开发来说有一个Redisson框架实际上封装了lua脚本来实现。
哨兵和主从模式下的分布式redis,如果一个主机更新了锁,但是恰好此时master发生了意外,还没有同步到slaver,此时新出现的master没有加锁,依然会产生问题。由此出现了Redlock+Rerdisson的解决方案,但是多个redis的集群会带来一些性能的损耗,而且由于时钟不同步,意外情况的发生也不能保证这种方案是一定的。

参考链接:

https://mp.weixin.qq.com/s/8fdBKAyHZrfHmSajXT_dnA
https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/
https://juejin.cn/post/6936956908007850014#heading-2
https://zhuanlan.zhihu.com/p/440865954
https://mp.weixin.qq.com/s?__biz=Mzg3NjU3NTkwMQ==&mid=2247505097&idx=1&sn=5c03cb769c4458350f4d4a321ad51f5a&source=41#wechat_redirect

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

低代码开发平台

低代码开发平台&#xff08;LCDP&#xff09;本身也是一种软件&#xff0c;它为开发者提供了一个创建应用软件的开发环境。看到“开发环境”几个字是不是很亲切&#xff1f;对于程序员而言&#xff0c;低代码开发平台的性质与IDEA、VS等代码IDE&#xff08;集成开发环境&#x…

蓝桥杯练习题(九)

&#x1f4d1;前言 本文主要是【算法】——蓝桥杯练习题&#xff08;九&#xff09;的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 …

Tomcat解压打包文件和并部署

一、文件压缩和上传解压 1.本地打包好dist.tar.gz文件 2.通过xftp拖拽上传到知道文件夹下,或者通过命令: cp dist.tar.gz /path/to/destination/folder注:将dist.tar.gz复制到 /path/to/destination/folder文件夹下,该文件夹只是举个例子怎么复制和解压! 3.进入/path/…

51-13 多模态论文串讲—BEiT v3 论文精读

BEiT-3的核心思想是将图像建模为一种语言&#xff0c;这样我们就可以对图像、文本以及图像-文本对进行统一的mask modeling。Multi-way transformer模型可以有效地完成不同的视觉和视觉语言任务&#xff0c;使其成为通用建模的一个有效选择。 同时&#xff0c;本文也对多模态大…

K8s-Pod资源(一)Pod介绍、创建Pod、Pod简单资源配额

Pod概述 Kubernetes Pod | Kubernetes Pod是Kubernetes中的最小调度单元&#xff0c;k8s都是以pod的方式运行服务的 一个pod可以指定镜像&#xff0c;封装一个或多个容器 pod需要调度到工作节点运行&#xff0c;节点的选择由scheduler调度器实现 pod定义时&#xff0c;会…

Android Studio 如何设置中文

Android Studio 是一个为 Adndroid 平台开发程序的集成开发环境&#xff08;IDE&#xff09;。 如何安装中文插件 在 Jetbrains 家族的插件市场上&#xff0c;是能够搜到语言包插件的&#xff0c;正常情况下安装之后只需要重启即可享受中文界面&#xff0c;可AndroidStudio 中…

视频剪辑软件Camtasia2024最新版本快捷键大全

Camtasia Studio是一款专门录制屏幕动作的工具&#xff0c;它能在任何颜色模式下轻松地记录 屏幕动作&#xff0c;包括影像、音效、鼠标移动轨迹、解说声音等等。 今天来给大家介绍一下Camtasia快捷键的相关内容&#xff0c;Camtasia也是一个十分好用的电脑屏幕录制与视频剪辑…

Linux系统——远程访问及控制

目录 一、OpenSSH服务器 1.SSH&#xff08;Secure Shell&#xff09;协议 2.OpenSSH 2.SSH原理 2.1公钥传输原理 2.2加密原理 &#xff08;1&#xff09;对称加密 &#xff08;2&#xff09;非对称加密 2.3远程登录 2.3.1延伸 2.3.2登录用户 3.SSH格式及选项 3.1延…

Pod调度

在默认情况下&#xff0c;一个Pod在哪个Node节点上运行&#xff0c;是由Scheduler组件采用相应的算法计算出来的&#xff0c;这个过程是不受人工控制的。但是在实际使用中&#xff0c;这并不满足的需求&#xff0c;因为很多情况下&#xff0c;我们想控制某些Pod到达某些节点上&…

高斯Hack算法

背景 刷leetcode时&#xff0c;碰到一题需要求解n个bit中选择m个bit的所有组合集&#xff0c;我只想到了递归求解&#xff0c;没啥问题&#xff0c;但是在官方题解中看到了牛逼的方法(Gospers Hack)&#xff0c;故记录一下。 4bit中2个1的情况 0011b0101b0110b1001b1010b1100b…

基于协同过滤的零食推荐系统

介绍 协同过滤算法&#xff1a;协同过滤是推荐系统中比较常用的算法之一&#xff0c;其核心思想是寻找用户之间的相似性&#xff0c;通过发现用户之间的共性&#xff0c;向用户推荐可能喜欢的商品或服务。Python Django Vue&#xff1a;Django 是一个基于 Python 的 Web 开发框…

模型的权值平均的原理和Pytorch的实现

一、前言 模型权值平均是一种用于改善深度神经网络泛化性能的技术。通过对训练过程中不同时间步的模型权值进行平均&#xff0c;可以得到更宽的极值点&#xff08;optima&#xff09;并提高模型的泛化能力。 在PyTorch中&#xff0c;官方提供了实现模型权值平均的方法。 这里…

PYTHON通过跳板机巡检CENTOS的简单实现

实现的细节和引用的文件和以前博客记录的基本一致 https://shaka.blog.csdn.net/article/details/106927633 差别在于,这次是通过跳板机登陆获取的主机信息,只记录差异的部份 1.需要在跳板机相应的路径放置PYTHON的脚本resc.py resc.py这个脚本中有引用的文件(pm.sh,diskpn…

查询速度提升15倍!银联商务基于 Apache Doris 的数据平台升级实践

本文导读&#xff1a; 在长期服务广大规模商户的过程中&#xff0c;银联商务已沉淀了庞大、真实、优质的数据资产数据&#xff0c;这些数据不仅是银联商务开启新增长曲线的基础&#xff0c;更是进一步服务好商户的关键支撑。为更好提供数据服务&#xff0c;银联商务实现了从 H…

EI级 | Matlab实现VMD-TCN-LSTM变分模态分解结合时间卷积长短期记忆神经网络多变量光伏功率时间序列预测

EI级 | Matlab实现VMD-TCN-LSTM变分模态分解结合时间卷积长短期记忆神经网络多变量光伏功率时间序列预测 目录 EI级 | Matlab实现VMD-TCN-LSTM变分模态分解结合时间卷积长短期记忆神经网络多变量光伏功率时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.【E…

Springboot3新特性:开发第一个 GraalVM 本机应用程序(完整教程)

在讲述之前&#xff0c;各位先自行在网上下载并安装Visual Studio 2022&#xff0c;安装的时候别忘了勾选msvc 概述&#xff1a;GraalVM 本机应用程序&#xff08;Native Image&#xff09;是使用 GraalVM 的一个特性&#xff0c;允许将 Java 应用程序编译成本机二进制文件&am…

AI-数学-高中-5.求函数解析式(4种方法)

原作者视频&#xff1a;函数】3函数解析式求法&#xff08;易&#xff09;_哔哩哔哩_bilibili 1.已知函数类型-待定系数法&#xff1a;先用待定系数法把一次或二次函数一般表达式写出来&#xff1b;再用“要变一起变”左右两边同时替换&#xff0c;计算出一般表达式的常数&…

SpringIOC之support模块GenericGroovyApplicationContext

博主介绍&#xff1a;✌全网粉丝5W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面有丰富的经验…

【c++】利用嵌套map创建多层树结构

通常树的深度都大于1&#xff0c;即树有多层&#xff0c;而树结构又可以用c的map容器来实现&#xff0c;所以&#xff0c;本文给出了一种多层树结构的实现思路&#xff0c;同时也给出了相应的c代码。 整体思路概述 首先定义一个节点类Node类&#xff0c;要包括children&#x…

FineBI实战项目一(17):热门商品Top10分析开发

点击新建组件&#xff0c;创建热门商品Top10组件。 选择柱状图&#xff0c;拖拽cnt&#xff08;总数&#xff09;到横轴&#xff0c;拖拽goodName到纵轴。 选择排序规则。 修改横轴和纵轴的标签名称 切换到仪表板&#xff0c;拖拽组件到仪表板 效果如下&#xff1a;