Redis分布式锁--java实现

news2024/12/29 10:45:30

文章目录

  • 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 then
   redis.call('expire',KEYS[1],ARGV[2])
else
   return 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.coloradmin.cn/o/1390203.html

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

相关文章

pod控制器的作用

pod控制器的作用 1、动态pv和pvc deployment是控制器 pod空气器:工作负载&#xff0c;workload用于管理pod的中间层&#xff0c;确保podi资源符合预期的状态 预期状态 1、副本数 2、容器重启策略 3、镜像拉取策略 pod、出现故障时重启等等 pod的控制器类型 1、replic…

Vue3组件库开发 之Button(1)

需求分析&#xff1a; Button 组件大部分关注样式&#xff0c;没有交互 根本分析可以得到具体的属性列表&#xff1a; type:不同的样式(Default,Primary,Danger,Info,Success,Warning) plain:样式的不同展现模式boolean round:圆角boolean circle:圆形按钮&#xff0c;适合图标…

【目标跟踪】跨相机如何匹配像素

文章目录 前言一、计算思路二、代码三、结果 前言 本本篇博客介绍一种非常简单粗暴的方法&#xff0c;做到跨相机像素匹配。已知各相机内外参&#xff0c;计算共视区域像素投影&#xff08;不需要计算图像特征&#xff09;。废话不多说&#xff0c;直接来&#xff0c;见下图。…

前端安全专题

xss (Cross Site Scripting) 跨站脚本攻击 原理 通常指黑客通过"HTML注入"篡改了网页&#xff0c;插入了恶意的脚本&#xff0c;从而在用户浏览网页时&#xff0c;控制用户浏览器的一种攻击。 常见攻击类型 存储型XSS 攻击者将恶意的 JavaScript 脚本存储在网站…

【网络工程师】NAT与动态路由

一、NAT网络地址转换 1、NAT&#xff1a;Network Address Translations 网络地址转换 2、ip地址问题&#xff1a;ipv4地址严重不够用了&#xff08;A、B、C类可以使用 D组播 E科研&#xff09; 3、解决&#xff1a;把IP地址分为了公网IP和私网IP 公网IP只能在公网上使用 私网…

手把手教你SWOT分析!建议收藏

最近&#xff0c;我一直为一件事情感到困扰。那家位于市中心的西点店生意越来越好&#xff0c;甚至已经开了两家分店&#xff0c;但是挣来的钱还不足够买房子。于是最近&#xff0c;我被这如火如荼的奶茶市场所吸引&#xff0c;想要利用已有的资源开一家奶茶店。但是我不确定这…

使用 Python 数据写入 Excel 工作表

在数据处理和报告生成等工作中&#xff0c;Excel 表格是一种常见且广泛使用的工具。然而&#xff0c;手动将大量数据输入到 Excel 表格中既费时又容易出错。为了提高效率并减少错误&#xff0c;使用 Python 编程语言来自动化数据写入 Excel 表格是一个明智的选择。Python 作为一…

linux GDB and GDB Sever

概念&#xff1a; GDB&#xff08;GNU Debugger&#xff09;是一个用于调试程序的强大工具。它是GNU项目的一部分&#xff0c;支持多种编程语言&#xff0c;包括C、C等。GDB 提供了一组命令和功能&#xff0c;允许跟踪检查程序的内部状态&#xff0c;跟踪代码的执行过程&#…

7.5 MySQL对数据的基本操作(❤❤❤)

7.5 MySQL对数据的基本操作 1. 提要2. 数据添加2.1 insert语法2.2 insert 子查询2.3 ignore关键字 3. 数据修改3.1 update语句3.2 update表连接 4. 数据删除4.1 delete语句4.2 delete表连接4.3 快速删除数据表全部数据 1. 提要 2. 数据添加 2.1 insert语法 2.2 insert 子查询 …

vivado 添加现有IP文件、生成IP

添加现有IP文件 作为从AMD IP目录添加和自定义IP的替代方案&#xff0c;您可以直接添加XCI或XCIX文件。此过程不同于从按以下方式编目&#xff1a; •XCI或XCIX文件可能是早期版本&#xff0c;也可能是相同或完全自定义的版本AMD IP目录中发现的类似IP。 •XCI或XCIX文件可能…

【linux】systemctl控制系统服务启动与关闭

systemctl简介 systemctl是一个在linux系统中&#xff0c;管理系统服务的命令。它通常是启动与关闭像防火墙&#xff0c;网络&#xff0c;数据库这类不是直接的软件&#xff0c;而是给软件提供支持的服务。 但是值得注意的是&#xff0c;并不是所有服务都可以用它管理&#x…

Vue学习笔记3--全局事件总线

Vue学习笔记3—全局事件总线 1.全局事件总线可以实现任意组件间通信 X需具备的条件&#xff1a; 所有的组件都要能看见X可以调用$on $off $emitVue.prototype.x {a:1, b:2} 可以被所有组件看见VueComponent.protoype.proto Vue.prototype组件实例对象(vc)可以访问到Vue原型上…

【.NET Core】C#预处理器指令

【.NET Core】C#预处理器指令 文章目录 【.NET Core】C#预处理器指令一、概述二、可为空上下文&#xff08;#nullable&#xff09;三、条件编译2.1 定义DEBUG是编译代码2.2 未定义MYTEST时&#xff0c;将编译以下代码 四、定义符号五、定义区域六、错误和警告信息 一、概述 预…

关于 ant-design-vue resetFields 失效

关于 ant-design-vue resetFields 失效 背景&#xff1a; 遇到这样的问题使用ant-design-vue useForm来制作表单的时候&#xff0c;resetFields()失效 场景&#xff1a; 编辑 -赋值 新增-初始值&#xff08;问题点&#xff1a;新增的时候他就不初始化&#xff09; 方案&…

【STM32】STM32学习笔记-USART串口收发HEX和文本数据包(29)

00. 目录 文章目录 00. 目录01. 串口简介02. 串口收发HEX数据包接线图03. 串口收发HEX数据包示例104. 串口收发HEX数据包示例205. 串口收发文本数据包接线图06. 串口收发文本数据包示例07. 程序示例下载08. 附录 01. 串口简介 串口通讯(Serial Communication)是一种设备间非常…

智能搬运机器人作为一种新型的物流技术

随着物流行业的快速发展&#xff0c;货物转运的效率和准确性成为了企业竞争的关键因素之一。智能搬运机器人作为一种新型的物流技术&#xff0c;已经在许多企业中得到了广泛应用。本文将介绍富唯智能智能搬运机器人在物流行业的应用和优势。 在实际应用中&#xff0c;智能搬运机…

在visual studio中调试时无法查看std::wstring

1.问题 在调试的时候发现std::wstring类型的变量查看不了&#xff0c;会显示(error)|0&#xff0c;百思不得其解。 2.解决方法 参考的&#xff1a;vs2015调试时无法显示QString变量的值&#xff0c;只显示地址_vs调试qstring的时候如何查看字符串-CSDN博客 在工具/选项/调试…

transfomer中Multi-Head Attention的源码实现

简介 Multi-Head Attention是一种注意力机制,是transfomer的核心机制,就是图中黄色框内的部分. Multi-Head Attention的原理是通过将模型分为多个头&#xff0c;形成多个子空间&#xff0c;让模型关注不同方面的信息。每个头独立进行注意力运算&#xff0c;得到一个注意力权…

弟12章 网络编程

文章目录 网络协议概述 p164TCP协议与UDP协议的区别 p165TCP服务器端代码的编写 p166TCP服务器端流程 TCP客户端代码的编写 p167TCP客户端流程主机和客户端的通信流程 tcp多次通信服务器端代码 p168TCP多次通信客户端代码 p169UDP的一次双向通信 p170udp通信模型udp接收方代码u…

6 - 常用工具类

目录 1. Scanner 扫描控制台输入 1.1 扫描控制台输入 1&#xff09;nextLine 2&#xff09;nextInt 3&#xff09;其他方法 1.2 扫描文件 1.3 查找匹配项 2. Arrays 数组工具 2.1 创建数组 1&#xff09;copyOf 2&#xff09;copyOfRange 3&#xff09;fill 2.2 比…