Java分布式注册中心之Zookeeper

Zookeeper是什么

ZooKeeper是一个用于维护配置信息、命名、提供分布式同步和提供组服务的集中式服务,它常作为一个注册中心服务用于分布式项目。

Zookeeper拥有以下几个重要特性

顺序一致性:来自客户端的相关指令会按照顺序执行,不会出现乱序的情况,客户端发送到服务的指令1->2->3->4,那个这些指令就会按照顺序执行;

原子性:更新只有成功和失败,没有中间状态;

可靠性:也可以称之为持久性,节点更新以后,在下次更新之前,它的数据不会发生变更;

准实时性:也可以称之为最终一致性,在zk集群中,一个客户端修改了其中的一个节点,一定时间以后,所有可用的服务对应的节点都会变成更新以后的值。

Zookeeper如何保证数据一致性

Zookeeper的数据一致性是通过ZAP(原子广播)协议来保证的且zk的数据一致性为最终一致性,意思是zk只保证数据在事务前后是一致的并不能保证在过程中全程一致(这是因为zk的数据同步是一个二阶段提交的过程,后面会讲zk数据同步过程)。

整个ZAB协议一共定义了三个阶段

1) 发现:要求zookeeper集群必须选举出一个 Leader 进程,同时 Leader 会维护一个 Follower 可用客户端列表。将来客户端可以和这些 Follower节点进行通信。

2) 同步:Leader 要负责将本身的数据与 Follower 完成同步,做到多副本存储。这样也是体现了CAP中的一致性(C)和分区容错(P)。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。
3) 广播:Leader 可以接受客户端新的事务Proposal请求,将新的Proposal请求广播给所有的 Follower。

补充:CAP原则

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
可用性(A):保证每个请求不管成功或者失败都有响应。
分区容忍性(P):系统中任意信息的丢失或失败不会影响系统的继续运作。

ZAB协议有两种模式:一种是消息广播模式,另一种是崩溃恢复模式

  • 消息广播模式:

  当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进入消息广播模式, 当一台同样遵守ZAB协议的服务器启动后加入到集群中,如果此时集群中已经存在一个Leader服务器在负责进入消息广播,那么加入的服务器就会自觉地进入数据恢复模式:找到Leader所在的服务器,并与其进⾏数据同步,然后⼀起参与到消息⼴播流程中去。 Zookeeper只允许唯一的一个Leader服务器来进⾏事务请求的处理, Leader服务器在接收到客户端的事务请求后,会生成对应的事务提议并发起一轮广播协议,而如果集群中的其他机器收到客户端的事务请求后,那么这些非Leader服务器会首先将这个事务请求转发给Leader服务器。

  • 崩溃恢复模式:

  当整个服务框架启动过程中,或者是Leader服务器出现网络中断、崩溃退出或重启等异常情况时, ZAB协议就会进入崩溃恢复模式,同时选举产生新的Leader服务器。当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后, ZAB协议就会退出恢复模式,其中,所谓的状态同步 就是指数据同步,用来保证集群中过半的机器能够和Leader服务器的数据状态保持一致。

Zookeeper数据结构

ZK内部的存储方式十分类似于文件存储结构,采用了分层存储结构。但是它和文件存储结构的区别是,它的各个节点中是允许存储数据的,需要注意的是zk的每个节点存储数据不能超过1M。每个节点被称为ZNode。Zookeeper中创建的节点分为两种:永久性节点和临时性节点。

临时节点:临时节点的生命周期和客户端会话绑定在一起,客户端会话失效,则这个节点就会被自动清除。
非临时节点(永久节点):该数据节点被创建后,就会一直存在于zookeeper服务器上,直到有删除操作来主动删除这个节点。

Zookeeper数据同步过程

  1. leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id(zxid)。
  2. leader为每个follower准备了一个FIFO队列(通过TCP协议来实现,以实现了全局有序这一个特点)将带有 zxid的消息作为一个提案(proposal)分发给所有的follower。
  3. 当follower接收到proposal,先把proposal写到磁盘,写入成功以后再向leader回复一个ack。
  4. 当leader接收到合法数量(超过半数节点,大于N/2)的ACK后,leader就会向这些follower发送commit命令,同时会在本地执行该消息。
  5. 当follower收到消息的commit命令以后,会提交该消息。

Zookeeper选举机制

集群机器ID

集群机器ID是指myid,它是每一个集群机器中的编号文件,代表Zookeeper集群服务器的标识,手动生成,全局全一。

事务ID

事务ID是指zxid,Zookeeper会给每个更新请求分配一个事务ID,它是一个64位的数字,由Leader统一进行分配,全局唯一,不断递增,在一个节点的状态信息中可以查看到最新的事务ID信息。

集群服务器角色

Zookeeper集群服务器有以下 3 种角色:
Leader(主)
Follower(从,参与投票)
Observer(观察者,不参与投票)

集群服务器状态

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

选举场景

1、Zookeeper 集群启动初始化时进行选举:
2、Zookeeper 集群 Leader 失联时重新选举

选举前提条件

1、Zookeeper 服务器处于 LOOKING 竞选状态
此时说明 Zookeeper 服务器集群处于群龙无首状态,另外,观察者状态不能参与竞选投票。


2、Zookeeper集群规模至少要3台机器或以上
集群规则为:2N+1台,N>0,即最少需要3台,因为Zookeeper集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在ZK 节点挂得太多,只剩一半或不到一半节点能工作时,集群才会失效。

3个节点的 Cluster 可以挂掉1个节点,此时Leader 可以得到2票,仍然满足大于N/2票(此时为3/2=1.5)
2个节点的 Cluster 就不能挂掉任何 1 个节点了,此时Leader可以得到1票,不满足大于N/2票(此时为2/2=1)

Zookeeper选举流程

1.初始化选举

Zookeeper 在集群启动时会进行选举,这里拿 3 台服务器进行举例:
依次启动3台服务器 zk1, zk2, zk3,初始情况下事务 ID 都为 0。

选举大致流程:

a、初始投票
服务器启动后,每个 Server 都会给自己投上一票,每次投票会包含所投票服务器的 myid 和 zxid,这里使用 Server(myid, zxid)的方式表示,此时的投票结果为:zk1(1, 0),zk2(2, 0),zk3(3, 0)

b、同步投票结果
集群中的服务器在投票后,会将各自的投票结果同步给集群中其他服务器。

c、检查投票有效性
各服务器在收到投票后会检查投票的有效性,如:是否本轮投票,是否来自 LOOKING 状态的服务器的投票等。

d、处理投票
服务器之间会进行投票比对,规则如下:
优先检查 zxid,较大的服务器优先作为 Leader
如果 zxid 相同,则 myid 较大的服务器作为 Leader
如:zk1 和 zk2 进行比对,此时 zk2 胜出,zk1 更新自己的投票为:zk1(2, 0)

e、统计投票结果
每轮投票比对之后都会统计投票结果,确认是否有超过半数的机器都得到相同的投票结果,如果是,则选出 Leader,否则继续投票。
本轮选举中,zk1 和 zk2 都得到了相同的投票结果(2, 0),2 指 zk2,并且超过了半数的机器(2 > 1.5),所以此时 zk2 就成为了本轮选举的 Leader。
所以,即使 zk3 启动了,因为集群已经有了 Leader,所以选举也结束了,zk3 不再参与选举,后面进来的都是小弟。

f、更改服务器状态
一旦选出 Leader,每个服务器就会各自更新自己的状态:

zk1 --》 FOLLOWINGzk2 --》 LEADINGZk3 --》 FOLLOWING

2.集群重新选举

Zookeeper 集群运行期间无法和 Leader 保持正常连接时,即如果 Leader 挂了,或者 Leader 服务器故障都会进行新一轮的 Leader 选举。
这里还是拿 3 台服务器进行举例:
如果作为初始选举的 Leader zk3 挂了,集群就会暂停对外提供服务,从而进行新的 Leader 选举。

选举大致过程:

a、状态变更
既然过去的老大 Leader 不可用了,那所有的 Follower 服务器就需要从 FOLLOWING 状态变更为:LOOKING,开始新的一轮 Leader 选举。

b、开始投票
投票逻辑和启动初始时一致。
zk1, zk3 第一轮投票默认还是会先投给自己,zk2 挂了不能进行投票。
第一轮投票结果为:zk1(1, 66)、zk3(3, 28),zk1 和 zk3 各得一票,这里假设 zk1 事务 ID 比 zk3 更大一点。

c、同步投票结果
同步投票逻辑和启动初始时一致。

d、检查投票有效性
检查投票逻辑和启动初始时一致。

e、处理投票
处理投票逻辑和启动初始时一致。
此时 zk1 和 zk3 进行比对,根据规则,由于 zk1 的事务 ID 更大一点,所以 zk1 胜出,zk3 也更新自己的投票为:zk3(1, 28)

f、统计投票结果
统计投票逻辑和启动初始时一致。
本轮选举中,zk1 和 zk3 都得到了相同的投票结果 zk1,并且超过了半数的机器(2 > 3 / 2),所以此时 zk1 就成为了本轮选举的 Leader。

g、更改服务器状态
更改状态逻辑和启动初始时一致。

总结

Zookeeper 集群按 myid 从小到大依次启动初始化时,在超过半数机器的投票的情况下,谁的 myid 最后,谁就是 Leader,知道这个定律,不同的集群规模我们都可以推算出谁是 Leader。

集群重新选举时,根据 myid 和 zxid 的大小共同决断,zxid 更大的优先成为 Leader。

Watch机制

ZooKeeper 提供了分布式数据发布/订阅功能,一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使他们能够做出相应的处理。

Watch机制的特点:

1、一次性触发数据发生改变时,一个 watcher event 会被发送到 client,但是 client只会收到一次这样的信息。 

2、watcher event 异步发送 watcher 的通知事件从 server 发送到 client 是异步的,这就存在一个问题,不同的客户端和服务器之间通过 socket进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于 Zookeeper 本身提供了 ordering guarantee,即客户端监听事件后,才会感知它所监视 znode发生了变化。所以我们使用Zookeeper不能期望能够监控到节点每次的变化。Zookeeper 只能保证最终的一致性,而无法保证强一致性。 

3、数据监视 Zookeeper 有数据监视和子数据监视 getdata() and exists()设置数据监视,getchildren()设置了子节点监视。

4、注册 watcher getData、exists、getChildren

5、触发 watcher create、delete、setData

6、setData()会触发 znode 上设置的 data watch(如果 set 成功的话)。一个成功的create()操作会触发被创建的 znode 上的数据 watch,以及其父节点上的 child watch。而一个成功的 delete()操作将会同时触发一个 znode 的 data watch 和 child watch(因为这样就没有子节点了),同时也会触发其父节点的 child watch。 

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

8、Watch 是轻量级的,其实就是本地 JVM 的 Callback,服务器端只是存了是否有设置了 Watcher 的布尔类型。

Zookeeper如何实现分布式锁

1、指定一个节点,作为存放节点的根节点。

2、使用临时有序节点,然后每次线程进来就有序创建临时节点。

3、每个节点都去监听前一个节点是否存在,存在则说明还没释放锁。

4、因为有序,所以最小的节点持有锁,用完以后删除临时节点,释放锁。

5、下一节点通过watch机制监控到上级已经删除即释放掉锁,所以可以获取到锁,后续以此类推。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 使用 ZooKeeper 实现分布式锁最常见的方法是使用公平锁(Fair Lock)。在这种情况下,在 ZooKeeper 集群中创建一个特殊的临时节点,并使用它来保持锁定状态。当一个客户端尝试获取锁时,它会尝试创建这个临时节点,如果节点创建成功,则说明该客户端已经获得了锁。 使用 ZooKeeper 进行分布式锁的实现需要使用 ZooKeeper 的原子操作,例如创建节点和监视节点,以保证锁的正确性。 代码示例如下: ```java public class DistributedLock { private ZooKeeper zk; private String lockNode; private String lockPath; public DistributedLock(ZooKeeper zk, String lockNode) { this.zk = zk; this.lockNode = lockNode; this.lockPath = "/locks/" + lockNode; } public void lock() throws Exception { while (true) { try { zk.create(lockPath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); return; } catch (KeeperException.NodeExistsException e) { waitForLock(); } } } public void unlock() throws Exception { zk.delete(lockPath, -1); } private void waitForLock() throws Exception { CountDownLatch latch = new CountDownLatch(1); Watcher lockWatcher = new Watcher() { @Override public void process(WatchedEvent event) { if (event.getType() == EventType.NodeDeleted) { latch.countDown(); } } }; zk.exists(lockPath, lockWatcher); latch.await(); } } ``` 该代码示例提供了一个简单的实现,可以作为创建自己的分布式锁的基 ### 回答2: 在基于 ZooKeeper 实现 Java 分布式锁的过程中,可以按照以下步骤进行: 1. 创建一个基于 ZooKeeper 的客户端对象。 2. 在 ZooKeeper 上创建一个持久化的节点,作为锁的根节点。 3. 当需要进行锁操作时,创建一个临时有序节点作为当前请求的锁节点。 4. 调用 `getChildren` 方法获取锁根节点下的所有节点,并根据节点的序号进行排序。 5. 检查当前节点是否是锁根节点下序号最小的节点,如果是则获取到锁,执行业务逻辑。 6. 如果当前节点不是锁根节点下序号最小的节点,则注册监听锁根节点下序号比自己小一的节点。 7. 当监听到序号比自己小一的节点删除时,重复步骤 4-6 直到获取到锁。 8. 业务逻辑执行完成后,可以删除当前节点,释放锁资源。 此外,还需要特别注意以下几个问题: 1. 分布式锁的超时机制:在创建临时节点时,可以指定一个超时时间,当超过该时间后,如果还未获取到锁,可以删除当前节点,避免死锁。 2. 锁的释放:在业务逻辑执行完成后,需要手动删除当前节点。如果由于某些原因未能正常删除,则需要提供一种机制,在锁节点创建时设置一个 TTL(time-to-live),让 ZooKeeper 在锁节点过期后自动删除。 3. 锁节点的竞争:在并发较高的情况下,可能会出现多个客户端同时创建临时节点的情况。这时可以使用 `CyclicBarrier` 或者 `CountDownLatch` 进行同步,确保每次只有一个客户端创建锁节点。 4. 异常情况的处理:在进行锁操作时,需要处理各种异常情况,比如连接断开、网络超时等,保证系统的稳定性和可靠性。 综上所述,基于 ZooKeeper 可以实现 Java 分布式锁,通过创建临时有序节点和监听上一个节点的删除来实现锁的竞争和获取。 ### 回答3: 实现基于 ZooKeeperJava 分布式锁可以遵循以下步骤: 1. 连接 ZooKeeper:首先,通过 Java API 连接到 ZooKeeper 服务器,可以使用 zookeeper API 提供的 ZooKeeper 类来创建一个连接对象。 2. 创建锁节点:在 ZooKeeper 上创建一个父节点作为锁的根节点,该节点的所有子节点都作为锁节点。可以使用 zookeeper API 的 create() 方法创建临时顺序节点。 3. 获取锁:每个需要获取锁的进程都要通过创建一个临时顺序节点来竞争锁。通过 zookeeper API 的 getChildren() 方法获取锁根节点的所有子节点,如果创建的节点序号是当前所有节点中最小的,则表示获取到了锁。 4. 监听锁节点变化:如果未能获取到锁,应该在创建节点后,使用 zookeeper API 的 exists() 方法注册一个监听器来监听创建的子节点。当监听到创建的子节点发生变化时,判断自己的节点是否变成了最小的节点,如果是则表示获取到了锁。 5. 释放锁:对于已经获取到锁的进程,执行完任务后,需要通过 zookeeper API 的 delete() 方法将自己创建的锁节点删除,这样其他进程就能获取到该锁了。 需要注意的是,在分布式环境下,网络通信可能会出现延迟或故障,因此需要考虑到这些情况来保证分布式锁的正确性和可靠性。此外,还需考虑到异常情况处理、死锁检测和容错等问题,以确保分布式锁的高可用性和可靠性。 以上是使用 ZooKeeper 实现 Java 分布式锁的基本步骤,通过合理地使用 ZooKeeper 的 API,可以轻松实现分布式环境下的锁机制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值