Redis第15讲——RedLock、Zookeeper及数据库实现分布式锁

由于篇幅原因,在上篇文章我们只介绍了redis实现分布式锁的两种方式——setnx和Redission,并对Reidssion加锁和看门狗机制的源码进行了分析,但这两种方案在极端情况下都会出现或多或少的问题。那么针对上述问题,比较主流的解决方案有两种:RedLock和Zookeeper实现的分布式锁。

ps:我们上篇文章提到过主流的分布式锁实现方案有zookeeper、redis和数据库,所以本节也会顺带介绍一下数据库实现分布式锁。

一、数据库

ps:这里就简单介绍一下。

最简单的方式可能就是直接创建一张锁表,然后通过该表的数据来实现。

表的字段可以是:id、method_name(唯一约束)、update_time等

  • 加锁:当我们想要锁住某个方法时,可以在表中insert一条数据,因为我们对method_name做了唯一性约束,所以这时如果有多个请求的话,数据库可以保证只有一个操作可以成功。
  • 解锁:当方法执行完毕可以用删除语句来解锁。

存在的问题:

  • 锁没有失效时间,一旦解锁失败,其它线程就无法获得锁。

    • 解决:弄个定时任务定时清理一遍。

  • 非阻塞锁,一旦插入失败就会直接报错,没有获得锁的线程并不会排队等待。

    • 解决:可以while循环把它变成阻塞的。

  • 非重入,同一个线程在释放锁之前无法再次获得该锁。

    • 解决:可以在表中加个记录获得锁的主机信息和线程信息,下次再获取的时候先查一把数据库。

虽然有对应的解决方案,但是这些解决方案的背后又都是问题,比如定时任务这个,假如任务还没执行完,定时任务就把锁给清理了;而且数据库也需要一定的开销,所以不推荐使用数据库实现分布式锁。

二、RedLock

进入正题。

简单来说RedLock是Redis的作者Antirez在单Redis节点基础上引入的高可用模式。

2.1 为什么要使用RedLock

在上篇文章也提到过,不管是用原生的setnx命令还是用Reidssion实现的分布式锁,它们都无法解决Reids在主从复制、哨兵集群下的多节点问题:

  • 线程A在Matser上获取锁。
  • 在Key的写入被传输到Slave之前,Matser崩溃。
  • Slave被提升为主节点。
  • 线程B 获取了与线程A已经持有锁相同的锁。

这个场景同一把锁被两个线程同时持有,这是不被允许的,所以Redis的作者提出RedLock算法来应对这种问题。

2.2 RedLock加锁原理

RedLock通过使用多个Redis节点,来提供一个更加健壮的分布式锁解决方案,能够在某些Redis节点故障的情况下,仍然能够保证分布式锁的可用性。

假设我们有N个Redis主节点,例如N=5,这些节点是完全独立的,我们不适用赋值或任何其它隐式协调系统,为了取到锁,客户端应该执行以下操作(来自Redis官方文档

  • 获取当前时间(毫秒)。
  • 依次从5个节点,使用相同的key和随机值(例如UUID)获取锁。
  • 当向Redis请求获取锁时,客户端应该设置一个超时时间,这个时间要远小于锁失效的时(例如,如果自动释放时间为 10 秒,超时时间可能在 5-50 毫秒范围内)这可以防止客户端在尝试与宕机的 Redis 节点通信时被长时间阻塞:如果一个实例不可用,客户端应该尽快尝试与下一个实例通信。
  • 客户端计算获取锁所用的时间减去步骤1的时间,就获得了获取锁消耗的时间。当前仅当大多数(N/2+1,这里是3个节点)的Redis节点都获取到锁,并且获取锁使用的时间小于锁失效的时间,锁才算获取成功。
  • 成功获取锁后,key的真正有效时间=TTL-锁的获取时间-时钟漂移(2.5小节会提)
  • 如果客户端由于某种原因未能获取锁(无法锁定 N/2+1 个实例或有效时间为负),它将尝试解锁所有实例(甚至是它认为自己无法锁定的实例)

ps:加锁失败的实例也要执行解锁操作的原因是:可能会出现服务端响应消息丢失但实际上成功了的情况。

2.3 存在的问题

上述看着似乎挺完美的,但在实际工作中,用的并不多,主要有两个原因:该方案的使用成本较高、且并不能完全解决分布式锁的问题:

  • 假设现在还是5个Redis节点,线程A此时已经在三个节点上获取到了锁,表示已经加锁成功了,那么极端场景下就会出现问题:

    • 严重依赖系统时间:如果获取到锁的三个节点中的某个节点时间稍微快一点,则它持有的锁就会被提前释放,当他释放后,就又有3个实例空闲了,这时线程2也可以获取到锁,就又出现了两个线程同时持有了锁。

    • 假设redis没有配置持久化:3个节点中的某一个节点重启了,此时又有3个节点空闲了,然后另一个线程又可以加锁成功了。

  • 在脑裂(网络分区)的情况下,RedLock也可能会产生两个线程同时持有锁的情况。

还有一个性能问题:setnx和Redission实现的分布式锁只需要在一个节点写成功就行了,而RedLock需要写多个节点才算加锁成功。

2.4 如何使用

在Redission客户端中也实现了基于Redis的RedLock加锁算法:

Config config1=new Config();
config1.useSingleServer().setAddress("redis://127.0.1.1:6379");
Config config2=new Config();
config1.useSingleServer().setAddress("redis://127.0.1.1:6378");
Config config3=new Config();
config1.useSingleServer().setAddress("redis://127.0.1.1:6377");
RedissonClient redissonClient1 = Redisson.create(config1);
RedissonClient redissonClient2 = Redisson.create(config2);
RedissonClient redissonClient3 = Redisson.create(config3);
RLock lock1 = redissonClient1.getLock("myLock");
RLock lock2 = redissonClient2.getLock("myLock");
RLock lock3 = redissonClient3.getLock("myLock");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
boolean b = lock.tryLock();
if (b){try {//业务逻辑} finally {lock.unlock();}
}else {//获取锁失败的逻辑
}

除了Redission,还有一些其它工具也可以实现RedLock,比如Java的RedLock-java库、Go的Redsync库等,这些工具的使用方法都类似,都是创建多个Redis实例,然后使用RedLock算法来获取分布式锁。

2.5 两位大佬的激烈讨论

关于RedLock,两位大佬展开过激烈的讨论,感兴趣的可以了解一下。

Martin——著名的分布式系统专家,在他博客上发表过一篇文章:How to do distributed locking — Martin Kleppmann’s blog。

在这篇文章中他认为RedLock实现分布式锁有问题:

  • 网络分区:在网络分区的情况下,不同的节点可能会获取相同的锁,这会导致分布式系统的不一致问题。
  • 时间漂移:由于不同的机器之间的时间可能存在微小的漂移,这会导致锁的失效时间不一致,也会导致不一致问题。
  • Redis的主从复制:在Redis主从复制的情况下,如果Redis的主节点出现故障,需要选举新的主节点,这个过程可能会导致锁丢失,同样存在一致性问题。

Antirez——Redis的作者,他在自己博客上也发表了一篇文章:Is Redlock safe? - <antirez>。

这篇文章对Matin指出的问题给予了回复:

  • 网络分区:在网络分区的情况下,RedLock仍然可以提供足够的可靠性。虽然会存在节点获取到相同锁的情况,但这种情况只会在网络分区时发生,且只会发生在一小部分节点上。而在网络恢复后,RedLock会自动解锁。
  • 时间漂移:RedLock可以使用NTP等工具来同步不同机器的时间,从而必变时间漂移的问题。
  • Redis的主从复制:虽然Redis的主从复制可能导致锁的丢失,但这种情况非常罕见,并且可以通过多种方式来避免,例如使用Redis Cluster。

三、Zookeeper实现

3.1 实现方案

基于Zookeeper临时有序节点也可以实现分布式锁,方案如下:

  • 创建一个锁目录/locks,该节点为持久节点。
  • 想要获取锁的线程都在锁目录下创建一个临时顺序节点。
  • 获取锁目录下所有子节点,对子节点按自增序号从小到大排序。
  • 判断本节点是不是第一个子节点(序号最小),如果是,则获取锁成功,反之,则监听自己的上一个节点的删除事件。
  • 持有锁的线程只需要删除当前节点,就可释放锁。
  • 当自己监听的节点被删除时,监听事件触发,则回到第3步重新进行判断,直到获取锁。

3.2 特性

下面我们看看它是否具有分布式锁的特性:

  • 锁释放:在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁随后挂掉(Session连接断开),那么这个临时节点就会被删除掉,其它客户端就可以再次获得锁。
  • 阻塞锁:没有获取锁的客户端会监听自己上一个节点的删除事件,一旦监听到被删除,ZK就会通知客户端判断自己创建的节点是不是第一个节点(序号最小),如果是就成功获取锁,反之继续排队。
  • 可重入:客户端在创建节点的时候,会把自己主机信息和线程信息直接写入到节点中,想要再次获取的时候就和当前第一个节点中的数据对比,如果信息一样就直接获取到锁,反之就再创建一个临时节点,参与排队。
  • 可用性:ZK是集群部署的,只要集群中有半数以上的机器存活,就可对外提供服务。

由于ZK保证了数据的强一致性,因此不会存在之前Redis方案中的问题。

3.3 存在的问题

由于ZK保证了数据的强一致性,因此不会存在之前Redis方案中的问题,没有银弹,任何方案都会有不足之处,如下:

  • 性能问题:ZK在性能方面肯定不如缓存服务那么高,因为每次创建和释放锁都要创建、销毁节点;而且创建和删除节点只能通过Leader来执行,然后将数据同步到所有的Follower上。

  • 并发问题:假如由于网络抖动,客户端和ZK集群的seesion连接断了,那么ZK以为是客户端挂了,就会删除临时节点,这时候其它客户端就可以获取到分布式锁。不过这种情况并不常见,因为ZK有重试机制,一旦ZK集群检测不到客户端的心跳,就会重试多次重试后还是不行的话才会删除临时节点。(Curator客户端支持多种重试策略)

3.4 如何使用

Curator是Netflix开源的一套Zookeeper客户端框架,可以直接使用Curator来实现Zookeeper分布式锁:

3.4.1 引入依赖

<!--引入对应的zookeeper -->
<dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.7.1</version>
</dependency>
<!--添加对应的curator框架依赖-->
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId><version>5.2.0</version>
</dependency>
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>5.2.0</version>
</dependency>
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-client</artifactId><version>5.2.0</version>
</dependency>

3.4.2 使用demo

public class ZKLock {public static void main(String[] args) {InterProcessLock lock=new InterProcessMutex(getCuratorFramework(),"/locks");try {//加锁lock.acquire();//业务逻辑//......//释放锁lock.release();} catch (Exception e) {throw new RuntimeException(e);}}
​private static CuratorFramework getCuratorFramework() {// 重试策略,这里使用的是指数补偿重试策略,重试3次,初始重试间隔1000ms,每次重试之后重试间隔递增。RetryPolicy retry = new ExponentialBackoffRetry(1000, 3);// 初始化Curator客户端:指定链接信息 及 重试策略CuratorFramework client = CuratorFrameworkFactory.newClient("ip:port", retry);// 开始链接client.start();return client;}
}

ps:如果想要重入,则需要使用同一个InterProcessMutex对象。

End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

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

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

相关文章

Linux服务器基本操作

Linux下服务器基本操作指令 Vim 文件名 进入 i编辑 esc退出编辑 &#xff1a;wq 保存退出 Cp -r文件夹 path 完整或…/ Cp 文件 path pwd 查看当前目录 rm -rf 2005 删除文件夹 Mkdir 创建文件夹 squeue查看提交队列 tail -f rsl.out.0000 在运行当前目录下查看进度 Scancel j…

用Scrapy 从数据挖掘到监控和自动化测试

Scrapy 是一个 BSD 许可的快速高级网络爬虫和网络抓取框架&#xff0c;用于抓取网站并从其页面中提取结构化数据。它可以用于广泛的用途&#xff0c;从数据挖掘到监控和自动化测试。 安装scrapy pip install scrapy 爬虫示例 示例代码写入文件 import scrapyclass QuotesSp…

Kylin Linux V10 SP1 aarch64部署k8s集群严重bug

目录 1.部署方式 2.遇到问题 3.问题解决 1.部署方式 通过sealos方式部署 2.遇到问题 适配Kylin Linux V10 SP1 aarch64部署pod 不少出现CrashLoopBackOff 通过命令: kubectl describe pod xxx -n default 查看,发现报错如下: Error response from daemon: OCI …

简约大气的全屏背景壁纸导航网源码(免费)

简约大气的全屏背景壁纸导航网模板 效果图部分代码领取源码下期更新预报 效果图 部分代码 <!DOCTYPE html> <html lang"zh-CN"> <!--版权归孤独 --> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible…

工厂模式和策略模式区别

工厂模式和策略模式都是面向对象设计模式&#xff0c;但它们的目的和应用场景有所不同。 工厂模式是一种创建型设计模式&#xff0c;旨在通过使用一个工厂类来创建对象&#xff0c;而不是直接使用new关键字来创建对象。这样做可以使系统更容易扩展和维护&#xff0c;因为新的对…

图论之最短路算法模板总结

来个大致的分类&#xff1a; 朴素的迪杰斯特拉&#xff1a; 实现&#xff1a; 我们让s表示当前已经确定的最短距离的点&#xff0c;我们找到一个不在s中的距离最近的点t&#xff0c;并用t来更新其他的点。 下面是AC代码&#xff1a; #include<bits/stdc.h> using nam…

C语言-整体内容简单的认识

目录 一、数据类型的介绍二、数据的变量和常量三、变量的作用域和生命周期四、字符串五、转义字符六、操作符六、常见的关键字6.1 关键字static 七、内存分配八、结构体九、指针 一、数据类型的介绍 sizeof是一个操作符&#xff0c;是计算机类型/变量所占内存空间的大小   sc…

中间件之异步通讯组件RabbitMQ入门

一、概述 微服务一旦拆分&#xff0c;必然涉及到服务之间的相互调用&#xff0c;目前我们服务之间调用采用的都是基于OpenFeign的调用。这种调用中&#xff0c;调用者发起请求后需要等待服务提供者执行业务返回结果后&#xff0c;才能继续执行后面的业务。也就是说调用者在调用…

Java IO流(二)

1. 缓冲流 1.1 字节缓冲流概述 当对文件或其他数据源进行频繁的读/写操作时&#xff0c;效率比较低&#xff0c;这时如果使用缓存流就能够更高效地读/写信息。 比如&#xff0c;可以使用缓冲输出流来一次性批量写出若干数据减少写出次数来提高写出效率。 如果用生活中的例子做…

使用qemu调试NVME driver

参考nvme驱动相关的博客&#xff0c;可以使用qemu buildroot进行nvme驱动的流程debug。 一、QEMU编译 首先需要编译qemu&#xff0c;可以参考QEMU编译。wget下载最新版本的QEMU&#xff0c;编译之前&#xff0c;最好检查下依赖包是否安装&#xff0c;避免安装过程出现各种错…

Qwen-Audio:推动通用音频理解的统一大规模音频-语言模型(开源)

随着人工智能技术的不断进步&#xff0c;音频语言模型&#xff08;Audio-Language Models&#xff09;在人机交互领域变得越来越重要。然而&#xff0c;由于缺乏能够处理多样化音频类型和任务的预训练模型&#xff0c;该领域的进展受到了限制。为了克服这一挑战&#xff0c;研究…

【WebGL】修改阴影体形状,实现相交分析

阴影体&#xff08;Shadow Volume&#xff09;技术是计算机图形学中实现阴影的重要方式&#xff0c;除了用于可视化阴影效果外&#xff0c;阴影体还能实现线、面等要素的贴地、贴对象显示。在用阴影体贴地、贴对象时&#xff0c;大多数情况下我们都会认为阴影体是一个带有高度的…

OpenCV的图像矩(64)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV如何为等值线创建边界旋转框和椭圆(63) 下一篇 &#xff1a;OpenCV4.9的点多边形测试(65) Image Moments&#xff08;图像矩&#xff09;是 OpenCV 库中的一个功能&#xff0c;它可…

神经网络中常见的激活函数:理解与实践

神经网络中常见的激活函数&#xff1a;理解与实践 在神经网络中&#xff0c;激活函数是一个非常重要的组成部分&#xff0c;它为神经元引入了非线性特性&#xff0c;使得神经网络可以拟合各种复杂的函数关系。本文将介绍9种常见的激活函数&#xff0c;包括它们的概述、公式以及…

python gmssl SM4不填充加解密

问题描述 使用gmssl(python版本)进行SM4加/解密时结果与国标(GMT0002)不符&#xff0c;或解密失败&#xff0c;原因是gmssl默认使用PKCS7填充&#xff0c;国标文档里的样例是没有填充的。 解决方法 方法一&#xff1a;创建CryptSM4对象时将填充模式设为-1。这是笔者推荐的方法…

MATLAB 数据导入

MATLAB 数据导入&#xff08;ImportData&#xff09; 在MATLAB中导入数据意味着从外部文件加载数据。该importdata功能允许加载不同格式的各种数据文件。它具有以下五种形式 序号 功能说明 1 A importdata(filename) 从filename表示的文件中将数据加载到数组A中。 2 A i…

MySQL-配置文件

1、配置文件格式 配置文件中启动选项被分为若干组&#xff0c;每组都有一个’组名’&#xff0c;用[ ] 包裹每组下都可定义若干个启动选项配置文件中指定的启动选项不允许添加--前缀配置文件中每行只能指定一个具体启动选项相关分组示例如下&#xff1a; [server] (具体启动选…

附录3-小程序常用事件

目录 1 点击事件 tap 2 文本框输入事件 input 3 状态改变事件 change 4 下拉刷新事件 onPullDownRefresh() 5 上拉触底事件 onReachBottom() 1 点击事件 tap 2 文本框输入事件 input 可以使用 e.detail.value 打印出当前文本框的值 我现在在文本框中依次输入12345&…

区块链 | IPFS 工作原理入门

&#x1f98a;原文&#xff1a;What is the InterPlanetary File System (IPFS), and how does it work? &#x1f98a;写在前面&#xff1a;本文属于搬运博客&#xff0c;自己留存学习。 1 去中心化互联网 尽管万维网是一个全球性的网络&#xff0c;但在数据存储方面&#…

帕金森患者应该怎么注意生活方式?

在面对帕金森病的挑战时&#xff0c;科学合理地改善日常生活方式&#xff0c;不仅能帮助患者更好地管理病情&#xff0c;还能提升生活质量。今天&#xff0c;让我们一起探索如何通过简单的日常调整&#xff0c;为患有帕金森病的朋友们带来积极的变化。 饮食调整&#xff1a;营养…