分布式锁(基于redis和zookeeper)详解

分布式锁(基于redis和zookeeper)详解

https://blog.csdn.net/a15835774652/article/details/81775044

为什么写这篇文章?

目前网上大部分的基于zookeeper,和redis的分布式锁的文章都不够全面。要么就是特意避开集群的情况,要么就是考虑不全,读者看着还是一脸迷茫。坦白说,这种老题材,很难写出新创意,博主内心战战兢兢,如履薄冰,文中有什么不严谨之处,欢迎批评。
博主的这篇文章,不上代码,只讲分析。
(1)在redis方面,有开源redisson的jar包供你使用。
(2)在zookeeper方面,有开源的curator的jar包供你使用
因为已经有开源jar包供你使用,没有必要再去自己封装一个,大家出门百度一个api即可,不需要再罗列一堆实现代码。
需要说明的是,Google有一个名为Chubby的粗粒度分布锁的服务,然而,Google Chubby并不是开源的,我们只能通过其论文和其他相关的文档中了解具体的细节。值得庆幸的是,Yahoo!借鉴Chubby的设计思想开发了zookeeper,并将其开源,因此本文不讨论Chubby。至于Tair,是阿里开源的一个分布式K-V存储方案。我们在工作中基本上redis使用的比较多,讨论Tair所实现的分布式锁,不具有代表性。
因此,主要分析的还是redis和zookeeper所实现的分布式锁。

文章结构

本文借鉴了两篇国外大神的文章,redis的作者antirez的《Is Redlock safe?》以及分布式系统专家Martin的《How to do distributed locking》,再加上自己微薄的见解从而形成这篇文章,文章的目录结构如下:
(1)为什么使用分布式锁
(2)单机情形比较
(3)集群情形比较
(4)锁的其他特性比较

正文

先上结论:
zookeeper可靠性比redis强太多,只是效率低了点,如果并发量不是特别大,追求可靠性,首选zookeeper。为了效率,则首选redis实现。

为什么使用分布式锁?

使用分布式锁的目的,无外乎就是保证同一时间只有一个客户端可以对共享资源进行操作。
但是Martin指出,根据锁的用途还可以细分为以下两类
(1)允许多个客户端操作共享资源
这种情况下,对共享资源的操作一定是幂等性操作,无论你操作多少次都不会出现不同结果。在这里使用锁,无外乎就是为了避免重复操作共享资源从而提高效率。
(2)只允许一个客户端操作共享资源
这种情况下,对共享资源的操作一般是非幂等性操作。在这种情况下,如果出现多个客户端操作共享资源,就可能意味着数据不一致,数据丢失。

第一回合,单机情形比较

(1)Redis
先说加锁,根据redis官网文档的描述,使用下面的命令加锁

SET resource_name my_random_value NX PX 30000
  • my_random_value是由客户端生成的一个随机字符串,相当于是客户端持有锁的标志

  • NX表示只有当resource_name对应的key值不存在的时候才能SET成功,相当于只有第一个请求的客户端才能获得锁

  • PX 30000表示这个锁有一个30秒的自动过期时间。

至于解锁,为了防止客户端1获得的锁,被客户端2给释放,采用下面的Lua脚本来释放锁

if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end
 

在执行这段LUA脚本的时候,KEYS[1]的值为resource_name,ARGV[1]的值为my_random_value。原理就是先获取锁对应的value值,保证和客户端穿进去的my_random_value值相等,这样就能避免自己的锁被其他人释放。另外,采取Lua脚本操作保证了原子性.如果不是原子性操作,则有了下述情况出现

分析:这套redis加解锁机制看起来很完美,然而有一个无法避免的硬伤,就是过期时间如何设置。如果客户端在操作共享资源的过程中,因为长期阻塞的原因,导致锁过期,那么接下来访问共享资源就不安全。
可是,有的人会说

那可以在客户端操作完共享资源后,判断锁是否依然归该客户端所有,如果依然归客户端所有,则提交资源,释放锁。若不归客户端所有,则不提交资源啊.

OK,这么做,只能降低多个客户端操作共享资源发生的概率,并不能解决问题。
为了方便读者理解,博主举一个业务场景。
业务场景:我们有一个内容修改页面,为了避免出现多个客户端修改同一个页面的请求,采用分布式锁。只有获得锁的客户端,才能修改页面。那么正常修改一次页面的流程如下图所示

注意看,上面的步骤(3)-->步骤(4.1)并不是原子性操作。也就说,你可能出现在步骤(3)的时候返回的是有效这个标志位,但是在传输过程中,因为延时等原因,在步骤(4.1)的时候,锁已经超时失效了。那么,这个时候锁就会被另一个客户端锁获得。就出现了两个客户端共同操作共享资源的情况。
大家可以思考一下,无论你如何采用任何补偿手段,你都只能降低多个客户端操作共享资源的概率,而无法避免。例如,你在步骤(4.1)的时候也可能发生长时间GC停顿,然后在停顿的时候,锁超时失效,从而锁也有可能被其他客户端获得。这些大家可以自行思考推敲。
(2)zookeeper
先简单说下原理,根据网上文档描述,zookeeper的分布式锁原理是利用了临时节点(EPHEMERAL)的特性。

  • 当znode被声明为EPHEMERAL的后,如果创建znode的那个客户端崩溃了,那么相应的znode会被自动删除。这样就避免了设置过期时间的问题。

  • 客户端尝试创建一个znode节点,比如/lock。那么第一个客户端就创建成功了,相当于拿到了锁;而其它的客户端会创建失败(znode已存在),获取锁失败。

分析:这种情况下,虽然避免了设置了有效时间问题,然而还是有可能出现多个客户端操作共享资源的。
大家应该知道,zookeeper如果长时间检测不到客户端的心跳的时候(Session时间),就会认为Session过期了,那么这个Session所创建的所有的ephemeral类型的znode节点都会被自动删除。
这种时候会有如下情形出现

如上图所示,客户端1发生GC停顿的时候,zookeeper检测不到心跳,也是有可能出现多个客户端同时操作共享资源的情形。当然,你可以说,我们可以通过JVM调优,避免GC停顿出现。但是注意了,我们所做的一切,只能尽可能避免多个客户端操作共享资源,无法完全消除。

第二回合,集群情形比较

我们在生产中,一般都是用集群情形,所以第一回合讨论的单机情形。算是给大家热热身。
(1)redis
为了redis的高可用,一般都会给redis的节点挂一个slave,然后采用哨兵模式进行主备切换。但由于Redis的主从复制(replication)是异步的,这可能会出现在数据同步过程中,master宕机,slave来不及同步数据就被选为master,从而数据丢失。具体流程如下所示:

  • (1)客户端1从Master获取了锁。

  • (2)Master宕机了,存储锁的key还没有来得及同步到Slave上。

  • (3)Slave升级为Master。

  • (4)客户端2从新的Master获取到了对应同一个资源的锁。

为了应对这个情形, redis的作者antirez提出了RedLock算法,步骤如下(该流程出自官方文档),假设我们有N个master节点(官方文档里将N设置成5,其实大等于3就行)

  • (1)获取当前时间(单位是毫秒)。

  • (2)轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。

  • (3)客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。

  • (4)如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。

  • (5)如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。

分析:RedLock算法细想一下还存在下面的问题
节点崩溃重启,会出现多个客户端持有锁
假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:
(1)客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
(2)节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。
(3)节点C重启后,客户端2锁住了C, D, E,获取锁成功。
这样,客户端1和客户端2同时获得了锁(针对同一资源)。

为了应对节点重启引发的锁失效问题,redis的作者antirez提出了延迟重启的概念,即一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,等待的时间大于锁的有效时间。采用这种方式,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。这其实也是通过人为补偿措施,降低不一致发生的概率。
时间跳跃问题
(1)假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:
(2)客户端1从Redis节点A, B, C成功获取了锁(多数节点)。由于网络问题,与D和E通信失败。
(3)节点C上的时钟发生了向前跳跃,导致它上面维护的锁快速过期。
客户端2从Redis节点C, D, E成功获取了同一个资源的锁(多数节点)。
客户端1和客户端2现在都认为自己持有了锁。

为了应对始终跳跃引发的锁失效问题,redis的作者antirez提出了应该禁止人为修改系统时间,使用一个不会进行“跳跃”式调整系统时钟的ntpd程序。这也是通过人为补偿措施,降低不一致发生的概率。
超时导致锁失效问题
RedLock算法并没有解决,操作共享资源超时,导致锁失效的问题。回忆一下RedLock算法的过程,如下图所示

如图所示,我们将其分为上下两个部分。对于上半部分框图里的步骤来说,无论因为什么原因发生了延迟,RedLock算法都能处理,客户端不会拿到一个它认为有效,实际却失效的锁。然而,对于下半部分框图里的步骤来说,如果发生了延迟导致锁失效,都有可能使得客户端2拿到锁。因此,RedLock算法并没有解决该问题。
(2)zookeeper
zookeeper在集群部署中,zookeeper节点数量一般是奇数,且一定大等于3。我们先回忆一下,zookeeper的写数据的原理
如图所示,这张图懒得画,直接搬其他文章的了。

那么写数据流程步骤如下
1.在Client向Follwer发出一个写的请求
2.Follwer把请求发送给Leader
3.Leader接收到以后开始发起投票并通知Follwer进行投票
4.Follwer把投票结果发送给Leader,只要半数以上返回了ACK信息,就认为通过
5.Leader将结果汇总后如果需要写入,则开始写入同时把写入操作通知给Leader,然后commit;
6.Follwer把请求结果返回给Client
还有一点,zookeeper采取的是全局串行化操作
OK,现在开始分析
集群同步
client给Follwer写数据,可是Follwer却宕机了,会出现数据不一致问题么?不可能,这种时候,client建立节点失败,根本获取不到锁。
client给Follwer写数据,Follwer将请求转发给Leader,Leader宕机了,会出现不一致的问题么?不可能,这种时候,zookeeper会选取新的leader,继续上面的提到的写流程。
总之,采用zookeeper作为分布式锁,你要么就获取不到锁,一旦获取到了,必定节点的数据是一致的,不会出现redis那种异步同步导致数据丢失的问题。
时间跳跃问题
不依赖全局时间,怎么会存在这种问题
超时导致锁失效问题
不依赖有效时间,怎么会存在这种问题

第三回合,锁的其他特性比较

(1)redis的读写性能比zookeeper强太多,如果在高并发场景中,使用zookeeper作为分布式锁,那么会出现获取锁失败的情况,存在性能瓶颈。
(2)zookeeper可以实现读写锁,redis不行。
(3)zookeeper的watch机制,客户端试图创建znode的时候,发现它已经存在了,这时候创建失败,那么进入一种等待状态,当znode节点被删除的时候,zookeeper通过watch机制通知它,这样它就可以继续完成创建操作(获取锁)。这可以让分布式锁在客户端用起来就像一个本地的锁一样:加锁失败就阻塞住,直到获取到锁为止。这套机制,redis无法实现

总结

OK,正文啰嗦了一大堆。其实只是想表明两个观点,无论是redis还是zookeeper,其实可靠性都存在一点问题。但是,zookeeper的分布式锁的可靠性比redis强太多!但是,zookeeper读写性能不如redis,存在着性能瓶颈。大家在生产上使用,可自行进行评估使用。

转载于:https://www.cnblogs.com/handsome1013/p/11430009.html

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

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

相关文章

JavaFX技巧12:在CSS中定义图标

当您是像我这样来自Swing的UI开发人员时,您很有可能仍在代码中直接设置图像/图标。 最可能是这样的: import javafx.scene.control.Label; import javafx.scene.image.ImageView;public class MyLabel extends Label {public MyLabel() {setGraphic(new…

第八次点评

本周心得: 本周没有作业 ,批改的上周的作业。需求分析以及团队的明确分工是开发前准备的要素。本次批改主要侧重于需求分析的调研。 博客园地址https://www.cnblogs.com/zhaojh123/ 博客园点评博客: https://www.cnblogs.com/yanqignkui-123/…

JDBC、Tomcat为什么要破坏双亲委派模型?

问题一:双亲委派模型是什么 如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加…

dpdk之路-环境部署

dpdk实验环境部署 1、实验环境说明 vmware workstatioin 12 centos 7.5.1804 dpdk-stable-18.11.1 2、实验步骤 (1)虚拟机安装 http://vault.centos.org/7.5.1804/isos/x86_64/从链接下载CentOS-7-x86_64-DVD-1804.iso,安装时需要准备3张虚拟…

基于知识图谱的医疗诊断系统论文

本作品禁止任何人/企业申请专利,禁止任何人使用本作品参加任何比赛或作为毕业设计,如使用本作品源码进行商业用途务必联系作者。 一.科学性 1.研究意义 信息科技经过 60 余年的发展,已经普及到社会生活的每一个角落。随着信息技术在国家治理、…

华为云(ECS)-linux服务器中-Ubuntu图形界面安装-解决root登录受限-VNCviwer/Teamviwer远程访问教程...

安装ubuntu-desktop 1.更新软件库 apt-get update2.升级软件 apt-get upgrade3.安装桌面 apt-get install ubuntu-desktop 解决root登录受限 华为云登录进去是guest用户,不能选择登录用户且不需要密码就即可登录。 登录进去会如下警告信息: 首先下载…

Java EE并发API教程

这是一个示例章节,摘自Francesco Marchioni编辑的WildFly上的实用Java EE 7开发 。 本章讨论了新的Java EE并发API(JSR 236) ,它概述了使用一组托管资源在Java EE容器上并行执行任务的标准方法。 为了描述如何在您的应用程序中使…

经桥科技与湖南文化艺术产业集团合力打造“网乐潇湘”

签约仪式现场 签约仪式现场 签约仪式现场 经网1月7日讯(记者 陈飞 曹亮) 湖南经桥网络科技有限公司与湖南文化艺术产业集团“网乐潇湘”项目签约仪式隆重举行。经桥科技董事长姜志强、湖南文化艺术产业集团总经理陈介辉等领导出席此次签约仪式。 经桥科技与湖南文化艺术产业集…

带有Angular JS的Java EE 7 –第1部分

今天的帖子将向您展示如何使用Java EE 7和Angular JS构建非常简单的应用程序。 在去那里之前,让我告诉您一个简短的故事: 我不得不承认,我从来都不是Java语言的忠实拥护者,但是我仍然记得我第一次使用它。 我不记得确切的年份&am…

深入理解CSS定位中的堆叠z-index

前面的话 对于所有定位,最后都不免遇到两个元素试图放在同一位置上的情况。显然,其中一个必须盖住另一个。但,如何控制哪个元素放在上层,这就引入了属性z-index 定义 利用z-index,可以改变元素相互覆盖的顺序。这个属性…

初识服务发现及Consul框架的简单使用

初识服务发现及Consul框架的简单使用 1.什么是服务发现? 服务发现组件记录了(大规模)分布式系统中所有服务的信息,人们或者其它服务可以据此找到这些服务。 DNS 就是一个简单的例子。 当然,复杂系统的服务发现组件要提…

进程间的通信方式

5,进程间通信方式____ ,____ ,____ ,____ ,____ ,_____。 管道 消息队列 共享内存 信号 信号量 套接字 管道通信:实质是管道文件操作,分为有名管道和 无名管道两种。 无名管…

Spring MVC中的验证组

可以通过groups属性将Bean验证中的验证约束添加到一个或多个组。 这使您可以限制在验证期间应用的约束集。 在某些小组应先于其他小组进行验证(例如在向导中)的情况下,这可能很方便。 从Spring MVC 3.1开始,可以通过org.springfra…

简而言之,JUnit:Hello World

对于Java世界中的开发人员而言, JUnit似乎是最受欢迎的测试工具 。 因此,难怪就此主题已经写了一些好书 。 但是,通过以顾问为生,我仍然经常遇到程序员,他们至多对工具及其正确用法都不了解。 因此,我想到…

命令设计模式的应用

嗨,您好! 今天,我将与您分享一种非常出色的编程设计模式。 它有很多用法,是我的最爱之一。 编程设计模式命令具有多种用例。 在这篇文章中,我们将看到如何实现现实世界中的某些东西。 我们将使用电子汽车钥匙来打开&…

adodb.stream对象的方法/属性

cancel 方法 使用方法如下 object.cancel 说明:取消执行挂起的异步 execute 或 open 方法的调用。 close 方法 使用方法如下 object.close :关闭对像 copyto 方法 使用方法如下 object.copyto(deststream,[charnumber]) 说明:将对像的数据复制…

MongoDB非关系型数据库开发手册

一:NoSql数据库 什么是NoSQL? NoSQL,指的是非关系型的数据库。NoSQL有时也称作Not Only SQL的缩写,是对不同于传统的关系型数据库的数据库管理系统的统称。 NoSQL用于超大规模数据的存储。(例如谷歌或Facebook每天为他们的用户收…

解决DataGridView绑定List后不能排序的问题

阅读全文并下载例子 :http://www.sufeinet.com/forum.php?modviewthread&tid190 以前不都是用table直接绑定DataGridView的,没有出现过不能排序的问题,初试List结果发现不管怎么样都不能实现排序的功能,有朋友说 DataGridVie…

maven编译的时候排除junit测试类

maven编译的时候排除junit测试类 maven项目中使用junit进行单元测试&#xff0c;在进行编译的时候&#xff0c;可以通过2种方式排除test测试类的编译。 有2种方式 &#xff1a; 使用命令的时候带上参数 mvn install -Dmaven.test.skiptrue 在pom.xml里面配置<plugins><…

生成器设计模式的应用

嗨&#xff0c;您好&#xff01; 今天&#xff0c;我将分享我制作的全新设计模式系列的第一个。 构建器设计模式是开发严肃的应用程序时非常有用且通用的模式。 在这篇文章中&#xff0c;我将提供一个很小的构建器模式框架&#xff0c;因此您随时可以回到这里并使用它。 助记…