理解网络营销型网站的建设,怎么修改网站默认首页,什么是网络设计图,宁波网站制作哪家强一、ZooKeeper 基础题
1.1、Zookeeper 的典型应用场景
Zookeeper 是一个典型的发布/订阅模式的分布式数据管理与协调框架#xff0c;开发人员可以使用它来进行分布式数据的发布和订阅。
通过对 Zookeeper 中丰富的数据节点进行交叉使用#xff0c;配合 Watcher 事件通知机…一、ZooKeeper 基础题
1.1、Zookeeper 的典型应用场景
Zookeeper 是一个典型的发布/订阅模式的分布式数据管理与协调框架开发人员可以使用它来进行分布式数据的发布和订阅。
通过对 Zookeeper 中丰富的数据节点进行交叉使用配合 Watcher 事件通知机制可以非常方便的构建一系列分布式应用涉及到的核心功能如 数据发布/订阅 负载均衡 命名服务 分布式协调/通知 集群管理 Master 选举 分布式锁 分布式队列
数据发布/订阅
介绍
数据发布/订阅系统即所谓的配置中心顾名思义就是发布者发布数据供订阅者进行数据订阅。
目的
动态获取数据配置信息
实现数据配置信息的集中式管理和数据的动态更新
设计模式
Push 模式
Pull 模式
数据配置信息特性
1数据量通常比较小
2数据内容在运行时会发生动态更新
3集群中各机器共享配置一致
如机器列表信息、运行时开关配置、数据库配置信息等
基于 Zookeeper 的实现方式 数据存储将数据配置信息存储到 Zookeeper 上的一个数据节点 数据获取应用在启动初始化节点从 Zookeeper 数据节点读取数据并在该节点上注册一个数据变更 Watcher 数据变更当变更数据时更新 Zookeeper 对应节点数据Zookeeper会将数据变更通知发到各客户端客户端接到通知后重新读取变更后的数据即可。
负载均衡
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 队列管理文件系统、通知机制
两种类型的队列
1同步队列当一个队列的成员都聚齐时这个队列才可用否则一直等待所有成员到达。
2队列按照 FIFO 方式进行入队和出队操作。
第一类在约定目录下创建临时目录节点监听节点数目是否是我们要求的数目。
第二类和分布式锁服务中的控制时序场景基本原理一致入列有编号出列按编号。在特定的目录下创建 PERSISTENT_SEQUENTIAL 节点创建成功时Watcher 通知等待的队列队列删除序列号最小的节点用以消费。此场景下Zookeeper 的 znode 用于消息存储znode 存储的数据就是消息队列中的消息内容SEQUENTIAL 序列号就是消息的编号按序取出即可。由于创建的节点是持久化的所以不必担心队列消息的丢失问题。
1.2、什么是 ZooKeeper
ZooKeeper 是一个开放源码的分布式协调服务它是集群的管理者监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终将简单易用的接口和性能高效、功能稳定的系统提供给用户。
分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
Zookeeper 保证了如下分布式一致性特性 顺序一致性 原子性 单一视图 可靠性 实时性最终一致性
客户端的读请求可以被集群中的任意一台机器处理如果读请求在节点上注册了监听器这个监听器也是由所连接的 zookeeper 机器来处理。对于写请求这些请求会同时发给其他 zookeeper 机器并且达成一致后请求才会返回成功。因此随着 zookeeper 的集群机器增多读请求的吞吐会提高但是写请求的吞吐会下降。
有序性是 zookeeper 中非常重要的一个特性所有的更新都是全局有序的每个更新都有一个唯一的时间戳这个时间戳称为 zxidZookeeper Transaction Id。而读请求只会相对于更新有序也就是读请求的返回结果中会带有这个zookeeper 最新的 zxid。
1.3、ZooKeeper 提供了什么 文件系统 通知机制
1.4、Zookeeper 文件系统
Zookeeper 提供一个多层级的节点命名空间节点称为 znode。与文件系统不同的是这些节点都可以设置关联的数据而文件系统中只有文件节点可以存放数据而目录节点不行。
Zookeeper 为了保证高吞吐和低延迟在内存中维护了这个树状的目录结构这种特性使得 Zookeeper 不能用于存放大量的数据每个节点的存放数据上限为1M。
1.5、ZAB 协议
ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。
ZAB 协议包括两种基本的模式崩溃恢复和消息广播。
当整个 zookeeper 集群刚刚启动或者 Leader 服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时所有进程服务器进入崩溃恢复模式首先选举产生新的 Leader 服务器然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步当集群中超过半数机器与该 Leader服务器完成数据同步之后退出恢复模式进入消息广播模式Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。
1.6、四种类型的数据节点 Znode
PERSISTENT-持久节点
除非手动删除否则节点一直存在于 Zookeeper 上
EPHEMERAL-临时节点
临时节点的生命周期与客户端会话绑定一旦客户端会话失效客户端与zookeeper 连接断开不一定会话失效那么这个客户端创建的所有临时节点都会被移除。
PERSISTENT_SEQUENTIAL-持久顺序节点
基本特性同持久节点只是增加了顺序属性节点名后边会追加一个由父节点维护的自增整型数字。
EPHEMERAL_SEQUENTIAL-临时顺序节点
基本特性同临时节点增加了顺序属性节点名后边会追加一个由父节点维护的自增整型数字。
1.7、Zookeeper Watcher 机制 – 数据变更通知
Zookeeper 允许客户端向服务端的某个 Znode 注册一个 Watcher 监听当服务端的一些指定事件触发了这个 Watcher服务端会向指定客户端发送一个事件通知来实现分布式的通知功能然后客户端根据 Watcher 通知状态和事件类型做出业务上的改变。
工作机制 客户端注册 watcher 服务端处理 watcher 客户端回调 watcher
Watcher 特性总结 一次性 无论是服务端还是客户端一旦一个 Watcher 被 触 发 Zookeeper 都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力不然对于更新非常频繁的节点服务端会不断的向客户端发送事件通知无论对于网络还是服务端的压力都非常大。 客户端串行执行 客户端 Watcher 回调的过程是一个串行同步的过程。 轻量 Watcher 通知非常简单只会告诉客户端发生了事件而不会说明事件的具体内容。 客户端向服务端注册 Watcher 的时候并不会把客户端真实的 Watcher 对象实体传递到服务端仅仅是在客户端请求中使用 boolean 类型属性进行了标记。 watcher event 异步发送 watcher 的通知事件从 server 发送到 client 是异步的这就存在一个问题不同的客户端和服务器之间通过 socket 进行通信由于网络延迟或其他因素导致客户端在不同的时刻监听到事件由于 Zookeeper 本身提供了 ordering guarantee即客户端监听事件后才会感知它所监视 znode发生了变化。所以我们使用 Zookeeper 不能期望能够监控到节点每次的变化。Zookeeper 只能保证最终的一致性而无法保证强一致性。 注册 watcher getData、exists、getChildren 触发 watcher create、delete、setData 当一个客户端连接到一个新的服务器上时watch 将会被以任意会话事件触发。当与一个服务器失去连接的时候是无法接收到 watch 的。而当 client 重新连接时如果需要的话所有先前注册过的 watch都会被重新注册。通常这是完全透明的。只有在一个特殊情况下watch 可能会丢失对于一个未创建的 znode的 exist watch如果在客户端断开连接期间被创建了并且随后在客户端连接上之前又删除了这种情况下这个 watch 事件可能会被丢失。
1.8、客户端注册 Watcher 实现 调用 getData()/getChildren()/exist()三个 API传入 Watcher 对象 标记请求 request封装 Watcher 到 WatchRegistration 封装成 Packet 对象发服务端发送 request 收到服务端响应后将 Watcher 注册到 ZKWatcherManager 中进行管理 请求返回完成注册。
1.9、服务端处理 Watcher 实现
服务端接收 Watcher 并存储
接收到客户端请求处理请求判断是否需要注册 Watcher需要的话将数据节点的节点路径和 ServerCnxnServerCnxn 代表一个客户端和服务端的连接实现了 Watcher 的 process 接口此时可以看成一个 Watcher 对象存储在WatcherManager 的 WatchTable 和 watch2Paths 中去。
Watcher 触发 以服务端接收到 setData() 事务请求触发 NodeDataChanged 事件为例 封装 WatchedEvent 将通知状态SyncConnected、事件类型NodeDataChanged以及节点路径封装成一个 WatchedEvent 对象 查询 Watcher 从 WatchTable 中根据节点路径查找 Watcher 没找到说明没有客户端在该数据节点上注册过 Watcher 找到提取并从 WatchTable 和 Watch2Paths 中删除对应 Watcher从这里可以看出 Watcher 在服务端是一次性的触发一次就失效了
调用 process 方法来触发 Watcher
这里 process 主要就是通过 ServerCnxn 对应的 TCP 连接发送 Watcher 事件通知。
1.10、客户端回调 Watcher
客户端 SendThread 线程接收事件通知交由 EventThread 线程回调 Watcher。
客户端的 Watcher 机制同样是一次性的一旦被触发后该 Watcher 就失效了。
1.11、ACL 权限控制机制
UGOUser/Group/Others
目前在 Linux/Unix 文件系统中使用也是使用最广泛的权限控制方式。是一种粗粒度的文件系统权限控制模式。
ACLAccess Control List访问控制列表
包括三个方面
权限模式Scheme IP从 IP 地址粒度进行权限控制 Digest最常用用类似于 username:password 的权限标识来进行权限配置便于区分不同应用来进行权限控制 World最开放的权限控制方式是一种特殊的 digest 模式只有一个权限标识“world:anyone” Super超级用户
授权对象
授权对象指的是权限赋予的用户或一个指定实体例如 IP 地址或是机器灯。
权限 Permission CREATE数据节点创建权限允许授权对象在该 Znode 下创建子节点 DELETE子节点删除权限允许授权对象删除该数据节点的子节点 READ数据节点的读取权限允许授权对象访问该数据节点并读取其数据内容或子节点列表等 WRITE数据节点更新权限允许授权对象对该数据节点进行更新操作 ADMIN数据节点管理权限允许授权对象对该数据节点进行 ACL 相关设置操作
1.12、 Chroot 特性
3.2.0 版本后添加了 Chroot 特性该特性允许每个客户端为自己设置一个命名空间。如果一个客户端设置了 Chroot那么该客户端对服务器的任何操作都将会被限制在其自己的命名空间下。
通过设置 Chroot能够将一个客户端应用于 Zookeeper 服务端的一颗子树相对应在那些多个应用公用一个 Zookeeper 集群的场景下对实现不同应用间的相互隔离非常有帮助。
1.13、会话管理
分桶策略将类似的会话放在同一区块中进行管理以便于 Zookeeper 对会话进行不同区块的隔离处理以及同一区块的统一处理。
分配原则每个会话的“下次超时时间点”ExpirationTime
计算公式
ExpirationTime_ currentTime sessionTimeout
ExpirationTime (ExpirationTime_ / ExpirationInrerval 1) *
ExpirationInterval , ExpirationInterval 是指 Zookeeper 会话超时检查时间间隔默认 tickTime
1.14、Zookeeper 对节点的 watch 监听通知是永久的吗为什么不是永久的?
不是。官方声明一个 Watch 事件是一个一次性的触发器当被设置了 Watch的数据发生了改变的时候则服务器将这个改变发送给设置了 Watch 的客户端以便通知它们。
为什么不是永久的举个例子如果服务端变动频繁而监听的客户端很多情况下每次变动都要通知到所有的客户端给网络和服务器造成很大压力。
一般是客户端执行 getData(“/节点 A”,true)如果节点 A 发生了变更或删除客户端会得到它的 watch 事件但是在之后节点 A 又发生了变更而客户端又没有设置 watch 事件就不再给客户端发送。
在实际应用中很多情况下我们的客户端不需要知道服务端的每一次变动我只要最新的数据即可。
1.15、Zookeeper 的 java 客户端都有哪些
java 客户端zk 自带的 zkclient 及 Apache 开源的 Curator。
1.16、chubby 是什么和 zookeeper 比你怎么看
chubby 是 google 的完全实现 paxos 算法不开源。zookeeper 是 chubby的开源实现使用 zab 协议paxos 算法的变种。
1.17、说几个 zookeeper 常用的命令。
常用命令ls get set create delete 等。
二、Zookeeper 提升
2.1、服务器角色
Leader 事务请求的唯一调度和处理者保证集群事务处理的顺序性 集群内部各服务的调度者
Follower 处理客户端的非事务请求转发事务请求给 Leader 服务器 参与事务请求 Proposal 的投票 参与 Leader 选举投票
Observer 3.0 版本以后引入的一个服务器角色在不影响集群事务处理能力的基础上提升集群的非事务处理能力 处理客户端的非事务请求转发事务请求给 Leader 服务器 不参与任何形式的投票
2.2、Zookeeper 下 Server 工作状态
服务器具有四种状态分别是 LOOKING、FOLLOWING、LEADING、OBSERVING。 LOOKING寻 找 Leader 状态。当服务器处于该状态时它会认为当前集群中没有 Leader因此需要进入 Leader 选举状态。 FOLLOWING跟随者状态。表明当前服务器角色是 Follower。 LEADING领导者状态。表明当前服务器角色是 Leader。 OBSERVING观察者状态。表明当前服务器角色是 Observer。
2.3、数据同步
整个集群完成 Leader 选举之后LearnerFollower 和 Observer 的统称会向Leader 服务器进行注册。当 Learner 服务器想像Leader 服务器完成注册后进入数据同步环节。
数据同步流程均以消息传递的方式进行 Learner 向 Learder 注册 数据同步 同步确认
Zookeeper 的数据同步通常分为四类 直接差异化同步DIFF 同步 先回滚再差异化同步TRUNCDIFF 同步 仅回滚同步TRUNC 同步 全量同步SNAP 同步
在进行数据同步前Leader 服务器会完成数据同步初始化
peerLastZxid
· 从 learner 服务器注册时发送的 ACKEPOCH 消息中提取 lastZxid该Learner 服务器最后处理的 ZXID
minCommittedLog
· Leader 服务器 Proposal 缓存队列 committedLog 中最小 ZXIDmaxCommittedLog
· Leader 服务器 Proposal 缓存队列 committedLog 中最大 ZXID直接差异化同步DIFF 同步
· 场景peerLastZxid 介于 minCommittedLog 和 maxCommittedLog之间先回滚再差异化同步TRUNCDIFF 同步
· 场景当新的 Leader 服务器发现某个 Learner 服务器包含了一条自己没有的事务记录那么就需要让该 Learner 服务器进行事务回滚–回滚到 Leader服务器上存在的同时也是最接近于 peerLastZxid 的 ZXID仅回滚同步TRUNC 同步
· 场景peerLastZxid 大于 maxCommittedLog
全量同步SNAP 同步
· 场景一peerLastZxid 小于 minCommittedLog
· 场景二Leader 服务器上没有 Proposal 缓存队列且 peerLastZxid 不等于 lastProcessZxid
2.4、zookeeper 是如何保证事务的顺序一致性的
zookeeper 采用了全局递增的事务 Id 来标识所有的 proposal提议都在被提出的时候加上了 zxidzxid 实际上是一个 64 位的数字高 32 位是 epoch 时期; 纪元; 世; 新时代用来标识 leader 周期如果有新的 leader 产生出来epoch会自增低 32 位用来递增计数。当新产生 proposal 的时候会依据数据库的两阶段过程首先会向其他的 server 发出事务执行请求如果超过半数的机器都能执行并且能够成功那么就会开始执行。
2.5、分布式集群中为什么会有 Master
在分布式环境中有些业务逻辑只需要集群中的某一台机器进行执行其他的机器可以共享这个结果这样可以大大减少重复计算提高性能于是就需要进行leader 选举。
2.6、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)
2.7、zookeeper 负载均衡和 nginx 负载均衡区别
zk 的负载均衡是可以调控nginx 只是能调权重其他需要可控的都需要自己写插件但是 nginx 的吞吐量比 zk 大很多应该说按业务选择用哪种方式。
2.8、Zookeeper 有哪几种几种部署模式
部署模式单机模式、伪集群模式、集群模式。
2.9、集群最少要几台机器集群规则是怎样的?
集群规则为 2N1 台N0即 3 台。
2.10、集群支持动态添加机器吗
其实就是水平扩容了Zookeeper 在这方面不太好。两种方式
全部重启关闭所有 Zookeeper 服务修改配置之后启动。不影响之前客户端的会话。
逐个重启在过半存活即可用的原则下一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。
3.5 版本开始支持动态扩容。
2.11、ZAB 和 Paxos 算法的联系与区别
相同点
1两者都存在一个类似于 Leader 进程的角色由其负责协调多个 Follower 进程的运行
2Leader 进程都会等待超过半数的 Follower 做出正确的反馈后才会将一个提案进行提交
3ZAB 协议中每个 Proposal 中都包含一个 epoch 值来代表当前的 Leader周期Paxos 中名字为 Ballot
不同点
ZAB 用来构建高可用的分布式数据主备系统ZookeeperPaxos 是用来构建分布式一致性状态机系统
三、分布式锁原理与实战
3.1、公平锁和可重入锁的原理
最经典的分布式锁是可重入的公平锁。什么是可重入的公平锁呢直接讲解的概念和原理会比较抽象难懂还是从具体的实例入手吧这里用一个简单的故事来类比估计就简单多了。
故事发生在一个没有自来水的古代在一个村子有一口井水质非常的好村民们都抢着取井里的水。井就那么一口村里的人很多村民为争抢取水打架斗殴甚至头破血流。
问题总是要解决于是村长绞尽脑汁最终想出了一个凭号取水的方案。井边安排一个看井人维护取水的秩序。取水秩序很简单
1取水之前先取号
2号排在前面的就可以先取水
3先到的排在前面那些后到的一个一个挨着在井边排成一队。
取水示意图如图所示。
这种排队取水模型就是一种锁的模型。排在最前面的号拥有取水权就是一种典型的独占锁。另外先到先得号排在前面的人先取到水取水之后就轮到下一个号取水挺公平的说明它是一种公平锁。
什么是可重入锁呢 假定取水时以家庭为单位家庭的某人拿到号其他的家庭成员过来打水这时候不用再取号如图所示。
图中排在1号的家庭老公取号假设其老婆来了直接排第一个正所谓妻凭夫贵。再看上图的2号父亲正在打水假设其儿子和女儿也到井边了直接排第二个所谓子凭父贵。总之如果取水时以家庭为单位则同一个家庭可以直接复用排号不用从后面排起重新取号。
以上这个故事模型中取号一次可以用来多次取水其原理为可重入锁的模型。在重入锁模型中一把独占锁可以被多次锁定这就叫做可重入锁。
3.2、ZooKeeper分布式锁的原理
理解了经典的公平可重入锁的原理后再来看在分布式场景下的公平可重入锁的原理。通过前面的分析基本可以判定ZooKeeper 的临时顺序节点天生就有一副实现分布式锁的胚子。为什么呢
ZooKeeper的每一个节点都是一个天然的顺序发号器。
在每一个节点下面创建临时顺序节点EPHEMERAL_SEQUENTIAL类型新的子节点后面会加上一个次序编号而这个生成的次序编号是上一个生成的次序编号加一。
例如有一个用于发号的节点“/test/lock”为父亲节点可以在这个父节点下面创建相同前缀的临时顺序子节点假定相同的前缀为“/test/lock/seq-”。第一个创建的子节点基本上应该为/test/lock/seq-0000000000下一个节点则为/test/lock/seq-0000000001依次类推如果所示。 ZooKeeper节点的递增有序性可以确保锁的公平
一个ZooKeeper分布式锁首先需要创建一个父节点尽量是持久节点PERSISTENT类型然后每个要获得锁的线程都在这个节点下创建个临时顺序节点。由于ZK节点是按照创建的次序依次递增的。
为了确保公平可以简单的规定编号最小的那个节点表示获得了锁。所以每个线程在尝试占用锁之前首先判断自己是排号是不是当前最小如果是则获取锁。
ZooKeeper的节点监听机制可以保障占有锁的传递有序而且高效
每个线程抢占锁之前先尝试创建自己的ZNode。同样释放锁的时候就需要删除创建的Znode。创建成功后如果不是排号最小的节点就处于等待通知的状态。等谁的通知呢不需要其他人只需要等前一个Znode的通知就可以了。前一个Znode删除的时候会触发Znode事件当前节点能监听到删除事件就是轮到了自己占有锁的时候。第一个通知第二个、第二个通知第三个击鼓传花似的依次向后。
ZooKeeper的节点监听机制能够非常完美地实现这种击鼓传花似的信息传递。具体的方法是每一个等通知的Znode节点只需要监听linsten或者监视watch排号在自己前面那个而且紧挨在自己前面的那个节点就能收到其删除事件了。只要上一个节点被删除了就进行再一次判断看看自己是不是序号最小的那个节点如果是自己就获得锁。
另外ZooKeeper的内部优越的机制能保证由于网络异常或者其他原因集群中占用锁的客户端失联时锁能够被有效释放。一旦占用Znode锁的客户端与ZooKeeper集群服务器失去联系这个临时Znode也将自动删除。排在它后面的那个节点也能收到删除事件从而获得锁。正是由于这个原因在创建取号节点的时候尽量创建临时znode节点
ZooKeeper的节点监听机制能避免羊群效应
ZooKeeper这种首尾相接后面监听前面的方式可以避免羊群效应。所谓羊群效应就是一个节点挂掉所有节点都去监听然后做出反应这样会给服务器带来巨大压力所以有了临时顺序节点当一个节点挂掉只有它后面的那一个节点才做出反应。
3.3、分布式锁的抢占过程
接下来我们一起来看看多客户端获取及释放zk分布式锁的整个流程及背后的原理。
首先大家看看下面的图如果现在有两个客户端一起要争抢zk上的一把分布式锁会是个什么场景 参见上图。zk里有一把锁这个锁就是zk上的一个节点。然后呢两个客户端都要来获取这个锁具体是怎么来获取呢
咱们就假设客户端A抢先一步对zk发起了加分布式锁的请求这个加锁请求是用到了zk中的一个特殊的概念叫做“临时顺序节点”。
简单来说就是直接在my_lock这个锁节点下创建一个顺序节点这个顺序节点有zk内部自行维护的一个节点序号。
3.3.1、客户端A发起一个加锁请求
比如说第一个客户端来搞一个顺序节点zk内部会给起个名字叫做xxx-000001。然后第二个客户端来搞一个顺序节点zk可能会起个名字叫做xxx-000002。大家注意一下最后一个数字都是依次递增的从1开始逐次递增。zk会维护这个顺序。
所以这个时候假如说客户端A先发起请求就会搞出来一个顺序节点大家看下面的图Curator框架大概会弄成如下的样子 大家看客户端A发起一个加锁请求先会在你要加锁的node下搞一个临时顺序节点这一大坨长长的名字都是Curator框架自己生成出来的。
然后那个最后一个数字是1。大家注意一下因为客户端A是第一个发起请求的所以给他搞出来的顺序节点的序号是1。
接着客户端A创建完一个顺序节点。还没完他会查一下my_lock这个锁节点下的所有子节点并且这些子节点是按照序号排序的这个时候他大概会拿到这么一个集合 接着客户端A会走一个关键性的判断就是说唉兄弟这个集合里我创建的那个顺序节点是不是排在第一个啊
如果是的话那我就可以加锁了啊因为明明我就是第一个来创建顺序节点的人所以我就是第一个尝试加分布式锁的人啊
bingo加锁成功大家看下面的图再来直观的感受一下整个过程。 3.3.2、客户端B过来排队
接着假如说客户端A都加完锁了客户端B过来想要加锁了这个时候他会干一样的事儿先是在my_lock这个锁节点下创建一个临时顺序节点此时名字会变成类似于 大家看看下面的图 客户端B因为是第二个来创建顺序节点的所以zk内部会维护序号为2。
接着客户端B会走加锁判断逻辑查询my_lock锁节点下的所有子节点按序号顺序排列此时他看到的类似于 同时检查自己创建的顺序节点是不是集合中的第一个
明显不是啊此时第一个是客户端A创建的那个顺序节点序号为01的那个。所以加锁失败
3.3.3、客户端B开启监听客户端A
加锁失败了以后客户端B就会通过ZK的API对他的顺序节点的上一个顺序节点加一个监听器。zk天然就可以实现对某个节点的监听。
客户端B的顺序节点是 他的上一个顺序节点不就是下面这个吗 即客户端A创建的那个顺序节点
所以客户端B会对
这个节点加一个监听器监听这个节点是否被删除等变化大家看下面的图。 接着客户端A加锁之后可能处理了一些代码逻辑然后就会释放锁。那么释放锁是个什么过程呢
其实很简单就是把自己在zk里创建的那个顺序节点也就是 这个节点给删除。
删除了那个节点之后zk会负责通知监听这个节点的监听器也就是客户端B之前加的那个监听器说兄弟你监听的那个节点被删除了有人释放了锁。 此时客户端B的监听器感知到了上一个顺序节点被删除也就是排在他之前的某个客户端释放了锁。
3.3.4、客户端B抢锁成功
此时就会通知客户端B重新尝试去获取锁也就是获取my_lock节点下的子节点集合此时为 集合里此时只有客户端B创建的唯一的一个顺序节点了
然后呢客户端B判断自己居然是集合中的第一个顺序节点bingo可以加锁了直接完成加锁运行后续的业务代码即可运行完了之后再次释放锁。 3.4、分布式锁的基本实现
接下来就是基于ZooKeeper实现一下分布式锁。首先定义了一个锁的接口Lock很简单仅仅两个抽象方法一个加锁方法一个解锁方法。Lock接口的代码如下
package com.crazymakercircle.zk.distributedLock;/*** create by 尼恩 疯狂创客圈**/
public interface Lock {/*** 加锁方法** return 是否成功加锁*/boolean lock() throws Exception;/*** 解锁方法** return 是否成功解锁*/boolean unlock();
}
使用ZooKeeper实现分布式锁的算法有以下几个要点 一把分布式锁通常使用一个Znode节点表示如果锁对应的Znode节点不存在首先创建Znode节点。这里假设为“/test/lock”代表了一把需要创建的分布式锁。 抢占锁的所有客户端使用锁的Znode节点的子节点列表来表示如果某个客户端需要占用锁则在“/test/lock”下创建一个临时有序的子节点。 这里所有临时有序子节点尽量共用一个有意义的子节点前缀。 比如如果子节点的前缀为“/test/lock/seq-”则第一次抢锁对应的子节点为“/test/lock/seq-000000000”第二次抢锁对应的子节点为“/test/lock/seq-000000001”以此类推。 再比如如果子节点前缀为“/test/lock/”则第一次抢锁对应的子节点为“/test/lock/000000000”第二次抢锁对应的子节点为“/test/lock/000000001”以此类推也非常直观。 如果判定客户端是否占有锁呢 很简单客户端创建子节点后需要进行判断自己创建的子节点是否为当前子节点列表中序号最小的子节点。如果是则认为加锁成功如果不是则监听前一个Znode子节点变更消息等待前一个节点释放锁。 一旦队列中的后面的节点获得前一个子节点变更通知则开始进行判断判断自己是否为当前子节点列表中序号最小的子节点如果是则认为加锁成功如果不是则持续监听一直到获得锁。 获取锁后开始处理业务流程。完成业务流程后删除自己的对应的子节点完成释放锁的工作以方面后继节点能捕获到节点变更通知获得分布式锁。
3.5、加锁的实现
Lock接口中加锁的方法是lock。lock方法的大致流程是首先尝试着去加锁如果加锁失败就去等待然后再重复。
3.5.1、lock方法的实现代码
lock方法加锁的实现代码大致如下
package com.crazymakercircle.zk.distributedLock;import com.crazymakercircle.zk.ZKclient;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;/*** create by 尼恩 疯狂创客圈**/
Slf4j
public class ZkLock implements Lock {//ZkLock的节点链接private static final String ZK_PATH /test/lock;private static final String LOCK_PREFIX ZK_PATH /;private static final long WAIT_TIME 1000;//Zk客户端CuratorFramework client null;private String locked_short_path null;private String locked_path null;private String prior_path null;final AtomicInteger lockCount new AtomicInteger(0);private Thread thread;public ZkLock() {ZKclient.instance.init();synchronized (ZKclient.instance) {if (!ZKclient.instance.isNodeExist(ZK_PATH)) {ZKclient.instance.createNode(ZK_PATH, null);}}client ZKclient.instance.getClient();}Overridepublic boolean lock() {
//可重入确保同一线程可以重复加锁synchronized (this) {if (lockCount.get() 0) {thread Thread.currentThread();lockCount.incrementAndGet();} else {if (!thread.equals(Thread.currentThread())) {return false;}lockCount.incrementAndGet();return true;}}try {boolean locked false;
//首先尝试着去加锁locked tryLock();if (locked) {return true;}//如果加锁失败就去等待while (!locked) {await();//获取等待的子节点列表ListString waiters getWaiters();
//判断是否加锁成功if (checkLocked(waiters)) {locked true;}}return true;} catch (Exception e) {e.printStackTrace();unlock();}return false;}//...省略其他的方法}3.5.2、tryLock尝试加锁
尝试加锁的tryLock方法是关键做了两件重要的事情
1创建临时顺序节点并且保存自己的节点路径
2判断是否是第一个如果是第一个则加锁成功。如果不是就找到前一个Znode节点并且保存其路径到prior_path。
尝试加锁的tryLock方法其实现代码如下 /*** 尝试加锁* return 是否加锁成功* throws Exception 异常*/private boolean tryLock() throws Exception {//创建临时Znodelocked_path ZKclient.instance.createEphemeralSeqNode(LOCK_PREFIX);//然后获取所有节点ListString waiters getWaiters();if (null locked_path) {throw new Exception(zk error);}//取得加锁的排队编号locked_short_path getShortPath(locked_path);//获取等待的子节点列表判断自己是否第一个if (checkLocked(waiters)) {return true;}// 判断自己排第几个int index Collections.binarySearch(waiters, locked_short_path);if (index 0) { // 网络抖动获取到的子节点列表里可能已经没有自己了throw new Exception(节点没有找到: locked_short_path);}//如果自己没有获得锁则要监听前一个节点prior_path ZK_PATH / waiters.get(index - 1);return false;}private String getShortPath(String locked_path) {int index locked_path.lastIndexOf(ZK_PATH /);if (index 0) {index ZK_PATH.length() 1;return index locked_path.length() ? locked_path.substring(index) : ;}return null;}
创建临时顺序节点后其完整路径存放在locked_path成员中另外还截取了一个后缀路径放在locked_short_path成员中后缀路径是一个短路径只有完整路径的最后一层。
为什么要单独保存短路径呢 因为在获取的远程子节点列表中的其他路径返回结果时返回的都是短路径都只有最后一层路径。所以为了方便后续进行比较也把自己的短路径保存下来。
创建了自己的临时节点后调用checkLocked方法判断是否是锁定成功。如果锁定成功则返回true如果自己没有获得锁则要监听前一个节点此时需要找出前一个节点的路径并保存在prior_path成员中供后面的await等待方法去监听使用。
3.5.3、checkLocked检查是否持有锁
在checkLocked方法中判断是否可以持有锁。判断规则很简单当前创建的节点是否在上一步获取到的子节点列表的第一个位置
1如果是说明可以持有锁返回true表示加锁成功
2如果不是说明有其他线程早已先持有了锁返回false。
checkLocked方法的代码如下 private boolean checkLocked(ListString waiters) {//节点按照编号升序排列Collections.sort(waiters);// 如果是第一个代表自己已经获得了锁if (locked_short_path.equals(waiters.get(0))) {log.info(成功的获取分布式锁,节点为{}, locked_short_path);return true;}return false;}
checkLocked方法比较简单将参与排队的所有子节点列表从小到大根据节点名称进行排序。排序主要依靠节点的编号也就是后Znode路径的10位数字因为前缀都是一样的。排序之后做判断如果自己的locked_short_path编号位置排在第一个如果是则代表自己已经获得了锁。如果不是则会返回false。
如果checkLocked为false外层的调用方法一般来说会执行await等待方法执行夺锁失败以后的等待逻辑。
3.5.4、await监听前一个节点释放锁
await也很简单就是监听前一个ZNode节点prior_path成员的删除事件代码如下
private void await() throws Exception {if (null prior_path) {throw new Exception(prior_path error);}final CountDownLatch latch new CountDownLatch(1);//订阅比自己次小顺序节点的删除事件Watcher w new Watcher() {Overridepublic void process(WatchedEvent watchedEvent) {System.out.println(监听到的变化 watchedEvent watchedEvent);log.info([WatchedEvent]节点删除);latch.countDown();}};client.getData().usingWatcher(w).forPath(prior_path);
/*//订阅比自己次小顺序节点的删除事件TreeCache treeCache new TreeCache(client, prior_path);TreeCacheListener l new TreeCacheListener() {Overridepublic void childEvent(CuratorFramework client,TreeCacheEvent event) throws Exception {ChildData data event.getData();if (data ! null) {switch (event.getType()) {case NODE_REMOVED:log.debug([TreeCache]节点删除, path{}, data{},data.getPath(), data.getData());latch.countDown();break;default:break;}}}};treeCache.getListenable().addListener(l);treeCache.start();*/latch.await(WAIT_TIME, TimeUnit.SECONDS);
}
首先添加一个Watcher监听而监听的节点正是前面所保存在prior_path成员的前一个节点的路径。这里仅仅去监听自己前一个节点的变动而不是其他节点的变动提升效率。完成监听之后调用latch.await线程进入等待状态一直到线程被监听回调代码中的latch.countDown() 所唤醒或者等待超时。
上面的代码中监听前一个节点的删除可以使用两种监听方式
1Watcher 订阅
2TreeCache 订阅。
两种方式的效果都差不多。但是这里的删除事件只需要监听一次即可不需要反复监听所以使用的是Watcher一次性订阅。而TreeCache 订阅的代码在源码工程中已经被注释仅仅供大家参考。
一旦前一个节点prior_path节点被删除那么就将线程从等待状态唤醒重新一轮的锁的争夺直到获取锁并且完成业务处理。
至此分布式Lock加锁的算法还差一点就介绍完成。这一点就是实现锁的可重入。
3.5.5、可重入的实现代码
什么是可重入呢只需要保障同一个线程进入加锁的代码可以重复加锁成功即可。 修改前面的lock方法在前面加上可重入的判断逻辑。代码如下
Override
public boolean lock() {//可重入的判断synchronized (this) {if (lockCount.get() 0) {thread Thread.currentThread();lockCount.incrementAndGet();} else {if (!thread.equals(Thread.currentThread())) {return false;}lockCount.incrementAndGet();return true;}}//....
}为了变成可重入在代码中增加了一个加锁的计数器lockCount 计算重复加锁的次数。如果是同一个线程加锁只需要增加次数直接返回表示加锁成功。
至此lock方法已经介绍完成接下来就是去释放锁
3.6、释放锁的实现
Lock接口中的unLock方法表示释放锁释放锁主要有两个工作
1减少重入锁的计数如果最终的值不是0直接返回表示成功的释放了一次
2如果计数器为0移除Watchers监听器并且删除创建的Znode临时节点。
unLock方法的代码如下 /*** 释放锁** return 是否成功释放锁*/Overridepublic boolean unlock() {
//只有加锁的线程能够解锁if (!thread.equals(Thread.currentThread())) {return false;}
//减少可重入的计数int newLockCount lockCount.decrementAndGet();
//计数不能小于0if (newLockCount 0) {throw new IllegalMonitorStateException(Lock count has gone negative for lock: locked_path);}
//如果计数不为0直接返回if (newLockCount ! 0) {return true;}//删除临时节点try {if (ZKclient.instance.isNodeExist(locked_path)) {client.delete().forPath(locked_path);}} catch (Exception e) {e.printStackTrace();return false;}return true;}
这里为了尽量保证线程安全可重入计数器的类型使用的不是int类型而是Java并发包中的原子类型——AtomicInteger。
3.7、分布式锁的使用
写一个用例测试一下ZLock的使用代码如下 Testpublic void testLock() throws InterruptedException {for (int i 0; i 10; i) {FutureTaskScheduler.add(() - {//创建锁ZkLock lock new ZkLock();lock.lock();
//每条线程执行10次累加for (int j 0; j 10; j) {
//公共的资源变量累加count;}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.info(count count);//释放锁lock.unlock();});}Thread.sleep(Integer.MAX_VALUE);}
以上代码是10个并发任务每个任务累加10次执行以上用例会发现结果会是预期的和100如果不使用锁结果可能就不是100因为上面的count是一个普通的变量不是线程安全的。
原理上一个Zlock实例代表一把锁并需要占用一个Znode永久节点如果需要很多分布式锁则也需要很多的不同的Znode节点。
3.8、ZooKeeper分布式锁的优点和缺点
总结一下ZooKeeper分布式锁
1优点ZooKeeper分布式锁如InterProcessMutex能有效的解决分布式问题不可重入问题使用起来也较为简单。
2缺点ZooKeeper实现的分布式锁性能并不太高。为啥呢 因为每次在创建锁和释放锁的过程中都要动态创建、销毁瞬时节点来实现锁功能。大家知道ZK中创建和删除节点只能通过Leader服务器来执行然后Leader服务器还需要将数据同不到所有的Follower机器上这样频繁的网络通信性能的短板是非常突出的。
总之在高性能高并发的场景下不建议使用ZooKeeper的分布式锁。而由于ZooKeeper的高可用特性所以在并发量不是太高的场景推荐使用ZooKeeper的分布式锁。
在目前分布式锁实现方案中比较成熟、主流的方案有两种
1基于Redis的分布式锁
2基于ZooKeeper的分布式锁
两种锁分别适用的场景为
1基于ZooKeeper的分布式锁适用于高可靠高可用而并发量不是太大的场景
2基于Redis的分布式锁适用于并发量很大、性能要求很高的、而可靠性问题可以通过其他方案去弥补的场景。
总之这里没有谁好谁坏的问题而是谁更合适的问题。