Redis分布式锁--java实现

news/2024/2/24 7:34:21/文章来源:https://blog.csdn.net/Artisan_w/article/details/135571700

文章目录

  • 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.luyixian.cn/news_show_926111.aspx

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

相关文章

springcloud Alibaba中gateway和sentinel联合使用

看到这个文章相信你有一定的sentinel和gateway基础了吧。 官网的gateway和sentinel联合使用有些过时了&#xff0c;于是有了这个哈哈&#xff0c;给你看看官网的&#xff1a; 才sentinel1.6&#xff0c;现在都几了啊&#xff0c;所以有些过时。 下面开始讲解&#xff1a; 首先…

【Linux】自定义shell

👑作者主页:@安 度 因 🏠学习社区:安度因 📖专栏链接:Linux 文章目录 获取命令行前置字段命令行输入解析命令行普通指令的执行子进程执行命令指令类型判断 && 内建命令总结 &&a

【Maven】007-Maven 工程的继承和聚合关系

【Maven】007-Maven 工程的继承和聚合关系 文章目录 【Maven】007-Maven 工程的继承和聚合关系一、Maven 工程的继承关系1、继承的概念2、继承的作用3、继承的语法4、父工程统一管理依赖版本父工程声明依赖版本子工程继承以来版本 二、Maven 工程的聚合关系1、聚合的概念2、聚合…

VitePress-01-从零开始的项目创建(npm版)

说明 本文介绍一下 VitePress的项目创建的步骤。 主要用到的命令工具是 npm。 本文的操作步骤是从无到有的创建一个完整的基本的【VitePress】项目。 环境准备 根据官方文档的介绍&#xff0c;截止本文发稿时&#xff0c;需要使用node.js 18 的版本。 可以使用node -v 的命令查…

【MySQL】MySQL表的约束-空属性/默认值/列属性/zerofill/主键/自增长/唯一键/外键

文章目录 表的约束1.空属性 --null && not null2.默认值 -- default3.列描述4.zerofill5.主键6.自增长7.唯一键8.外键 表的约束 表的约束&#xff1a;表中一定要有各种约束&#xff0c;通过约束&#xff0c;让我们未来插入数据库表中的数据是符合预期的。约束的本质是…

【GCC】6 接收端实现:周期构造RTCP反馈包

基于m98代码。GCC涉及的代码,可能位于:webrtc/modules/remote_bitrate_estimator webrtc/modules/congestion_controller webrtc/modules/rtp_rtcp/source/rtcp_packet/transport_feedback.cc webrtc 之 RemoteEstimatorProxy 对 remote_bitrate_estimator 的 RemoteEstimato…

Spark与HBase的集成与数据访问

Apache Spark和Apache HBase分别是大数据处理和分布式NoSQL数据库领域的两个重要工具。在本文中&#xff0c;将深入探讨如何在Spark中集成HBase&#xff0c;并演示如何通过Spark访问和操作HBase中的数据。将提供丰富的示例代码&#xff0c;以便更好地理解这一集成过程。 Spark…

【EI会议征稿通知】第四届图像处理与智能控制国际学术会议(IPIC 2024)

第四届图像处理与智能控制国际学术会议&#xff08;IPIC 2024&#xff09; 2024 4th International Conference on Image Processing and Intelligent Control 2024年第四届图像处理与智能控制国际学术会议&#xff08;IPIC 2024&#xff09;将于2024年5月3日-5日在吉隆坡举…

【Jmeter之get请求传递的值为JSON体实践】

Jmeter之get请求传递的值为JSON体实践 get请求的常见传参方式 1、在URL地址后面拼接&#xff0c;有多个key和value时&#xff0c;用&链接 2、在Parameters里面加上key和value 第一次遇到value的值不是字符串也不是整型&#xff0c;我尝试把json放到value里面&#xff0…

C练习——杨辉三角

题目&#xff1a; 打印近似杨辉三角&#xff0c;行数n自选 百度找的杨辉三角&#xff0c;参考一下&#xff1a; 解析&#xff1a; 把它的全部元素左对齐&#xff0c;就可以看成近似杨辉三角的样子 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …… 每个数等于它上方两数…

OpenCV C++ 环境搭建和简单示例

OpenCV介绍 OpenCV&#xff1a;开源发行的跨平台计算机视觉和机器学习软件库&#xff0c;用C语言编写&#xff0c;提供了C &#xff0c;Python&#xff0c;Java和MATLAB接口&#xff0c;并支持Windows&#xff0c;Linux&#xff0c;Android和Mac OS。 OpenCV下载 去官网http…

常见面试题之CSS

CSS3的新特性 新增选择器&#xff1a;:nth-child()、:first-of-type、:last-of-type等 弹性盒子&#xff1a;display: flex 媒体查询&#xff1a;media根据设备的特性和屏幕大小应用不同的样式规则 多列布局&#xff1a;column-count和column-with等属性可以实现将内容分为多…

蓝桥杯每日一题----货物摆放

题目 分析 上来一看&#xff0c;三个for循环&#xff0c;从1到n&#xff0c;寻找满足lwhn的个数&#xff0c;但是这样根本跑不出来答案&#xff0c;n太大了&#xff0c;1e15的级别&#xff0c;O&#xff08;n&#xff09;的时间复杂度都不行&#xff0c;更何况是O&#xff08;…

Webpack 怎么实现按需异步加载模块

要弄懂这个问题&#xff0c;需要先来看关于webpack打包的3个问题。 三个问题 第一个问题 项目中的json文件&#xff0c;如何使用webpack进行处理&#xff1f; 如果我们希望把json文件当做静态配置&#xff0c;例如有如下json文件 {"version": "1.0.0"…

高级 Python 面试问题与解答

文章目录 专栏导读1.什么是PIP&#xff1f;2.什么是 zip 函数&#xff1f;3.Python 中的 __init __ () 是什么&#xff1f;4.Python 中的访问说明符是什么&#xff1f;5.Python 中的单元测试是什么&#xff1f;6.Python全局解释器锁&#xff08;GIL&#xff09;&#xff1f;7.P…

Linux Debian12使用VSCode和Python搭建flask开发环境

一、安装VSCode 在Linux Debian12系统上安装VSCode教程可以参考网上相关教程。 二、安装Python 打开VSCode&#xff0c;安装python和python扩展包&#xff0c;如下图所示&#xff1a; 三、创建Python虚拟环境 1.新建文件夹testFlask 2.用vscode打开文件夹testFlask&#xf…

FFmpeg连载6-音频重采样

今天我们的实战内容是将音频解码成PCM&#xff0c;并将PCM重采样成特定的采样率&#xff0c;然后输出到本地文件进行播放。 什么是重采样&#xff1f; 所谓重采样&#xff0c;一句话总结就是改变音频的三元素&#xff0c;也就是通过重采样改变音频的采样率、采样格式或者声道数…

【docker-compose】【nginx】内网环境https配置

目录 1、openssl生成自签名证书和私钥2、nginx.conf配置ssl3、docker-compose挂载 1、openssl生成自签名证书和私钥 在部署服务器上&#xff0c;新建cert目录&#xff0c;执行以下指令&#xff0c;然后生成.crt和.key文件 openssl req -newkey rsa:2048 -nodes -keyout rsa_pri…

成都力寰璨泓科技有限公司:抖小店新势力

在当下这个信息爆炸的时代&#xff0c;科技的发展日新月异&#xff0c;电商行业更是风起云涌。在这场没有硝烟的战争中&#xff0c;成都力寰璨泓科技有限公司以其敏锐的商业洞察和卓越的技术实力&#xff0c;成功抢滩抖音小店市场&#xff0c;成为一股不可忽视的新生力量。 成都…

ADA-YOLO:YOLOv8+注意力+Adaptive Head,mAP提升3%

生物医学图像分析中的目标检测和定位至关重要&#xff0c;尤其是在血液学领域&#xff0c;检测和识别血细胞对于诊断和治疗决策至关重要。虽然基于注意力的方法在各个领域中目标检测方面取得了显著的进展&#xff0c;但由于医学影像数据集的独特挑战&#xff0c;其在医学目标检…