一、ZooKeeper基本介绍

1.1.什么是ZooKeeper

ZooKeeper 是一个开源的分布式协调服务框架,为分布式系统提供一致性服务。

那么什么是分布式?什么是协调程序?和集群又有什么区别?

举一个例子来说明,现在有一个网上商城购物系统,并发量太大单机系统承受不住,那我们可以多加几台服务器支持大并发量的访问需求,这个就是所谓的Cluster 集群 。

在这里插入图片描述

如果我们将这个网上商城购物系统拆分成多个子系统,比如订单系统、积分系统、购物车系统等等,然后将这些子系统部署在不同的服务器上 ,这个时候就是 Distributed 分布式 。

在这里插入图片描述
对于集群来说,多加几台服务器就行(当然还得解决session共享,负载均衡等问题),而对于分布式来说,你首先需要将业务进行拆分,然后再加服务器,同时还要去解决分布式带来的一系列问题。比如各个分布式组件如何协调起来,如何减少各个系统之间的耦合度,如何处理分布式事务,如何去配置整个分布式系统,如何解决各分布式子系统的数据不一致问题等等。ZooKeeper 主要就是解决这些问题的。

1.2.目前使用ZooKeeper的开源项目

许多著名的开源项目用到了 ZooKeeper,比如:

  1. Kafka : ZooKeeper 主要为 Kafka 提供 Broker 和 Topic 的注册以及多个 Partition 的负载均衡等功能。
  2. Hbase : ZooKeeper 为 Hbase 提供确保整个集群只有一个 Master 以及保存和提供 regionserver 状态信息(是否在线)等功能。
  3. Hadoop : ZooKeeper 为 Namenode 提供高可用支持。
  4. Dubbo: 阿里巴巴集团开源的分布式服务框架,它使用 ZooKeeper 来作为其命名服务,维护全局的服务地址列表。

1.3.ZooKeeper的三种运行模式

ZooKeeper 有三种运行模式:单机模式、伪集群模式和集群模式

  • 单机模式: 这种模式一般适用于开发测试环境,一方面我们没有那么多机器资源,另外就是平时的开发调试并不需要极好的稳定性。
  • 集群模式: 一个 ZooKeeper 集群通常由一组机器组成,一般 3 台以上就可以组成一个可用的 ZooKeeper 集群了。组成 ZooKeeper 集群的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都会互相保持通信。
  • 伪集群模式: 这是一种特殊的集群模式,即集群的所有服务器都部署在一台机器上。当你手头上有一台比较好的机器,如果作为单机模式进行部署,就会浪费资源,这种情况下,ZooKeeper
    允许你在一台机器上通过启动不同的端口来启动多个 ZooKeeper 服务实例,从而以集群的特性来对外服务。

二、CAP和BASE理论

一个分布式系统必然会存在一个问题:因为 分区容忍性(partition tolerance) 的存在,就必定要求我们需要在系统 可用性(availability)数据一致性(consistency) 中做出权衡 。这就是著名的 CAP 定理。

举个例子来说明,假如班级代表整个分布式系统,而学生是整个分布式系统中一个个独立的子系统。这个时候班里的小红小明偷偷谈恋爱被班里的小花发现了,小花欣喜若狂告诉了周围的人,然后小红小明谈恋爱的消息在班级里传播起来了。当在消息的传播(散布)过程中,你问班里一个同学的情况,如果他回答你不知道,那么说明整个班级系统出现了数据不一致的问题(因为小花已经知道这个消息了)。而如果他直接不回答你,因为现在消息还在班级里传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。

这个例子中前者就是 Eureka 的处理方式,它保证了AP(可用性),后者就 ZooKeeper 的处理方式,它保证了CP(数据一致性)。

CAP理论中,P(分区容忍性)是必然要满足的,因为毕竟是分布式,不能把所有的应用全放到一个服务器里面,这样服务器是吃不消的。所以,只能从AP(可用性)和CP(一致性)中找平衡。

怎么个平衡法呢?在这种环境下出现了BASE理论:即使无法做到强一致性,但分布式系统可以根据自己的业务特点,采用适当的方式来使系统达到最终的一致性。BASE理论由:Basically Avaliable 基本可用Soft state 软状态Eventually consistent 最终一致性组成。

  • 基本可用(Basically Available): 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。例如,电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层在该页面只提供降级服务。
  • 软状态(Soft State): 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有多个副本,允许不同节点间副本同步的延时就是软状态的体现。
  • 最终一致性(Eventual Consistency): 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

一句话概括就是:平时系统要求是基本可用,运行有可容忍的延迟状态,但是,无论如何经过一段时间的延迟后系统最终必须达成数据是一致的。

ACID 是传统数据库常用的设计理念,追求强一致性模型。BASE 支持的是大型分布式系统,通过牺牲强一致性获得高可用性。

其实可能发现不管是CAP理论,还是BASE理论,他们都是理论,这些理论是需要算法来实现的,这些算法有2PC、3PC、Paxos、Raft、ZAB,它们所解决的问题全部都是:在分布式环境下,怎么让系统尽可能的高可用,而且数据能最终能达到一致。

三、Zookeeper的特点

在这里插入图片描述

  1. 集群:Zookeeper是一个领导者(Leader),多个跟随者(Follower)组成的集群。
  2. 高可用性:集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。
  3. 全局数据一致:每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。
  4. 更新请求顺序进行:来自同一个Client的更新请求按其发送顺序依次执行。
  5. 数据更新原子性:一次数据更新要么成功,要么失败。
  6. 实时性:在一定时间范围内,Client能读到最新数据。
  7. 从设计模式角度来看,zk是一个基于观察者设计模式的框架,它负责管理跟存储大家都关心的数据,然后接受观察者的注册,数据反生变化zk会通知在zk上注册的观察者做出反应。
  8. Zookeeper是一个分布式协调系统,满足CP性,跟SpringCloud中的Eureka满足AP不一样。

四、一致性协议之ZAB

推荐大家先了解其他的一致性算法,如2PC、3PC、Paxos、Raft,可参考大数据中的2PC、3PC、Paxos、Raft、ZAB。

作为一个优秀高效且可靠的分布式协调框架,ZooKeeper 在解决分布式数据一致性问题时并没有直接使用 Paxos ,而是专门定制了一致性协议叫做 ZAB(ZooKeeper Automic Broadcast) 原子广播协议,该协议能够很好地支持 崩溃恢复

4.1.ZAB 中的三个角色

ZAB 中三个主要的角色,Leader 领导者、Follower跟随者、Observer观察者 。

  • Leader 集群中 唯一的写请求处理者 ,能够发起投票(投票也是为了进行写请求)。
  • Follower 能够接收客户端的请求,如果是读请求则可以自己处理,如果是写请求则要转发给 Leader 。在选举过程中会参与投票,有选举权和被选举权
  • Observer 就是没有选举权和被选举权的 Follower 。

ZAB 协议中对 zkServer(即上面我们说的三个角色的总称) 还有两种模式的定义,分别是 消息广播崩溃恢复

4.2.ZXID和myid

ZooKeeper 采用全局递增的事务 id 来标识,所有 proposal(提议)在被提出的时候加上了ZooKeeper Transaction Id 。ZXID是64位的Long类型,这是保证事务的顺序一致性的关键。ZXID中高32位表示纪元epoch,低32位表示事务标识xid。你可以认为zxid越大说明存储数据越新,如下图所示:

在这里插入图片描述

  1. 每个leader都会具有不同的epoch值,表示一个纪元/朝代,用来标识
    leader周期。每个新的选举开启时都会生成一个新的epoch,从1开始,每次选出新的Leader,epoch递增1,并会将该值更新到所有的zkServer的zxid的epoch。
  2. xid是一个依次递增的事务编号。数值越大说明数据越新,可以简单理解为递增的事务id。每次epoch变化,都将低32位的序号重置,这样保证了zxid的全局递增性。

每个ZooKeeper服务器,都需要在数据文件夹下创建一个名为myid的文件,该文件包含整个ZooKeeper集群唯一的id(整数)。例如,某ZooKeeper集群包含三台服务器,hostname分别为zoo1、zoo2和zoo3,其myid分别为1、2和3,则在配置文件中其id与hostname必须一一对应,如下所示。在该配置文件中,server.后面的数据即为myid

server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888

4.3.历史队列

每一个follower节点都会有一个先进先出(FIFO)的队列用来存放收到的事务请求,保证执行事务的顺序。所以:

1.可靠提交由ZAB的事务一致性协议保证
2.全局有序由TCP协议保证
3.因果有序由follower的历史队列(history queue)保证

4.4.消息广播模式

ZAB协议两种模式:消息广播模式和崩溃恢复模式。
在这里插入图片描述
说白了就是 ZAB 协议是如何处理写请求的,上面我们不是说只有 Leader 能处理写请求嘛?那么我们的 Follower Observer 是不是也需要 同步更新数据 呢?总不能数据只在Leader中更新了,其他角色都没有得到更新吧。

第一步肯定需要 Leader 将写请求 广播 出去呀,让Leader问问Followers是否同意更新,如果超过半数以上的同意那么就进行FollowerObserver 的更新(和 Paxos 一样)。消息广播机制是通过如下图流程保证事务的顺序一致性的:
在这里插入图片描述

  1. leader从客户端收到一个写请求
  2. leader生成一个新的事务并为这个事务生成一个唯一的ZXID
  3. leader将这个事务发送给所有的follows节点,将带有 zxid 的消息作为一个提案(proposal)分发给所有 follower。
  4. follower节点将收到的事务请求加入到历史队列(history queue)中,当 follower 接收到 proposal,先将 proposal 写到硬盘,写硬盘成功后再向 leader 回一个 ACK
  5. 当leader收到大多数follower(超过一半)的ack消息,leader会向follower发送commit请求(leader自身也要提交这个事务)
  6. 当follower收到commit请求时,会判断该事务的ZXID是不是比历史队列中的任何事务的ZXID都小,如果是则提交事务,如果不是则等待比它更小的事务的commit(保证顺序性)
  7. Leader将处理结果返回给客户端

过半写成功策略: Leader节点接收到写请求后,这个Leader会将写请求广播给各个Server,各个Server会将该写请求加入历史队列,并向Leader发送ACK信息,当Leader收到一半以上的ACK消息后,说明该写操作可以执行。Leader会向各个server发送commit消息,各个server收到消息后执行commit操作。

这里要注意以下几点:

  1. Leader并不需要得到Observer的ACK,即Observer无投票权

  2. Leader不需要得到所有Follower的ACK,只要收到过半的ACK即可,同时Leader本身对自己有一个ACK

  3. Observer虽然无投票权,但仍须同步Leader的数据从而在处理读请求时可以返回尽可能新的数据
    另外,Follower/Observer也可以接受写请求,此时:

  4. Follower/Observer接受写请求以后,不能直接处理,而需要将写请求转发给Leader处理

  5. 除了多了一步请求转发,其它流程与直接写Leader无任何区别

  6. Leader处理写请求是通过上面的消息广播模式,实质上最后所有的zkServer都要执行写操作,这样数据才会一致

而对于读请求,Leader/Follower/Observer都可直接处理读请求,从本地内存中读取数据并返回给客户端即可。由于处理读请求不需要各个服务器之间的交互,因此Follower/Observer越多,整体可处理的读请求量越大,也即读性能越好。

4.5 崩溃恢复模式

恢复模式大致可以分为四个阶段:选举、发现、同步、广播。

  1. 选举阶段(Leader election): 当leader崩溃后,集群进入选举阶段(下面会将如何选举Leader),开始选举出潜在的准 leader,然后进入下一个阶段。
  2. 发现阶段(Discovery): 用于在从节点中发现最新的ZXID和事务日志。准Leader接收所有Follower发来各自的最新epoch值。Leader从中选出最大的epoch,基于此值加1,生成新的epoch分发给各个Follower。各个Follower收到全新的epoch后,返回ACK给Leader,带上各自最大的ZXID和历史提议日志。Leader选出最大的ZXID,并更新自身历史日志,此时Leader就用拥有了最新的提议历史。(注意:每次epoch变化时,ZXID的第32位从0开始计数)。
  3. 同步阶段(Synchronization): 主要是利用 leader 前一阶段获得的最新提议历史,同步给集群中所有的Follower。只有当超过半数Follower同步成功,这个准Leader才能成为正式的Leader。这之后,follower
    只会接收 zxid 比自己的 lastZxid 大的提议。
  4. 广播阶段(Broadcast): 集群恢复到广播模式,开始接受客户端的写请求。

在发现阶段,或许有人会问:既然Leader被选为主节点,已经是集群里数据最新的了,为什么还要从节点中寻找最新事务呢?这是为了防止某些意外情况。所以这一阶段,Leader集思广益,接收所有Follower发来各自的最新epoch值。

这里有两点要注意:

(1)确保已经被Leader提交的提案最终能够被所有的Follower提交

假设 Leader (server2) 发送 commit 请求(忘了请看上面的消息广播模式),他发送给了 server3,然后要发给 server1 的时候突然挂了。这个时候重新选举的时候我们如果把 server1 作为 Leader 的话,那么肯定会产生数据不一致性,因为 server3 肯定会提交刚刚 server2 发送的 commit 请求的提案,而 server1 根本没收到所以会丢弃。

在这里插入图片描述

那怎么解决呢?

这个时候 server1 已经不可能成为 Leader 了,因为 server1 和 server3 进行投票选举的时候会比较 ZXID ,而此时 server3 的 ZXID 肯定比 server1 的大了(后面讲到选举机制时就明白了)。同理,只能由server3当Leader,server3当上Leader之后,在同步阶段,会将最新提议历史同步给集群中所有的Follower,这就保证数据一致性了。如果server2在某个时刻又重新恢复了,它作为Follower 的身份进入集群中,再向Leader同步当前最新提议和Zxid即可。

(2)确保跳过那些已经被丢弃的提案

在这里插入图片描述

假设 Leader (server2) 此时同意了提案N1,自身提交了这个事务并且要发送给所有 Follower 要 commit 的请求,却在这个时候挂了,此时肯定要重新进行 Leader 的选举,假如此时选 server1 为 Leader (这无所谓,server1和server2都可以当选)。但是过了一会,这个 挂掉的 Leader 又重新恢复了 ,此时它肯定会作为 Follower 的身份进入集群中,需要注意的是刚刚 server2 已经同意提交了提案N1,但其他 server 并没有收到它的 commit 信息,所以其他 server 不可能再提交这个提案N1了,这样就会出现数据不一致性问题了,所以 该提案N1最终需要被抛弃掉 。

在这里插入图片描述

4.6.脑裂问题

脑裂问题:所谓的“脑裂”即“大脑分裂”,也就是本来一个“大脑”被拆分了两个或多个“大脑”。通俗的说,就是比如当你的 cluster 里面有两个节点,它们都知道在这个 cluster 里需要选举出一个 master。那么当它们两之间的通信完全没有问题的时候,就会达成共识,选出其中一个作为 master。但是如果它们之间的通信出了问题,那么两个结点都会觉得现在没有 master,所以每个都把自己选举成 master,于是 cluster 里面就会有两个 master。

ZAB为解决脑裂问题,要求集群内的节点数量为2N+1, 当网络分裂后,始终有一个集群的节点数量过半数,而另一个集群节点数量小于N+1(即小于半数), 因为选主需要过半数节点同意,所以任何情况下集群中都不可能出现大于一个leader的情况。

因此,有了过半机制,对于一个Zookeeper集群,要么没有Leader,要没只有1个Leader,这样就避免了脑裂问题。

五、Zookeeper选举机制

Leader 选举可以分为两个不同的阶段,第一个是我们提到的 Leader 宕机需要重新选举,第二则是当 Zookeeper 启动时需要进行系统的 Leader 初始化选举。下面是zkserver的几种状态:

  1. LOOKING 不确定Leader状态。该状态下的服务器认为当前集群中没有Leader,会发起Leader选举。
  2. FOLLOWING 跟随者状态。表明当前服务器角色是Follower,并且它知道Leader是谁。
  3. LEADING 领导者状态。表明当前服务器角色是Leader,它会维护与Follower间的心跳。
  4. OBSERVING 观察者状态。表明当前服务器角色是Observer,与Folower唯一的不同在于不参与选举,也不参与集群写操作时的投票。

5.1.初始化Leader选举

假设我们集群中有3台机器,那也就意味着我们需要2台同意(超过半数)。这里假设服务器1~3的myid分别为1,2,3,初始化Leader选举过程如下:

  1. 服务器 1 启动,发起一次选举。它会首先 投票给自己 ,投票内容为(myid, ZXID),因为初始化所以 ZXID 都为0,此时
    server1 发出的投票为(1, 0),即myid为1, ZXID为0。此时服务器 1 票数一票,不够半数以上,选举无法完成,服务器1 状态保持为 LOOKING。
  2. 服务器 2 启动,再发起一次选举。服务器2首先也会将投票选给自己(2, 0),并将投票信息广播出去(server1也会,只是它那时没有其他的服务器了),server1 在收到 server2 的投票信息后会将投票信息与自己的作比较。首先它会比较 ZXID ,ZXID 大的优先为 Leader,如果相同则比较 myid,myid 大的优先作为 Leader。所以,此时server1 发现 server2 更适合做 Leader,它就会将自己的投票信息更改为(2, 0)然后再广播出去,之后server2 收到之后发现和自己的一样无需做更改。此时,服务器1票数0票,服务器2票数2票,投票已经超过半数,确定 server2 为 Leader。服务器 1更改状态为 FOLLOWING,服务器 2 更改状态为 LEADING。
  3. 服务器 3 启动,发起一次选举。此时服务器 1,2已经不是 LOOKING 状态,它会直接以 FOLLOWING 的身份加入集群。

5.2 运行时Leader选举

运行时候如果Leader节点崩溃了会走崩溃恢复模式,新Leader选出前会暂停对外服务,大致可以分为四个阶段:选举、发现、同步、广播(见4.5节),此时Leader选举流程如下:

  1. Leader挂掉,剩下的两个 Follower 会将自己的状态 从 Following 变为 Looking 状态,每个Server会发出一个投票,第一次都是投自己,其中投票内容为(myid, ZXID),注意这里的 zxid 可能不是0了
  2. 收集来自各个服务器的投票
  3. 处理投票,处理逻辑:优先比较ZXID,然后比较myid
  4. 统计投票,只要超过半数的机器接收到同样的投票信息,就可以确定leader
  5. 改变服务器状态Looking变为Following或Leading
  6. 然后依次进入发现、同步、广播阶段

举个例子来说明,假设集群有三台服务器,Leader (server2)挂掉了,只剩下server1和server3。 server1 给自己投票为(1,99),然后广播给其他 server,server3 首先也会给自己投票(3,95),然后也广播给其他 server。server1 和 server3 此时会收到彼此的投票信息,和一开始选举一样,他们也会比较自己的投票和收到的投票(zxid 大的优先,如果相同那么就 myid 大的优先)。这个时候 server1 收到了 server3 的投票发现没自己的合适故不变,server3 收到 server1 的投票结果后发现比自己的合适于是更改投票为(1,99)然后广播出去,最后 server1 收到了发现自己的投票已经超过半数就把自己设为 Leader,server3 也随之变为 Follower。

六、Zookeeper数据模型

ZooKeeper 数据模型(Data model)采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二级制序列。并且,每个节点还可以拥有 N 个子节点,最上层是根节点以/来代表。

每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。并且,每个 znode 都一个唯一的路径标识。由于ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为1M。

和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。默认有四种类型的znode:

  1. 持久化目录节点 PERSISTENT: 客户端与zookeeper断开连接后,该节点依旧存在。
  2. 持久化顺序编号目录节点PERSISTENT_SEQUENTIAL: 客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号。
  3. 临时目录节点 EPHEMERAL: 客户端与zookeeper断开连接后,该节点被删除。
  4. 临时顺序编号目录节点EPHEMERAL_SEQUENTIAL: 客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号。

在zookeeper客户端使用get命令可以查看znode的内容和状态信息:

[zk: localhost:2181(CONNECTED) 2] get /zk01
updateed02
cZxid = 0x600000023
ctime = Mon Mar 01 21:20:26 CST 2021
mZxid = 0xb0000000d
mtime = Fri Mar 05 17:15:53 CST 2021
pZxid = 0xb00000018
cversion = 5
dataVersion = 7
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 10
numChildren = 3

下面我们来看一下每个 znode 状态信息究竟代表的是什么吧

znode 状态信息解释
cZxidcreate ZXID,即该数据节点被创建时的事务 id
ctimecreate time,znode 被创建的毫秒数(从1970 年开始)
mZxidmodified ZXID,znode 最后更新的事务 id
mtimemodified time,znode 最后修改的毫秒数(从1970 年开始)
pZxidznode 最后更新子节点列表的事务 id,只有子节点列表变更才会更新 pZxid,子节点内容变更不会更新
cversionznode 子节点变化号,znode 子节点修改次数,子节点每次变化时值增加 1
dataVersionznode 数据变化号,节点创建时为 0,每更新一次节点内容(不管内容有无变化)该版本号的值增加 1
aclVersionznode 访问控制列表(ACL )版本号,表示该节点 ACL 信息变更次数
ephemeralOwner如果是临时节点,这个是 znode 拥有者的 sessionid。如果不是临时节,则 ephemeralOwner=0
dataLengthznode 的数据长度
numChildrenznode 子节点数量

七、Zookeeper监听通知机制

Watcher 监听机制是 Zookeeper 中非常重要的特性, 我们基于 Zookeeper上创建的节点,可以对这些节点绑定监听事件,比如可以监听节点数据变更、节点删除、子节点状态变更等事件,通过这个事件机制,可以基于 Zookeeper 实现分布式锁、集群管理等多种功能,它有点类似于订阅的方式,即客户端向服务端 注册 指定的 watcher ,当服务端符合了 watcher 的某些事件或要求则会 向客户端发送事件通知 ,客户端收到通知后找到自己定义的 Watcher 然后 执行相应的回调方法

当客户端在Zookeeper上某个节点绑定监听事件后,如果该事件被触发,Zookeeper会通过回调函数的方式通知客户端,但是客户端只会收到一次通知。如果后续这个节点再次发生变化,那么之前设置 Watcher 的客户端不会再次收到消息(Watcher是一次性的操作),可以通过循环监听去达到永久监听效果。

ZooKeeper 的 Watcher 机制,总的来说可以分为三个过程:

  1. 客户端注册 Watcher,注册 watcher 有 3 种方式,getData、exists、getChildren。
  2. 服务器处理 Watcher 。
  3. 客户端回调 Watcher 客户端。

监听通知机制的流程如下:

在这里插入图片描述

  1. 首先要有一个main()线程。
  2. 在main线程中创建zkClient,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
  3. 通过connect线程将注册的监听事件发送给Zookeeper。
  4. 在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
  5. Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
  6. listener线程内部调用了process()方法。

八、Zookeeper分布式锁

分布式锁是雅虎研究员设计Zookeeper的初衷。利用Zookeeper的临时顺序节点,可以轻松实现分布式锁。

8.1.获取锁

首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。

在这里插入图片描述
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

在这里插入图片描述
这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。

在这里插入图片描述
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。

于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。

在这里插入图片描述
这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。

在这里插入图片描述
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。

于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。

这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的AQS(AbstractQueuedSynchronizer)。

8.2.释放锁

释放锁分为两种情况:

1.任务完成,客户端显示释放

当任务完成时,Client1会显示调用删除节点Lock1的指令。

在这里插入图片描述
2.任务执行过程中,客户端崩溃

获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。

在这里插入图片描述
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。

在这里插入图片描述
同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。

在这里插入图片描述
最终,Client3成功得到了锁。

在这里插入图片描述

8.3.Zk和Redis分布式锁的比较

下面的表格总结了Zookeeper和Redis分布式锁的优缺点:
在这里插入图片描述
有人说Zookeeper实现的分布式锁支持可重入,Redis实现的分布式锁不支持可重入,这是错误的观点。两者都可以在客户端实现可重入逻辑。

什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁

九、Zookeeper几个应用场景

9.1.数据发布/订阅

当某些数据由几个机器共享,且这些信息经常变化数据量还小的时候,这些数据就适合存储到ZK中。

  • 数据存储:将数据存储到 Zookeeper 上的一个数据节点。
  • 数据获取:应用在启动初始化节点从 Zookeeper 数据节点读取数据,并在该节点上注册一个数据变更 Watcher
  • 数据变更:当变更数据时会更新 Zookeeper
    对应节点数据,Zookeeper会将数据变更通知发到各客户端,客户端接到通知后重新读取变更后的数据即可。

9.2.统一配置管理

本质上,统一配置管理和数据发布/订阅是一样的。

分布式环境下,配置文件的同步可以由Zookeeper来实现。

  1. 将配置文件写入Zookeeper的一个ZNode
  2. 各个客户端服务监听这个ZNode
  3. 一旦ZNode发生改变,Zookeeper将通知各个客户端服务

在这里插入图片描述

9.3.统一集群管理

可能我们会有这样的需求,我们需要了解整个集群中有多少机器在工作,我们想对及群众的每台机器的运行时状态进行数据采集,对集群中机器进行上下线操作等等。

例如,集群机器监控:这通常用于那种对集群中机器状态,机器在线率有较高要求的场景,能够快速对集群中机器变化作出响应。这样的场景中,往往有一个监控系统,实时检测集群机器是否存活。过去的做法通常是:监控系统通过某种手段(比如ping)定时检测每个机器,或者每个机器自己定时向监控系统汇报“我还活着”。 这种做法可行,但是存在两个比较明显的问题:

  1. 集群中机器有变动的时候,牵连修改的东西比较多。
  2. 有一定的延时。

利用ZooKeeper有两个特性,就可以实时另一种集群机器存活性监控系统:

  1. 客户端在某个节点上注册一个Watcher,那么如果该节点的子节点变化了,会通知该客户端。
  2. 创建EPHEMERAL类型的节点,一旦客户端和服务器的会话结束或过期,那么该节点就会消失。

如下图所示,监控系统在/manage节点上注册一个Watcher,如果/manage子节点列表有变动,监控系统就能够实时知道集群中机器的增减情况,至于后续处理就是监控系统的业务了。

在这里插入图片描述

9.4.负载均衡

在这里插入图片描述

多个相同的jar包在不同的服务器上开启相同的服务,可以通过nginx在服务端进行负载均衡的配置。也可以通过ZooKeeper在客户端进行负载均衡配置。

  1. 多个服务注册
  2. 客户端获取中间件地址集合
  3. 从集合中随机选一个服务执行任务

ZooKeeper负载均衡和Nginx负载均衡区别:

  • ZooKeeper不存在单点问题,zab机制保证单点故障可重新选举一个leader只负责服务的注册与发现,不负责转发,减少一次数据交换(消费方与服务方直接通信),需要自己实现相应的负载均衡算法。
  • Nginx存在单点问题,单点负载高数据量大,需要通过 KeepAlived + LVS
    备机实现高可用。每次负载,都充当一次中间人转发角色,增加网络负载量(消费方与服务方间接通信),自带负载均衡算法。

9.5.命名服务

在这里插入图片描述

命名服务是指通过指定的名字来获取资源或者服务的地址,利用 zk 创建一个全局唯一的路径,这个路径就可以作为一个名字,指向集群中某个具体的服务器,提供的服务的地址,或者一个远程的对象等等。

阿里巴巴集团开源的分布式服务框架 Dubbo 中使用 ZooKeeper 来作为其命名服务,维护全局的服务地址列表。在 Dubbo 的实现中:

  • 服务提供者在启动的时候,向 ZooKeeper 上的指定节点/dubbo/${serviceName}/providers
    目录下写入自己的 URL 地址,这个操作就完成了服务的发布。
  • 服务消费者启动的时候,订阅/dubbo/${serviceName} /consumers 目录下写入自己的 URL 地址。

注意:所有向 ZooKeeper 上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。

另外,Dubbo 还有针对服务粒度的监控,方法是订阅/dubbo/${serviceName} 目录下所有提供者和消费者的信息。

另外,分布式锁和选举也是Zookeeper的典型应用场景。

参考资料地址: :https://blog.csdn.net/qq_37555071/article/details/114609145

如有问题,请联系作者修改!

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐