分布式锁 关键技术、常看法决方案

| 导语 在单机环境下,因为使用环境简单和通讯可靠,锁的可见性和原子性很容易能够保证,能够简单和可靠地实现锁功能。到了分布式的环境下,因为公共资源和使用方之间的分离,以及使用方和使用方之间的分离,相互之间的通讯由线程间的内存通讯变为网络通讯。网络通讯的时延和不可靠,加上分布式环境中各类故障的常态化发生,致使实现一个可靠的分布式锁服务须要考虑更多更复杂的问题。
 

 

锁,核心是协调各个使用方对公共资源使用的一种机制。当存在多个使用方互斥地使用某一个公共资源时,为了不并行使用致使的修改结果不可控,须要在某个地方记录一个标记,这个标记可以被全部使用方看到,当标记不存在时,能够设置标记而且得到公共资源的使用权,其他使用者发现标记已经存在时,只能等待标记拥有方释放后,再去尝试设置标记。这个标记便可以理解为锁。redis

在单机多线程的环境下,因为使用环境简单和通讯可靠,锁的可见性和原子性很容易能够保证,因此使用系统提供的互斥锁等方案,能够简单和可靠地实现锁功能。到了分布式的环境下,因为公共资源和使用方之间的分离,以及使用方和使用方之间的分离,相互之间的通讯由线程间的内存通讯变为网络通讯。网络通讯的时延和不可靠,加上分布式环境中各类故障的常态化发生,致使实现一个可靠的分布式锁服务须要考虑更多更复杂的问题。算法

      目前常见的分布式锁服务,能够分为如下两大类:
  •       一、基于分布式缓存实现的锁服务及其变种:典型表明是使用Redis实现的锁服务和基于Redis实现的RedLock方案;
  •       二、基于分布式一致性算法实现的锁服务:典型表明为Zookeeper和Chubby。

本文从上述两大类常见的分布式锁服务实现方案入手,从分布式锁服务的各个核心问题(核心架构、锁数据一致性、锁服务可用性、死锁预防机制、易用性、性能)展开,尝试对比分析各个实现方案的优劣和特色。数据库

一、基于分布式缓存实现的锁服务

基于分布式缓存实现的锁服务,思路最为简单和直观。和单机环境的锁同样,咱们把锁数据存放在分布式环境中的一个惟一结点,全部须要获取锁的调用方,都去此结点访问,从而实现对调用方的互斥,而存放锁数据的结点,使用各种分布式缓存产品充当。其核心架构以下(以Redis为例):后端

1.基于分布式缓存实现的锁服务典型架构缓存

1.1 加解锁流程

基于Redis官方的文档,对于一个尝试获取锁的操做,流程以下:安全

一、  向Redis结点发送命令:服务器

                          SET (key=Lock_Name, value=my_random_value) NX PX 30000

其中:网络

  • 1)、my_random_value是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在全部客户端的全部获取锁的请求中都是惟一的,用于惟一标识锁持有方。
  • 2)、NX表示只有当Lock_Name对应的key值不存在的时候才能SET成功。这保证了只有第一个请求的客户端才能得到锁,而其它客户端在锁被释放以前都没法得到锁。
  • 3)、PX 30000表示这个锁结点有一个30秒的自动过时时间。(自动过时时间,目的是为了防止持有锁的客户端故障后,锁没法被释放致使死锁而设置,从而要求锁拥有者必须在过时时间以内执行完相关操做并释放锁)。(对于第2、三点的特性,目前应该绝大部分缓存产品都具有)

二、  若是命令返回成功,则表明获取锁成功,不然获取锁失败。多线程

对于一个拥有锁的客户端,释放锁,其流程以下:架构

一、  向Redis结点发送命令:
                                                            GET (key=Lock_Name)
二、  若是查询回来的value和自己my_random_value一致,则表示本身是锁的持有者,能够发起解锁操做,发送命令:
                                                           DEL (key=Lock_Name)

1.2 锁安全性分析

基于上述流程,因为Redis结点是单点存在,因此在锁过时时间以内且Redis结点不发生故障的状况下,锁的安全性(即互斥性)能够获得保证。可是仍然有以下几个问题须要考虑:

一、  预防死锁的必要性

考虑以下场景,一个客户端获取锁成功,可是在释放锁以前崩溃了,此时实际上它已经放弃了对公共资源的操做权,可是却没有办法请求解锁,那么它就会一直持有这个锁,而其它客户端永远没法得到锁。所以,对于绝大部分场景,此类死锁场景是应该获得考虑和避免。

二、  引入锁自动过时时间来预防死锁带来的问题

为了预防死锁,利用分布式缓存的结点自动过时特性来按期删除死锁结点,看似能够解决问题。可是其中隐藏的隐患是:实质上,锁自动过时清理是释放了一个不属于本身的锁。那么几乎必然的,会破坏锁的互斥性,考虑以下场景:

  • 1)、客户端1获取锁成功
  • 2)、客户端1在某个操做上阻塞了很长时间
  • 3)、过时时间到,锁自动释放
  • 4)、客户端2获取到了对应同一个资源的锁
  • 5)、客户端1从阻塞中恢复过来,认为本身依旧持有锁,继续操做同一个资源,致使互斥性失效

也许有一个疑问,第五步中,客户端1恢复回来后,能够比较下目前已经持有锁的时间,若是发现已经快过时,则放弃对共享资源的操做便可避免互斥性失效的问题。事实上,客户端1的时间和Redis结点的时间自己就存在偏移的可能性,更极端一点,Redis上的时间还可能发生跳变或者比客户端时间跑得更快,因此,严格来说,任何依赖两个时间比较的互斥性算法,都存在潜在的隐患。

三、  解锁操做的原子性

引入全局惟一的my_random_value,目的是想保证每次解锁操做,必定是解锁的本身加的锁。因为Redis没有可以提供基于数据版本号来删除Key的原子操做的特性,其Watch的CAS机制自己基于链接(有其余的分布式缓存产品可以支持这个特性)。所以解锁须要两步,先查锁回来确认Value这把锁是本身加的,而后再发起Del解锁。因为Get和Del操做的非原子性,那么解锁自己也会存在破坏互斥性的状况,考虑以下场景:

  • 1)、客户端1获取锁成功。
  • 2)、客户端1访问共享资源。
  • 3)、客户端1为了释放锁,先执行'GET'操做获取随机字符串的值。
  • 4)、客户端1判断随机字符串的值,与预期的值相等。
  • 5)、客户端1因为某个缘由阻塞住了很长时间。
  • 6)、过时时间到了,锁自动释放了。
  • 7)、客户端2获取到了对应同一个资源的锁。
  • 8)、客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。

四、  Redis结点故障后,主备切换的数据一致性

考虑Redis结点宕机,若是长时间没法恢复,则致使锁服务长时间不可用。为了保证锁服务的可用性,一般的方案是给这个Redis节点挂一个Slave,当Master节点不可用的时候,系统自动切到Slave上。可是因为Redis的主从复制(replication)是异步的,这可能致使在宕机切换过程当中丧失锁的安全性。考虑下面的时序:

  • 1)、客户端1从Master获取了锁。
  • 2)、Master宕机了,存储锁的key尚未来得及同步到Slave上。
  • 3)、Slave升级为Master。
  • 4)、客户端2重新的Master获取到了对应同一个资源的锁。
  • 5)、客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。

         设想下,若是要避免这种状况,只有在写数据的时候,就阻塞地把数据写多份,所有写成功才返回,这样才能保证锁的安全性(分布式缓存的同步主从复制)。但这样就能够即保证数据一致性,又保证服务可用性了吗?其实否则,在锁数据写Master和Slave两份,都写成功才认为加锁成功的状况下,若是Master写成功,Slave写超时(其实写成功了),这个时候认为加锁是失败的,可是主和备的数据产生了不一致,并且Slave自身稳定性以及Master和Slave的通讯稳定性还成为了致使服务不可用的额外因素。因此基于分布式缓存实现的锁服务,要想解决分布式系统一致性和可用性的核心问题,并非简单的主从同步能够搞定(核心仍是要靠Paxos这样的分布式一致性协议)。

1.3 总结

一、锁服务性能

因为锁数据基于Redis等分布式缓存保存,基于内存的数据操做特性使得这类锁服务拥有着很是好的性能表现。同时锁服务调用方和锁服务自己只有一次RTT就能够完成交互,使得加锁延迟也很低。因此,高性能、低延迟是基于分布式缓存实现锁服务的一大优点。所以,在对性能要求较高,可是能够容忍极端状况下丢失锁数据安全性的场景下,很是适用。

二、数据一致性和可用性

锁数据一致性基于上述的分析,基于分布式缓存的锁服务受限于通用分布式缓存的定位,没法彻底保证锁数据的安全性,核心的问题能够归纳为三点:

  • 1)、锁数据写入的时候,没有保证同时写成功多份:任何过后的同步在机制上都是不够安全的,所以在故障时,锁数据存在丢失的可能。解决此类问题,须要在写多份和服务可用性之间找到平衡(典型思想:多数派,详细描述见后面的两类锁服务方案)。
  • 2)、没有原子性的保证持有者才能解锁:锁服务须要提供一种机制,使得在网络各类乱序以及包重放的时候,保证只有锁当前持有者方能解锁,同时要保证解锁操做的原子性。
  • 3)、锁服务缺少和调用方(或者公共资源方)的确认机制:预防死锁等问题,光靠锁服务自身,是不够安全的,只有调用方和公共资源的一同参与,方能全面保证(Chubby提供了一种作法,详细见后面的描述)

二、基于分布式缓存实现锁服务的变种

基于分布式缓存实现锁服务,在业界还存在各种变种的方案,其核心是利用不一样分布式缓存产品的额外特性,来改善基础方案的各种缺点,各种变种方案能提供的安全性和可用性也不尽相同。此处介绍一种业界最出名,同时也是引发过最大争论的一个锁服务变种方案-RedLock。

RedLock由Redis的做者Antirez提出,算是Redis官方对于实现分布式锁的指导规范。Redlock的算法描述就放在Redis的官网上(https://redis.io/topics/distlock)。

         选择对比分析RedLock,第一是由于它做为Redis官方的锁服务指导规范,在提出的时候业内也对其进行过不少争议和讨论;第二是RedLock的算法中,已经有了分布式一致性算法中最核心的概念-多数派的思想。所以咱们在众多变种中选择RedLock来进行介绍和分析。

2.1 核心架构和流程

2.RedLock锁服务流程图

对于一个客户端,依次执行下面各个步骤,来完成获取锁的操做:

  • 1)、获取当前时间(毫秒数)。
  • 2)、按顺序依次向N个Redis节点执行获取锁的操做(其实能够并发同时向N个Redis获取锁)。这个获取操做跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过时时间。为了保证在某个Redis节点不可用的时候算法可以继续运行,这个获取锁的操做还有一个超时时间,它要远小于锁的有效时间。客户端在向某个Redis节点获取锁失败之后,应该当即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,好比该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有。
  • 3)、计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。若是客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,而且获取锁总共消耗的时间没有超过锁的有效时间,那么这时客户端才认为最终获取锁成功;不然,认为最终获取锁失败。
  • 4)、若是最终获取锁成功了,那么这个锁的有效时间应该从新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
  • 5)、若是最终获取锁失败了(可能因为获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该当即向全部Redis节点发起释放锁的操做(同基于单Redis节点的释放一致)。释放锁的过程比较简单,客户端向全部Redis节点发起释放锁的操做,无论这些节点当时在获取锁的时候成功与否。

2.2 锁安全性分析

RedLock算法的最核心也是最有价值之处,是引入了多数派思想,来解决单点故障对数据安全性和服务可用性的影响。因为加锁成功须要全部Redis结点中的多数结点赞成,所以只要集群中结点有一半可以提供服务时,服务的可用性就可以保证。同时对于数据的一致性,只要对于一把锁,其多数派结点的数据不丢,那么锁就不可能被另外的调用方同时得到(不够多数派),因此锁的安全性也能够获得保证。因此从核心算法来讲,多数派的思想是对数据一致性的保证下,向保证服务可用性又进了一大步。

可是,多数派仅仅是算法最核心的理论保证。要实现一个工程上彻底保证锁数据安全性,同时高可用的锁服务,RedLock还有很远的距离,这也是RedLock在业界引发不少争议的地方,核心的问题见下面的分析。

一、  RedLock的安全性依旧强依赖于系统时间

在以前单点Redis锁服务的时候已经分析过,因为为了预防死锁,使用了过时自动删除锁的机制,因此致使安全性依赖于单机Redis上的时间服务不能异常,从而存在隐患(本质是违反了锁持有者才能删除锁的原则)。一样的,到了RedLock中,仍然有此问题,考虑以下的时序:假设一共有5个Redis节点:A, B, C, D, E。

  • 1)、客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
  • 2)、节点C时间异常,致使C上的锁数据提早到期,而被释放。
  • 3)、客户端2此时尝试获取同一把锁:锁住了C, D, E,获取锁成功。

         因此一个安全的算法,是不该该依赖于系统时间的。消息可能在网络中延迟任意长的时间,甚至丢失,系统时钟也可能以任意方式出错。一个好的分布式算法,这些因素不该该影响它的安全性,只可能影响到它的有效性,也就是说,即便在很是极端的状况下(好比系统时钟严重错误),算法顶可能是不能在有限的时间内给出结果而已,而不该该给出错误的结果。

二、  缺少锁数据丢失的识别机制和恢复机制

假设一共有5个Redis节点:A, B, C, D, E。见以下的事件序列:

  • 1)、客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
  • 2)、节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。(Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),所以最坏状况下可能丢失1秒的数据。为了尽量不丢数据,Redis容许设置成每次修改数据都进行fsync,但这会下降性能。固然,即便执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现))。
  • 3)、节点C重启后,客户端2锁住了C, D, E,获取锁成功。
  • 4)、客户端1和客户端2如今都认为本身持有了锁。

此类问题的本质,是做为多数派数据的一个结点,数据丢失以后(好比故障未落地、超时被清理等等),首先没有可以区分丢失了哪些数据的能力,其次尚未恢复丢失数据的能力。这两种能力都缺少的状况下,数据结点就继续正常地参与投票,从而致使的数据一致性被破坏。

RedLock也意识到了这个问题,因此在其中有一个延迟重启(delayed restarts)的概念。也就是说,一个节点崩溃后,先不当即重启它,而是等待一段时间再重启,这段时间应该大于其上全部锁的有效时间的最大值。这样的话,这个节点在重启前所参与的锁都会过时,它在重启后就不会对现有的锁形成影响。这个方案,是在缺少丢失数据识别的能力下,实现的较“悲观”的一个替代方案,首先其方案依旧依赖于时间,其次如何肯定最大过时时间,也是一个麻烦的事情,由于最大过时时间极可能也一块儿丢失了(未持久化),再有延迟重启使得故障结点恢复的时间延长,增长了集群服务可用性的隐患。怎么来看,都不算一个优雅的方案。

2.3 总结

一、锁服务性能

因为RedLock锁数据仍然基于Redis保存,因此和基于单点的Redis锁同样,具备高性能和低延迟的特性,不过因为引入多数派的思想,加锁和解锁时的并发写,因此在流量消耗来讲,比基于单点的Redis锁消耗要大。从资源角度来讲,是用流量换取了比单点Redis稍高的数据一致性和服务可用性。

二、数据一致性和可用性

RedLock的核心价值,在于多数派思想。不过根据上面的分析,它依然不是一个工程上能够彻底保证锁数据一致性的锁服务。相比于基于单点Redis的锁服务,RedLock解决了锁数据写入时多份的问题,从而能够克服单点故障下的数据一致性问题,可是仍是受限于通用存储的定位,其锁服务总体机制上的不完备,使得没法彻底保证锁数据的安全性。在继承自基于单点的Redis锁服务缺陷(解锁不具有原子性;锁服务、调用方、资源方缺少确认机制)的基础上,其核心的问题为:缺少锁数据丢失的识别和学习机制。

RedLock中的每台Redis,充当的仍旧只是存储锁数据的功能,每台Redis之间各自独立,单台Redis缺少全局的信息,天然也不知道本身的锁数据是不是完整的。在单台Redis数据的不完整的前提下,没有识别和学习机制,使得在各类分布式环境的典型场景下(结点故障、网络丢包、网络乱序),没有完整数据但参与决策,从而破坏数据一致性。

三、基于分布式一致性算法实现的锁服务

分析完上述的锁服务方案,能够看到,各类方案核心仍是在一致性和可用性之间作取舍。对于锁服务自己的定位和用途而言,其是一个相对中心化,对数据一致性有严格要求的场景。因此在分布式环境下,把数据严格一致性做为第一要求的状况下,Paxos算法是绕不开的一个算法(“all working protocols for asynchronous consensus we have so far encountered have Paxos at their core”)。因而就有了Chubby和Zookeeper这类,基于分布式一致性算法(核心是Paxos和相关变种)实现的锁服务。

Chubby是由Google开发实现,在其内部使用的一个分布式锁服务,其核心设计Google经过论文的形式开源出来。而Zookeeper做为Chubby的开源实现版本,由开源社区开发,目前也普遍应用在各类场景下。因为Zookeeper和Chubby之间的关系,二者在绝大部分的设计上都十分类似,所以此部分以Chubby为例,来分析此类锁服务的相关特色,关于Zookeeper和Chubby设计上的差别,在本节最后简要分析。

3.1 Chubby设计细节

Chubby核心架构

3.Chubby的系统结构

如上图,一个典型的Chubby集群,或者叫Chubby Cell,一般由5台服务器(奇数台)组成。这些服务器之间采用Paxos协议,经过投票方式决定一个服务器做为Master。一旦一个服务器成为Master,Chubby会保证一段时间其余服务器不会成为Master,这段时间被称为租期。在运行过程当中,Master服务器会不断续租,若是Master服务器发生故障,余下的服务器会选举新的Master产生新的Master服务器。

Chubby客户端经过DNS发现Chubby集群的地址,而后Chubby客户端会向Chubby集群询问Master服务器IP,在询问过程,那些非Master服务器会将Master服务器标识反馈给客户端,能够很是快的定位到Master。

在实际运行中,全部的读写请求都发给Master。针对写请求,Chubby Master会采用一致性协议将其广播到全部副本服务器,而且在过半机器接受请求后,再响应客户端。对于读请求,Master服务器直接处理返回。

Chubby的一致性协议是Paxos算法的工程实现,对于Paxos协议自己,因为不是此文的重点,因此此处不展开详细介绍。整体上,能够理解为Chubby的一致性协议,能够保证经过Master写成功以后的数据,最终会扩散到集群内的全部机器上,同时对于屡次的写操做,Chubby能够严格保证时序(不管是Master挂掉从新选举产生新Master,仍是其中非Master机器的故障或者是被替换),另外从Master读取的数据也是最新的数据。而知足这一切要求的前提,只须要Chubby集群中的大部分机器能够正常提供服务便可。

每台Chubby服务器的基本架构大体分三层:

4.Chubby结点的基本架构

  • 一、最底层是容错日志系统,经过Paxos协议保证集群上的日志彻底一致。
  • 二、日志之上是KV类型的容错数据库,经过下层的日志来保证一致性和容错性。
  • 三、最上层是对外提供的分布式锁服务和小文件存储服务。

Chubby数据组织形式

Chubby做为分布式锁服务,提供的数据操做接口是相似于Unix文件系统接口风格的接口,这样设计的初衷听说是文件系统操做风格的接口在Google内部更加符合使用者的习惯。Chubby中全部的数据都是以文件结点的形式提供给调用者访问,Chubby中典型的结点以下:/ls/foo/wombat/pouch。

结点分为永久结点和临时结点,临时结点在没有客户端打开或者其子目录下已经为空的状况下自动删除。每一个结点均可以设置访问控制权限(ACL),同时结点的原数据(MeteData)中有四个递增的64位数,用于区分结点在各个方面的修改时序:一、实体编号:区分同名结点的前后;二、文件内容编号:文件内容修改时自增;三、锁编号:锁结点被获取时自增;四、ACL编号:ACL变化时自增。

基于文件结点的组织形式,Chubby提供的数据操做API以下:

  • 一、Open():打开文件结点;
  • 二、Close():关闭文件结点;
  • 三、GetContentsAndStat():获取文件内容;
  • 四、SetContents():写文件,能够同时提供文件内容编号,Chubby验证文件目前的序号和提供的一致,方能修改;
  • 五、Delete():删除文件(须要文件无子节点);
  • 六、 Acquire(), TryAcquire(), Release():获取和释放锁
  • 七、 GetSequencer():加锁成功后,获取锁序号;
  • 八、 CheckSequencer():检验锁序号是否有效。

Chubby事件机制

为了不大量客户端轮询服务器带来的压力,Chubby提供了事件通知机制。Chubby客户端能够向Chubby注册事件通知,当触发了这些事件后服务端就会向客户端发送事件通知。Chubby支持的事件类型包括不限于:

  • 一、文件内容变化:一般用于监控文件是否被其余客户端修改;
  • 二、子节点新增、删除、变化:用于监控文件结点的变化;
  • 三、Master故障:警告客户端可能已经没法再收到其余事件了,须要从新检查数据;
  • 四、文件句柄已经失效:通知客户端目前使用的文件句柄已经失效,一般是网络问题。

3.2 Chubby加锁流程

结合上述Chubby的设计细节,Chubby中客户端完成加锁的操做序列以下:

  • 一、  全部客户端打开锁文件(Open),尝试获取锁(Acquire)。
  • 二、  只有一个客户端成功,其余失败。
  • 三、  成功的客户端得到了锁,能够写本身的相关信息到文件(SetContent),其余客户端能够读取到锁持有者的信息(GetContentsAndStat)(能够经过订阅事件,也能够经过加锁失败后去读取结果)。
  • 四、  获取锁失败的客户端,能够随后轮询再试,也能够经过订阅事件,等待锁释放后Chubby的主动通知。
  • 五、  加锁成功的客户端从Master获取一个序列号(GetSequencer),而后在与后端资源服务器的通讯的时候带上此序列号,后端资源服务器处理请求前,调用CheckSequence去检查锁是否依然有效。有效则认为此客户端依旧是锁的持有者,能够为其提供服务。

Chubby的加锁流程看起来十分简单,咱们来详细分析下,Chubby如何解决以前几种方案碰到的问题:

结点故障,数据的一致性保证

  • 一、  Master故障后,Chubby集群内部会经过一致性协议从新发起选主流程,在新Master产生以前,Chubby集群没法对外提供服务,一致性协议保证Chubby集群内能够选出惟一的Master。
  • 二、  新当选的Master结点,因为以前已经在集群中,因此其上已经有绝大部分的数据。而对于暂未同步的数据(即新Master原来做为普通结点,未参与投票的那部分数据),新Master经过一致性协议从集群内其余结点学习获得。学习完毕后,新Master开始对外提供服务。整个过程,耗时在6-30S之间。
  • 三、  对于一台普通结点故障,若是在短期内恢复,那么其使用本地的一致性日志恢复数据,再用一致性协议和Master学习未同步的数据,学习完毕后,参与投票。
  • 四、  对于一台普通结点故障,若是在长时间都没法恢复,那么使用新的空闲结点替换,替换时使用集群其余结点的一致性日志文件恢复绝大部分数据,剩余的再用一致性协议和Master学习,学习完毕后,参与投票。

所以,Chubby经过一致性协议,解决了单点Redis数据没有多份的问题,同时解决了RedLock没法识别缺失数据和学习缺失数据的问题。在可用性方面,只要集群大部分机器正常工做,Chubby就能保持正常对外提供服务。在数据一致性和可用性方面,Chubby这类方案明显优于前两种方案(这自己就是Paxos协议的长处)。

预防死锁

  • 一、  Client和Master之间经过保活包(KeepLive)维护会话(Session)。保活包由Client定时发起给Master,Master收到KeepLive包以后,能够当即回复(有事件须要下发时),也能够等待一段时间,等到Session快超时前回复,Client收到回复后,再马上发起另一个新的保活包。保活包承担了延长Session租约和通知事件的功能。
  • 二、  当锁持有方A发生故障,Session没法维护。Master会在Session租约到期后,自动删除该Client持有的锁,以免锁长时间没法释放而致使死锁。
  • 三、  另一个客户端B发现锁已经被释放,发起获取锁操做,成功获取到锁;
  • 四、  对于同一把锁,每一次获取锁操做,都会使得锁的序列号(GetSequencer)自增。锁序号是一个64位自增的整形。
  • 五、  对于锁持有方,每次去访问后续的资源服务器时,都会带上本身的锁序号;而资源服务器在处理请求以前,会去Master请求验证当前锁序号是不是最新(CheckSequencer)。
  • 六、  因此考虑这样一种状况,若是在B获取锁成功以后,A恢复了,A认为本身仍旧持有锁,而发起修改资源的请求,会由于锁序号已通过期而失败,从而保障了锁的安全性。

总结起来,Chubby引入了资源方和锁服务的验证,来避免了锁服务自己孤立地作预防死锁机制而致使的破坏锁安全性的风险。同时依靠Session来维持锁的持有状态,在正常状况下,客户端能够持有锁任意长的时间,这能够确保它作完全部须要的资源访问操做以后再释放锁。这避免了基于Redis的锁对于有效时间(lock validity time)到底设置多长的两难问题。

不过引入的代价是资源方须要做对应修改,对于资源方不方便做修改的场景,Chubby提供了一种替代的机制Lock-Delay,来尽可能避免因为预防死锁而致使的锁安全性被破坏。Chubby容许客户端为持有的锁指定一个Lock-Delay的时间值(默认是1分钟)。当Chubby发现客户端被动失去联系的时候,并不会当即释放锁,而是会在Lock-Delay指定的时间内阻止其它客户端得到这个锁。这是为了在把锁分配给新的客户端以前,让以前持有锁的客户端有充分的时间把请求队列排空(draining the queue),尽可能防止出现延迟到达的未处理请求。

可见,为了应对锁失效问题,Chubby提供的两种处理方式:CheckSequencer()检查和Lock-Delay,它们对于安全性的保证是从强到弱的。可是Chubby确实提供了单调递增的锁序号,以及资源服务器和Chubby的沟通渠道,这就容许资源服务器在须要的时候,利用它提供更强的安全性保障。

Zookeeper和Chubby的设计差别

一、  对于服务读负载的取舍

Chubby设计为全部的读写都通过Master处理,这必然致使Master的负载太高,所以Chubby在Client端实现了缓存机制。Client端在本地有文件内容的Cache,Client端对Cache的维护只是负责让Cache失效,而不持续更新Cache,失效后的Cache,在Client下一次访问Master以后从新建立。每次修改后,Master经过Client的保活包(因此保活包除了有延长Session租约和通知事件的功能外,还有一个功能是Cacha失效),通知每一个拥有此Cache的Client(Master维护了每一个Client可能拥有的Cache信息),让他们的Cache失效,Client收到保活包以后,删除本地Cache。若是Client未收到本次保活包,那么只有两种可能,后续的保活包学习到Cache失效的内容,或者Session超时,清空全部Cache从新创建Session。因此对Cache机制而言,不可以保证Cache数据的随时最新,可是能够保证最终的Cache数据一致性,同时能够大量避免每次向Master读带来的网络流量开销和Master的高负载。

Zookeeper设计采起了另一个思路,其中Client能够链接集群中任意一个节点,而不是必需要链接Master。Client的全部写请求必须转给Master处理,而读请求,能够由普通结点直接处理返回,从而分担了Master的负载。一样的,读数据不能保证时刻的一致性,可是能够保证最终一致性。

二、  预防死锁方面

Chubby提供了CheckSequencer()检查和Lock-Delay两种方式来避免锁失效带来的问题,引入了资源方和锁服务方的交互来保证锁数据安全性。不过Zookeeper目前尚未相似于CheckSequencer的机制,而只有相似于Lock-Delay的等待机制来尽可能避免锁失效带来的安全性问题。因此在锁失效方面的安全性来讲,Chubby提供了更好的保证。

三、  锁使用便利性方面的差别

Chubby和Zookeeper都提供了事件机制,这个机制能够这样来使用,好比当客户端试图建立/lock的时候,发现它已经存在了,这时候建立失败,但客户端不必定就此对外宣告获取锁失败。客户端能够进入一种等待状态,等待当/lock节点被释放的时候,锁服务经过事件机制通知它,这样它就能够继续完成建立操做(获取锁)。这可让分布式锁在客户端用起来就像一个本地的锁同样:加锁失败就阻塞住,直到获取到锁为止。

可是考虑这样一个问题,当有大量的客户端都阻塞在/lock结点上时,一旦以前的持有者释放锁,那么阻塞的潜在调用方都会被激活,可是大量客户端被激活,从新发起加锁操做时,又只有一个客户端能成功,形成所谓的“惊群”效应。

考虑到这一点,Zookeeper上实现了一个“有序临时结点”的功能,来避免惊群。对于一个临时锁结点,Zookeeper支持每次建立均可以成功,可是每次建立的结点经过一个自增的序号来区别。建立成功最小结点的客户端代表得到了锁,而其余调用方建立的结点序号代表其处于锁等待队列中的位置。因此,对于获取锁失败的客户端,其只须要监听序号比其小的最大结点的释放状况,就能够判断什么时候本身有机会竞争锁。而不是每次一旦有锁释放,都去尝试从新加锁,从而避免“惊群”效应产生。

四、 结语

本文经过分析三类分布式锁服务,基本涵盖了全部分布式锁服务中涉及到的关键技术,以及对应具体的工程实现方案。

基于分布式存储实现的锁服务,因为其内存数据存储的特性,因此具备结构简单,高性能和低延迟的优势。可是受限于通用存储的定位,其在锁数据一致性上缺少严格保证,同时

其在解锁验证、故障切换、死锁处理等方面,存在各类问题。因此其适用于在对性能要求较高,可是能够容忍极端状况下丢失锁数据安全性的场景下。

基于分布式一致性算法实现的锁服务,其使用Paxos协议保证了锁数据的严格一致性,同时又具有高可用性。在要求锁数据严格一致的场景下,此类锁服务几乎是惟一的选择。可是因为其结构和分布式一致性协议的复杂性,其在性能和加锁延迟上,比基于分布式存储实现的锁服务要逊色。

因此实际应用场景下,须要根据具体需求出发,权衡各类考虑因素,选择合适的锁服务实现模型。不管选择哪种模型,须要咱们清楚地知道它在安全性上有哪些不足,以及它会带来什么后果。更特别的,若是是对锁数据安全性要求十分严格的应用场景,那么须要更加慎之又慎。在本文的讨论中,咱们看到,在分布式锁的正确性上走得最远的,是基于Paxos实现,同时引入分布式资源进行验证的方案。下一篇,咱们来介绍欢乐游戏基于实际业务场景,结合三类方案各自的特色,实现的一个严格保证锁数据安全性的高可用高性能锁服务方案。

相关文章
相关标签/搜索