Java工程师面试题-web开发-ZooKeeper

推荐先阅读Java工程师面试题

1、ZooKeeper 是什么?**

ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现, 它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易 用的接口和性能高效、功能稳定的系统提供给用户。客户端的读请求可以被集群中的任意一台机器处理,如果Sidecar 异构开发语言读请求在节点上注册了监听器,这个监听器也是由所 连接的 zookeeper 机器来处理。对于写请求,这些请求会同时发给其他 zookeeper 机器并且达成一致后,请 求才会返回成功。因此,随着 zookeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。有序性是 zookeeper 中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳, 这个时间戳称为 zxid(Zookeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回 结果中会带有这个 zookeeper 最新的 zxid。

2、ZooKeeper 提供了什么?

  1. 文件系统
  2. 通知机制

3、Zookeeper 文件系统

Zookeeper 提供一个多层级的节点命名空间(节点称为 znode)。与文件系统不同的是,这些节点都可以设置 关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。Zookeeper 为了保证高吞吐和低延 迟,在内存中维护了这个树状的目录结构,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存 放数据上限为 1M。

4、四种类型的数据节点 Znode

  • PERSISTENT-持久节点
    除非手动删除,否则节点一直存在于Zookeeper上
  • EPHEMERAL-临时节点
    临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与zookeeper连接断开不一定会话失效),那么这个客户端创建的所有临时节点都会被移除。
  • PERSISTENT_SEQUENTIAL-持久顺序节点
    基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
  • EPHEMERAL_SEQUENTIAL-临时顺序节点
    基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

5、Zookeeper Watcher 机制 – 数据变更通知

Zookeeper允许客户端向服务端的某个Znode注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据Watcher通知状态和事件类型做出业务上的改变。

工作机制:

  • 客户端注册watcher
  • 服务端处理watcher
  • 客户端回调watcher

Watcher特性总结:

  1. 一次性
    无论是服务端还是客户端,一旦一个Watcher被触发,Zookeeper都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。

  2. 客户端串行执行
    客户端Watcher回调的过程是一个串行同步的过程。

  3. 轻量

    • Watcher通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。
  • 客户端向服务端注册Watcher的时候,并不会把客户端真实的Watcher对象实体传递到服务端,仅仅是在客户端请求中使用boolean类型属性进行了标记。
  1. watcher event异步发送watcher的通知事件从server发送到client是异步的,这就存在一个问题,不同的客户端和服务器之间通过socket进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于Zookeeper本身提供了ordering guarantee,即客户端监听事件后,才会感知它所监视znode发生了变化。所以我们使用Zookeeper不能期望能够监控到节点每次的变化。Zookeeper只能保证最终的一致性,而无法保证强一致性。

  2. 注册watcher getData、exists、getChildren

  3. 触发watcher create、delete、setData

  4. 当一个客户端连接到一个新的服务器上时,watch将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到watch的。而当client重新连接时,如果需要的话,所有先前注册过的watch,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,watch可能会丢失:对于一个未创建的znode的exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个watch事件可能会被丢失。

6、客户端注册Watcher实现**

  1. 调用getData()/getChildren()/exist()三个API,传入Watcher对象
  2. 标记请求request,封装Watcher到WatchRegistration
  3. 封装成Packet对象,发服务端发送request
  4. 收到服务端响应后,将Watcher注册到ZKWatcherManager中进行管理
  5. 请求返回,完成注册。

7、服务端处理Watcher实现**

  1. 服务端接收Watcher并存储
    接收到客户端请求,处理请求判断是否需要注册Watcher,需要的话将数据节点的节点路径和ServerCnxn(ServerCnxn代表一个客户端和服务端的连接,实现了Watcher的process接口,此时可以看成一个Watcher对象)存储在WatcherManager的WatchTable和watch2Paths中去。

  2. Watcher触发
    以服务端接收到 setData() 事务请求触发NodeDataChanged事件为例:

    • 封装WatchedEvent
      将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点路径封装成一个WatchedEvent对象
  • 查询Watcher
    从WatchTable中根据节点路径查找Watcher
  • 没找到;说明没有客户端在该数据节点上注册过Watcher
  • 找到;提取并从WatchTable和Watch2Paths中删除对应Watcher(从这里可以看出Watcher在服务端是一次性的,触发一次就失效了
  1. 调用process方法来触发Watcher
    这里process主要就是通过ServerCnxn对应的TCP连接发送Watcher事件通知。

8、客户端回调Watcher**

客户端SendThread线程接收事件通知,交由EventThread线程回调Watcher。客户端的Watcher机制同样是一次性的,一旦被触发后,该Watcher就失效了。

9、服务器角色**

Leader

  • 事务请求的唯一调度和处理者,保证集群事务处理的顺序性
  • 集群内部各服务的调度者

Follower

  • 处理客户端的非事务请求,转发事务请求给Leader服务器
  • 参与事务请求Proposal的投票
  • 参与Leader选举投票

Observer

3.3.0版本以后引入的一个服务器角色,在不影响集群事务处理能力的基础上提升集群的非事务处理能力

  • 处理客户端的非事务请求,转发事务请求给Leader服务器

  • 不参与任何形式的投票

Zookeeper 通知机制**

client 端会对某个 znode 建立一个 watcher 事件,当该 znode 发生变化时,这些 client 会收到 zk 的通知, 然后 client 可以根据 znode 变化来做出业务上的改变等。

Zookeeper 做了什么?**

1、命名服务

2、配置管理

3、集群管理

4、分布式锁

5、队列管理

主从集群和主备集群的区别?**

  • 主从集群:一主多从,主从各司其职,主节点负责资源分配和任务调度,从节点负责具体的执行;
  • 主备集群:一主一备,主要是为了解决单点故障问题,便于备份恢复

zookeeper为什么能保证全局数据一致性?**

(1)首先,zookeeper集群每个节点都可以接收到客户端请求;

(2)其次,客户端请求分为两种请求,一种请求是事务性请求,另一种是非事务性请求,下边就这两种请求展开论述:

       1)事务性请求:当事务性请求是从节点接收到的,就会将请求转发给主节点,然后让主节点按照时间顺序来指定各个节点完             成请求;当事务性请求是主节点接收到的,主节点将会直接自己按照客户端请求顺序来处理请求;

       2)非事务性请求:任何节点收到了都可以自行处理;

Zookeeper有哪几种节点类型**

  • 持久:创建之后一直存在,除非有删除操作,创建节点的客户端会话失效也不影响此节点。
  • 持久顺序:跟持久一样,就是父节点在创建下一级子节点的时候,记录每个子节点创建的先后顺序,会给每个子节点名加上一个数字后缀。
  • 临时:创建客户端会话失效(注意是会话失效,不是连接断了),节点也就没了。不能建子节点。
  • 临时顺序:不用解释了吧。

Zookeeper 分布式锁(文件系统、通知机制)**

有了 zookeeper 的一致性文件系统,锁的问题变得容易。锁服务可以分为两类:

  1. 保持独占
  2. 控制时序

对于第一类,我们将 zookeeper 上的一个 znode 看作是一把锁,通过 createznode 的方式来实现。所有客户 端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的distribute_lock 节点就释放出锁。

对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选master 一样,编号最小的获得锁,用完删除,依次方便。

一个客户端修改了某个节点的数据,其他客户端能够马上获取到这个最新数据吗?**

ZooKeeper 不能确保任何客户端能够获取(即 Read Request)到一样的数据,除非客户端自己要求,方法是客户端在获取数据之前调用 sync。通常情况下(这里所说的通常情况满足:1. 对获取的数据是否是最新版本不敏感,2. 一个客户端修改了数据,其它客户端是否需要立即能够获取最新数据),可以不关心这点。在其它情况下,最清晰的场景是这样:ZK 客户端 A 对 /my_test 的内容从 v1->v2, 但是 ZK 客户端 B 对 /my_test 的内容获取,依然得到的是 v1. 请注意,这个是实际存在的现象,当然延时很短。解决的方法是客户端 B 先调用 sync(), 再调用 getData()。

Zookeeper 数据复制**

Zookeeper 作为一个集群提供一致的数据服务,自然,它要在所有机器间做数据复制。数据复制的好处:

  1. 容错:一个节点出错,不致于让整个系统停止工作,别的节点可以接管它的工作。
  2. 提高系统的扩展能力:把负载分布到多个节点上,或者增加节点来提高系统的负载能力。
  3. 提高性能:让客户端本地访问就近的节点,提高用户访问速度。

客户端读写访问的透明度来看,数据复制集群系统分下面两种:

  1. 写主(WriteMaster) :对数据的修改提交给指定的节点。读无此限制,可以读取任何一个节点。这种情况下 客户端需要对读与写进行区别,俗称读写分离。
  2. **写任意(Write Any)**:对数据的修改可提交给任意的节点,跟读一样。这种情况下,客户端对集群节点的角 色与变化透明。

对 zookeeper 来说,它采用的方式是写任意。通过增加机器,它的读吞吐能力和响应能力扩展性非常好,而 写,随着机器的增多吞吐能力肯定下降(这也是它建立 observer 的原因),而响应能力则取决于具体实现方 式,是延迟复制保持最终一致性,还是立即复制快速响应。

Zooke**eper 工作原理

Zookeeper 的核心是原子广播,这个机制保证了各个 Server 之间的同步。实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是**恢复模式(选主)广播模式(同步)**。当服务启动或者在领导者崩溃 后,Zab 就进入了恢复模式,当领导者被选举出来,且大多数 Server 完成了和 leader 的状态同步以后,恢复 模式就结束了。状态同步保证了 leader 和 Server 具有相同的系统状态。

ZooKeeper 的监听原理是什么?**

在应用程序中,mian()方法首先会创建 zkClient,创建 zkClient 的同时就会产生两个进程,即Listener 进程(监听进程)和connect 进程(网络连接/传输进程),当zkClient 调用getChildren()等方法注册监视器时,connect 进程向 ZooKeeper 注册监听器,注册后的监听器位于ZooKeeper 的监听器列表中,监听器列表中记录了 zkClient 的 IP,端口号以及要监控的路径,一旦目标文件发生变化,ZooKeeper 就会把这条消息发送给对应的zkClient 的Listener()进程,Listener 进程接收到后,就会执行 process()方法,在 process()方法中针对发生的事件进行处理。

zookeeper 是如何保证事务的顺序一致性 的?**

zookeeper 采用了递增的事务 Id 来标识,所有的 proposal(提议)都在被提出的时候加上了 zxid,zxid 实际 上是一个 64位的数字高 32 位是 epoch(时期; 纪元; 世; 新时代)用来标识 leader 是否发生改变,如果有 新的 leader 产生出来,epoch 会自增,低 32 位用来递增计数。当新产生 proposal 的时候,会依据数据库的 两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就 会开始执行。

分布式集群中为什么会有Master?**

在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机器可以共享这个结果,这样可以大大减少重复计算,提高性能,于是就需要进行leader选举。

Zookeeper 下 Server 工作状态**

服务器具有四种状态,分别是LOOKING、FOLLOWING、LEADING、OBSERVING。

  • LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态。
  • FOLLOWING:跟随者状态。表明当前服务器角色是Follower。
  • LEADING:领导者状态。表明当前服务器角色是Leader。
  • OBSERVING:观察者状态。表明当前服务器角色是Observer。

zookeeper 是如何选取主 leader 的?**

当 leader 崩溃或者 leader 失去大多数的 follower,这时 zk 进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的 Server 都恢复到一个正确的状态。Zk 的选举算法有两种:一种是基于 basic paxos 实现 的,另外一种是基于 fast paxos 算法实现的。系统默认的选举算法为 fast paxos

1、Zookeeper 选主流程(basic paxos)

  1. 选举线程由当前 Server 发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server;

  2. 选举线程首先向所有 Server 发起一次询问(包括自己);

  3. 选举线程收到回复后,验证是否是自己发起的询问(验证 zxid 是否一致),然后获取对方的 id(myid),并存 储到当前询问对象列表中,最后获取对方提议的 leader 相关信息(id,zxid),并将这些信息存储到当次选举的投 票记录表中;

  4. 收到所有 Server 回复以后,就计算出 zxid 最大的那个 Server,并将这个 Server 相关信息设置成下一次 要投票的 Server;

  5. 线程将当前 zxid 最大的 Server 设置为当前 Server 要推荐的 Leader,如果此时获胜的 Server 获得 n/2 + 1 的 Server 票数,设置当前推荐的 leader 为获胜的 Server,将根据获胜的 Server 相关信息设置自己的状 态,否则,继续这个过程,直到 leader 被选举出来。通过流程分析我们可以得出:要使 Leader 获得多数Server 的支持,则 Server 总数必须是奇数 2n+1,且存活的 Server 的数目不得少于 n+1. 每个 Server 启动后 都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的 server 还会从磁盘快照中恢复数据和会话信息,zk 会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。

2、Zookeeper 选主流程(basic paxos)

fast paxos 流程是在选举过程中,某 Server 首先向所有 Server 提议自己要成为 leader,当其它 Server 收到提 议以后,解决 epoch 和 zxid 的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出 Leader。

Zookeeper 同步流程**

选完 Leader 以后,zk 就进入状态同步过程。

  1. Leader 等待 server 连接;
  2. Follower 连接 leader,将最大的 zxid 发送给 leader;
  3. Leader 根据 follower 的 zxid 确定同步点;
  4. 完成同步后通知 follower 已经成为 uptodate 状态;
  5. Follower 收到 uptodate 消息后,又可以重新接受 client 的请求进行服务了。

分布式通知和协调**

对于系统调度来说:操作人员发送通知实际是通过控制台改变某个节点的状态,然后 zk 将这些变化发送给注册 了这个节点的 watcher 的所有客户端。对于执行情况汇报:每个工作进程都在某个目录下创建一个临时节点。并携带工作的进度数据,这样汇总的进 程可以监控目录子节点的变化获得工作进度的实时的全局情况。

机器中为什么会有 leader?**

在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机器可以共享这个结果,这样可 以大大减少重复计算,提高性能,于是就需要进行 leader 选举。

zk 节点宕机如何处理?**

Zookeeper 本身也是集群,推荐配置不少于 3 个服务器。Zookeeper 自身也要保证当一个节点宕机时,其他 节点会继续提供服务。

如果是一个 Follower 宕机,还有 2 台服务器提供访问,因为 Zookeeper 上的数据是有多个副本的,数据并不 会丢失;

如果是一个 Leader 宕机,Zookeeper 会选举出新的 Leader。

ZK 集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在 ZK 节点挂得太多,只剩一半或不 到一半节点能工作,集群才失效。
所以:

  • 3 个节点的 cluster 可以挂掉 1 个节点(leader 可以得到 2 票>1.5)
  • 2 个节点的 cluster 就不能挂掉任何 1 个节点了(leader 可以得到 1 票<=1)

zookeeper 负载均衡和 nginx 负载均衡 区别**

zk 的负载均衡是可以调控,nginx 只是能调权重,其他需要可控的都需要自己写插件;但是 nginx 的吞吐量比zk 大很多,应该说按业务选择用哪种方式。

Zookeeper有哪几种几种部署模式?**

部署模式:单机模式、伪集群模式、集群模式。

集群最少要几台机器,集群规则是怎样的?**

集群规则为2N+1台,N>0,即3台。

31、集群如果有3台机器,挂掉一台集群还能工作吗?****挂掉两台呢?

记住一个原则:过半存活即可用。

集群支持动态添加机器吗?**

其实就是水平扩容了,Zookeeper在这方面不太好。两种方式:

  • 全部重启:关闭所有Zookeeper服务,修改配置之后启动。不影响之前客户端的会话。
  • 逐个重启:在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。

3.5版本开始支持动态扩容。

Zookeeper对节点的watch监听通知是永久的吗?**为什么不是永久的?

不是。官方声明:一个Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端,以便通知它们。

为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。
一般是客户端执行getData(“/节点A”,true),如果节点A发生了变更或删除,客户端会得到它的watch事件,但是在之后节点A又发生了变更,而客户端又没有设置watch事件,就不再给客户端发送。

在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。

ZAB协议是什么?**

ZAB协议是一种专门为zookeeper设计的一种支持崩溃回复的原子广播协议,是一种通用的分布式一致性算法,基于该协议,zookeeper实现了一种主备模式的系统架构来保持集群中各副本之间数据的一致性。具体来说,zookeeper使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更为以事务的形式广播到所有的副本进程上去,该协议的这个主备模式架构保证了同一时刻集群中只能够有一个主进程来广播服务器的状态变更。

简言之,该协议核心就是定义了对于那些会改变zookeeper服务器数据状态的事务请求的处理方式,所有事务请求都必须由一个全局唯一的服务器来协调处理,即leader服务器。

Zookeeper的典型应用场景**

Zookeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架,开发人员可以使用它来进行分布式数据的发布和订阅。

通过对Zookeeper中丰富的数据节点进行交叉使用,配合Watcher事件通知机制,可以非常方便的构建一系列分布式应用中年都会涉及的核心功能,如:

  • 数据发布/订阅
  • 负载均衡
  • 命名服务
  • 分布式协调/通知
  • 集群管理
  • Master选举
  • 分布式锁
  • 分布式队列

1. 数据发布/订阅

介绍

数据发布/订阅系统,即所谓的配置中心,顾名思义就是发布者发布数据供订阅者进行数据订阅。

目的

  • 动态获取数据(配置信息)
  • 实现数据(配置信息)的集中式管理和数据的动态更新

设计模式

  • Push 模式
  • Pull 模式

数据(配置信息)特性:

  • 数据量通常比较小
  • 数据内容在运行时会发生动态更新
  • 集群中各机器共享,配置一致

如:机器列表信息、运行时开关配置、数据库配置信息等

基于Zookeeper的实现方式

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

2. 负载均衡

zk的命名服务

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

分布式通知和协调

对于系统调度来说:操作人员发送通知实际是通过控制台改变某个节点的状态,然后zk将这些变化发送给注册了这个节点的watcher的所有客户端。
对于执行情况汇报:每个工作进程都在某个目录下创建一个临时节点。并携带工作的进度数据,这样汇总的进程可以监控目录子节点的变化获得工作进度的实时的全局情况。

zk的命名服****务(文件系统)

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

zk的配置管理(文件系统、通知机制)

程序分布式的部署在不同的机器上,将程序的配置信息放在zk的znode下,当有配置发生改变时,也就是znode发生变化时,可以通过改变zk中某个目录节点的内容,利用watcher通知给各个客户端,从而更改配置。

Zookeeper集群管理(文件系统、通知机制)

所谓集群管理无在乎两点:是否有机器退出和加入、选举master。
对于第一点,所有机器约定在父目录下创建临时目录节点,然后监听父目录节点的子节点变化消息。一旦有机器挂掉,该机器与 zookeeper的连接断开,其所创建的临时目录节点被删除,所有其他机器都收到通知:某个兄弟目录被删除,于是,所有人都知道:它上船了。
新机器加入也是类似,所有机器收到通知:新兄弟目录加入,highcount又有了,对于第二点,我们稍微改变一下,所有机器创建临时顺序编号目录节点,每次选取编号最小的机器作为master就好。

Zookeeper分布式锁(文件系统、通知机制)

有了zookeeper的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是保持独占,另一个是控制时序。

对于第一类,我们将zookeeper上的一个znode看作是一把锁,通过createznode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的distribute_lock 节点就释放出锁。

对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选master一样,编号最小的获得锁,用完删除,依次方便。

Zookeeper队列管理(文件系统、通知机制)

两种类型的队列:

  • 同步队列,当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。
  • 队列按照 FIFO 方式进行入队和出队操作。

第一类,在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目。

第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。在特定的目录下创建PERSISTENT_SEQUENTIAL节点,创建成功时Watcher通知等待的队列,队列删除序列号最小的节点用以消费。此场景下Zookeeper的znode用于消息存储,znode存储的数据就是消息队列中的消息内容,SEQUENTIAL序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以不必担心队列消息的丢失问题。

Zookeeper获取分布式锁流程

见下题。

**
**

36、Zookeeper是如何实现分布式锁的?

基于Zookeeper的分布式锁都是依赖于zk节点路径唯一的机制来实现的.

什么意思呢?

就是在zk中,在分布式锁的场景下 对于同一个路径,只能有一个客户端能创建成功,其它的都创建失败.(这个不难理解,在平时系统中也没见过有哪2个文件地址完全相同)

下面就说一下zk分布式锁2种实现,没错 本篇就是干的不能再干的干货!!!

第一种分布式锁

具体流程

第一种实现是利用的zk的临时节点, 在争抢锁的时候,所有的客户端都尝试创建一个临时节点(代表锁住的资源),只有一个客户端会创建成功,创建成功的客户端得到锁,其它的客户端则监听(利用zk的watch)该节点的状态改变并且进入阻塞,节点改变后 zk server 会通知剩下的客户端,剩下的客户端停止阻塞并且重新争抢锁.

zk中有持久节点和临时节点,为什么使用临时节点呢?

如果使用的是持久节点,则这个节点在客户端下线后,依旧会一直存在,不会自动删除,导致 其它客户端一直无法争抢到锁 .如果使用的是临时节点的话, 在客户端下线后zk会删除与其相关的临时节点,这样其它客户端就能重新争抢锁 .

代码实现

@Override
public void lock() {
// 如果获取不到锁,阻塞等待
if (!tryLock()) {
// 没获得锁,阻塞自己
waitForLock();
// 再次尝试
lock();
}
}

@Override
public boolean tryLock() { // 不会阻塞
// 创建节点
try {
// 创建临时节点,zk中的节点(路径)唯一,只有一个会创建成功
// 为什么使用临时节点: 客户端掉线后会自动删除节点(释放锁)
client.createEphemeral(lockPath);
} catch (ZkNodeExistsException e) {
return false;
}
return true;
}

/**
*争抢不到锁的话,等待锁的释放
*/
private void waitForLock() {
CountDownLatch cdl = new CountDownLatch(1);
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataDeleted(String dataPath) throws Exception {
System.out.println("收到节点被删除的消息,停止等待,重新争夺锁");
cdl.countDown();
}
@Override
public void handleDataChange(String dataPath, Object data)
throws Exception {
}
};

// 监听
client.subscribeDataChanges(lockPath, listener);
// 判断锁节点是否存在,存在的话表明有别人
if (this.client.exists(lockPath)) {
try {
// 等待接收到消息后,继续往下执行
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 取消监听消息
client.unsubscribeDataChanges(lockPath, listener);
}

总结一下

实现简单,但是会有 羊群效应 ,节点的删除都会通知所有的客户端,并且所有的客户端会 取消监听 + 重新一起争夺锁 + 争夺失败 + 再次开启监听 ,如此循环,资源耗费多,并且这种耗费是可以避免的,那么如何避免呢?就是下面第二种的 改进版分布式锁 .

第二种分布式锁

这一种分布式锁的实现是利用zk的临时顺序节点,每一个客户端在争夺锁的时候都由zk分配一个顺序号(sequence),客户端则按照这个顺序去获取锁.

具体流程

lock跟前面的一样,不过lockPath(锁住的资源)是一个持久节点,客户端在该持久节点下面创建临时顺序节点,获取到顺序号后,根据自己是否是最小的顺序号来获取锁,顺序号最小则获取锁,序号不为最小则监听(watch)前一个顺序号,当前一个顺序号被删除的时候表明锁被释放了,则会通知下一个客户端.

代码实现

下面贴出跟第一种实现不同的代码

/**
* 尝试加锁
*
* @return
*/
@Override
public boolean tryLock() {
// 创建临时顺序节点
if (this.currentPath == null) {
// 在lockPath节点下面创建临时顺序节点
currentPath = this.client.createEphemeralSequential(LockPath + "/", "aaa");
}
// 获得所有的子节点
List<String> children = this.client.getChildren(LockPath);

// 排序list
Collections.sort(children);

// 判断当前节点是否是最小的,如果是最小的节点,则表明此这个client可以获取锁
if (currentPath.equals(LockPath + "/" + children.get(0))) {
return true;
} else {
// 如果不是当前最小的sequence,取到前一个临时节点
// 1.单独获取临时节点的顺序号
// 2.查找这个顺序号在children中的下标
// 3.存储前一个节点的完整路径
int curIndex = children.indexOf(currentPath.substring(LockPath.length() + 1));
beforePath = LockPath + "/" + children.get(curIndex - 1);
}
return false;
}

private void waitForLock() {
CountDownLatch cdl = new CountDownLatch(1);
// 注册watcher
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataDeleted(String dataPath) throws Exception {
System.out.println("监听到前一个节点被删除了");
cdl.countDown();
}
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
};

// 监听前一个临时节点
client.subscribeDataChanges(this.beforePath, listener);

// 前一个节点还存在,则阻塞自己
if (this.client.exists(this.beforePath)) {
try {
// 直至前一个节点释放锁,才会继续往下执行
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 醒来后,表明前一个临时节点已经被删除,此时客户端可以获取锁 && 取消watcher监听
client.unsubscribeDataChanges(this.beforePath, listener);
}

总结一下

实现比第一种复杂一点,但是更加的合理,少做了很多不必要的操作,只唤醒了后面一个客户端.

总结

由zk自身的设计,zk不适合高并发写,需要在使用zk分布式锁前先做一定过滤操作,先过滤掉部分请求,再进行锁争夺.

分布式锁当然不止zk的实现,各个实现都有其适用的场景,在分布式系统中,没有最完美的方案,只有最合适的方案,往往都是取舍问题.

参考资料:https://www.cnblogs.com/lanqiu5ge/p/9405601.html