深刻理解分布式之抉择分布式锁

 

引言node

为何写这篇文章?redis

目前网上大部分的基于zookeeper,和redis的分布式锁的文章都不够全面。要么就是特地避开集群的状况,要么就是考虑不全,读者看着仍是一脸迷茫。坦白说,这种老题材,很难写出新创意,博主心里战战兢兢,如履薄冰,文中有什么不严谨之处,欢迎批评。算法

博主的这篇文章,不上代码,只讲分析api

(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所实现的分布式锁。并发

文章结构dom

本文借鉴了两篇国外大神的文章,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] then return redis.call("del",KEYS[1]) else return 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

7.给你们推荐一个架构交流qq群:698581634 进群便可免费获取资料,有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。

还有一点,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,存在着性能瓶颈。你们在生产上使用,可自行进行评估使用。

相关文章
相关标签/搜索