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

Redis实现分布式锁

什么是分布式锁?

分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。如下图所示:

如何实现分布式锁?

基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。

  • 互斥性:加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;
  • 设定过期时间:锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
  • 保证同一把锁:锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端;

满足这三个条件的分布式命令如下:

SET lock_key unique_value NX PX 10000

  • lock_key 就是 key 键;
  • unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
  • NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
  • PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。

而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。

可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。

// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

基于原生redis实现分布式锁的缺点?

1、超时时间不好设置。如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。比如在有些场景中,一个线程 A 获取到了锁之后,由于业务代码执行时间可能比较长,导致超过了锁的超时时间,自动失效,注意 A 线程没执行完,后续线程 B 又意外的持有了锁,意味着可以操作共享资源,那么两个线程之间的共享资源就没办法进行保护了。

  • 那么如何合理设置超时时间呢? 我们可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可,不过这种方式实现起来相对复杂。

2、Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

Redis 如何解决集群情况下分布式锁的可靠性?

为了保证集群环境下分布式锁的可靠性,Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)。

        它是基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署 5 个 Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。

        Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败

        这样一来,即使有某个 Redis 节点发生故障,因为锁的数据在其他节点上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。

Redlock 算法加锁三个过程:

  • 第一步是,客户端获取当前时间(t1)。
  • 第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作:
  1. 加锁操作使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。
  2. 如果某个 Redis 节点发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,我们需要给「加锁操作」设置一个超时时间(不是对「锁」设置超时时间,而是对「加锁操作」设置超时时间),加锁操作的超时时间需要远远地小于锁的过期时间,一般也就是设置为几十毫秒。
  • 第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。

可以看到,加锁成功要同时满足两个条件(简述:如果有超过半数的 Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功):

  • 条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁;
  • 条件二:客户端从大多数节点获取锁的总耗时(t2-t1)小于锁设置的过期时间。

加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁最初设置的过期时间」减去「客户端从大多数节点获取锁的总耗时(t2-t1)」。如果计算的结果已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。

加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。

Redission实现分布式锁

       Redission 是 【 原生redis set nx 】的一个进阶版。它具有支持自动续期分布式锁的超时时间的功能,避免锁因超时而自动释放;并且还实现了可重入锁、公平锁、读写锁等,以及监控、异步执行等功能。

主要特性和功能:

  1. 分布式对象: Redisson支持分布式的Java对象,例如List、Set、Map、Queue等,这些对象可以在多个JVM之间共享和操作,而无需开发人员手动处理分布式环境下的一致性和同步问题。
  2. 分布式锁: Redisson提供了基于Redis的分布式锁实现,支持不同的锁类型,如可重入锁、公平锁、红锁等,帮助开发人员在分布式环境下实现线程安全的并发控制。
  3. 分布式集合: Redisson支持各种分布式集合,如分布式Set、分布式List、分布式Map等,使得开发人员可以方便地在分布式环境中使用这些数据结构。
  4. 分布式服务: Redisson还提供了分布式对象、锁、消息队列等服务,方便开发人员构建分布式应用程序,处理分布式应用中的各种场景和问题。
  5. Reactive编程支持: Redisson还支持在Reactive编程模型下操作数据,使得在异步和非阻塞场景下更容易地与Redis进行交互。

参考案例:

编写一个简单的Java类来演示使用Redisson的分布式锁功能:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;public class RedissonLockDemo {public static void main(String[] args) {// 创建Redisson配置Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");// 创建Redisson客户端RedissonClient redisson = Redisson.create(config);// 获取分布式锁String lockKey = "myLock";redisson.getLock(lockKey).lock();try {System.out.println("获取分布式锁成功,执行业务逻辑...");// 模拟业务逻辑Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放分布式锁redisson.getLock(lockKey).unlock();System.out.println("释放分布式锁");}// 关闭Redisson客户端redisson.shutdown();}
}

在上面的示例中,我们创建了一个Redisson客户端,获取了一个名为"myLock"的分布式锁,然后在锁定的情况下执行业务逻辑(此处使用了一个简单的休眠来模拟业务逻辑),最后释放了分布式锁。

ps:以下是我整理的java面试资料,密码是obht,感兴趣的可以看看。最后,创作不易,觉得写得不错的可以点点关注!

链接:https://www.yuque.com/u39298356/uu4hxh?# 《Java面试宝典》 

相关文章:

  • Git 基于ED25519、RSA算法生成 SSH 密钥
  • linux 搭建web网站
  • unity学习(45)——选择角色菜单——客户端处理服务器的数据
  • 2.2 mul、div、and、or乘除指令及所有寄存器英文名
  • Flutter 设置每帧绘制结束调用的回调函数
  • RK3568 android11 调试陀螺仪模块 MPU6500
  • flink 设置空闲等待推进水位线,避免子任务上游最小的水位线迟迟未达到触发时间
  • python的正则表达式
  • 《高性能MYSQL》-架构,锁,事务
  • H264的打包,nal,es,pes,pts,dts,ps,ts
  • 第十四届蓝桥杯大赛B组 JAVA 蜗牛 (递归剪枝)
  • 模版进阶C++
  • AI写的wordpress网站首页模板 你觉得怎么样?
  • [GXYCTF2019]BabyUpload1 -- 题目分析与详解
  • 探讨苹果 Vision Pro 的 AI 数字人形象问题
  • 【Redis学习笔记】2018-06-28 redis命令源码学习1
  • 【翻译】babel对TC39装饰器草案的实现
  • 2017-09-12 前端日报
  • CentOS 7 修改主机名
  • ES6之路之模块详解
  • Fabric架构演变之路
  • Java的Interrupt与线程中断
  • Java小白进阶笔记(3)-初级面向对象
  • Spring Cloud Feign的两种使用姿势
  • SpringBoot几种定时任务的实现方式
  • Spring技术内幕笔记(2):Spring MVC 与 Web
  • Vue--数据传输
  • 产品三维模型在线预览
  • 大快搜索数据爬虫技术实例安装教学篇
  • 设计模式走一遍---观察者模式
  • 手写双向链表LinkedList的几个常用功能
  • 跳前端坑前,先看看这个!!
  • 微信小程序上拉加载:onReachBottom详解+设置触发距离
  • 看到一个关于网页设计的文章分享过来!大家看看!
  • ​插件化DPI在商用WIFI中的价值
  • #pragma pack(1)
  • %@ page import=%的用法
  • (4)(4.6) Triducer
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (笔试题)分解质因式
  • (机器学习的矩阵)(向量、矩阵与多元线性回归)
  • (转载)微软数据挖掘算法:Microsoft 时序算法(5)
  • .locked1、locked勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .NET MVC、 WebAPI、 WebService【ws】、NVVM、WCF、Remoting
  • .NET/ASP.NETMVC 深入剖析 Model元数据、HtmlHelper、自定义模板、模板的装饰者模式(二)...
  • .NET牛人应该知道些什么(2):中级.NET开发人员
  • .net网站发布-允许更新此预编译站点
  • .net专家(张羿专栏)
  • .sys文件乱码_python vscode输出乱码
  • @KafkaListener注解详解(一)| 常用参数详解
  • @RequestMapping用法详解
  • @SpringBootApplication 包含的三个注解及其含义
  • [ element-ui:table ] 设置table中某些行数据禁止被选中,通过selectable 定义方法解决
  • [2016.7.test1] T2 偷天换日 [codevs 1163 访问艺术馆(类似)]
  • [BT]BUUCTF刷题第9天(3.27)