文章目录
- 03-zookeeper理论相关——watch及API操作
- zookeeper中的事件和状态
- zookeeper客户端与zookeeper server连接的状态
- zookeeper中的watch事件(当zookeeper客户端监听某个znode节点"/node-x"时)
- watch机制
- watch特性1——一次性触发器
- watch特性2——发送至客户端
- watch特性3——设置watch的数据内容
- zookeeper watch的运行机制
- zookeeper API操作
03-zookeeper理论相关——watch及API操作
zookeeper主要是为了统一分布式系统中各个节点工作状态,在资源冲突的情况下协调提供节点的资源抢占,提供给每个节点了解整个集群所处状态的途径。这一切的实现都依赖于zookeeper中的事件监听和通知机制。
zookeeper中的事件和状态
事件和状态构成了zookeeper客户端连接描述的两个维度。通过下面两个表详细介绍zookeeper中的事件和状态。(API中被定义为@Deprecated的事件和状态就不介绍了)。
zookeeper客户端与zookeeper server连接的状态
连接状态 | 状态含义 |
KeeperState.Expire | 客户端和服务器在ticktime的时间周期内,是要发送心跳通知的。这是租约协议的一个实现。客户端发送request,告诉服务器其上一个租约时间,服务器收到这个请求后,告诉客户端其下一个租约时间是哪个时间点。当客户端时间戳达到最后一个租约时间,而没有收到服务器发来的任何新租约时间,即认为自己下线(此后客户端会废弃这次连接)。这个过期状态就是Expired状态。 |
KeeperState.Disconnected | 就像上面哪个状态所述,当客户端断开一个连接(可能是租约期满,也可能是客户端主动断开)这时客户端和服务器的连接就是Disconnected状态。 |
KeeperState.SyncConnected | 一旦客户端和服务器的某一个节点建立连接(注意,虽然集群有多个节点,但是客户端一次连接到一个节点就行了),并完成一次version、zxid的同步,这时的客户端和服务器的连接状态就是SyncConnected。 |
KeeperState.AuthFailed | zookeeper客户端进行连接认证失败时,发生该状态。 |
需要说明的是,这些状态在触发时,所记录的事件类型都是:EventType.None
zookeeper中的watch事件(当zookeeper客户端监听某个znode节点"/node-x"时)
zookeeper事件 | 事件含义 |
EventType.NodeCreated | 当node-x这个节点被创建时,该事件被触发。 |
EventType.NodeChildrenChanged | 当node-x这个节点的直接子节点被创建、被删除、子节点数据发生变更时,该事件被触发。 |
EventType.NodeDataChanged | 当node-x这个节点的数据发生变更时,该事件触发。 |
EventType.NodeDeleted | 当node-x这个节点被删除时,该事件被触发。 |
EventType.None | 当zookeeper客户端的连接状态发生变更时,即KeeperState.Expired、KeeperState.Disconnected、KeeperState.SyncConnected、KeeperState.AuthFailed状态切换时,描述的事件类型为EventType.None |
watch机制
znode发生变化(znode本身的增加、删除、修改、以及子znode的变化)可以通过watch机制通知到客户端。那么要实现watch,就必须实现org.apache.zookeeper.Watcher
接口,并且将实现类的对象传入到可以watch的方法中。zookeeper中所有读操作(getData(),getChildren(),exits())都可以设置watch选项。watch事件具有**one-time trigger(一次性触发)**的特性,如果watch监听的znode有变化,那么就会通知设置该watch的客户端。
在上述说到的所有读操作中,如果需要watch,可以自定义watch,如果是boolean型变量,当为true时,则使用系统默认的watch,系统默认的watch是在zookeeper的构造函数中定义的watch(在new zk时也可以自己指定)。参数中watch为空或者false,表示不启用watch。
watch特性1——一次性触发器
客户端在znode设置了watch时,如果znode内容发生改变,那么客户端就会获得watch事件。例如:客户端设置getData("/znode1",true)
后,如果/znode1发生改变或者删除,那么客户端就会得到一个/znode1的watch事件,但是/znode1再次发生变化,那客户端是无法收到watch事件的,除非客户端设置了新的watch。
watch特性2——发送至客户端
watch事件是异步发送到Client。zookeeper可以保证客户端发送过去的更新顺序是有序的。例如:某个znode没有设置watch,那么客户端对这个znode设置watch发送到集群之前,该客户端是感知不到该znode任何的改变情况。换个角度解释:由于watch有一次性触发的特点,所以在服务器端没有watch的情况下,znode的任何变更就不会通知到客户端。
不过,即使某个znode设置了watch,且在znode有变化的情况下通知到了客户端,但是在客户端接收到这个变化事件,还没有再次设置watch之前,如果其他客户端对znode做了修改,这种情况下,znode第二次的变化客户端是无法收到通知的。这可能是由于网络延迟或者其他因素导致,所以在使用zookeeper不能期望能够监控到节点每次的变化。zookeeper只能保证最终的一致性,而无法保证强一致性。
watch特性3——设置watch的数据内容
znode改变有很多种方式,例如:节点创建,节点删除,子节点改变等等。zookeeper维护了两个watch列表,一个节点数据watch列表,另一个是子节点watch列表。getData()
和exits()
设置数据watch,getChildren()
设置子节点watch。两者选其一,可以让我们根据不同的返回结果选择不同的watch方式,getData()
和exits()
返回节点的内容,getChildren()
返回子节点列表。
因此,setData()
触发内容watch,create()
触发当前节点的内容watch或者是其父节点的子节点watch。delete()
同时触发父节点的子节点watch和内容watch,以及子节点的内容watch。
zookeeper watch的运行机制
- watch是轻量级的,其实就是本地JVM的callback,服务器端只是存了是否有设置watch的布尔类型。(源码见:
org.apache.zookeeper.server.FinalRequestProcessor
) - 在服务端,在
FianlRequestProcessor
处理对应的znode操作时,会根据客户端传递的watch变量,添加到对应的ZKDatabase(org.apache.zookeeper.server.ZKDatabase)中进行持久化存储,同时将自己NIOServerCnxn作为一个Watcher callback,监听服务端事件变化。 - leader通过投票通过了某次znode变化的请求后,然后通知对应的follower,follower根据自己内存中的zkDataBase信息,发送notification信息给zookeeper客户端。
- zookeeper客户端接收到notification信息后,找到对应变化path的watch列表,挨个进行触发回调。
zookeeper API操作
创建一个maven项目,引入以下依赖:
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
<!--<type>pom</type>-->
</dependency>
注意:引入的版本需要和你服务器上的zookeeper版本一致。
代码如下,代码很简单,配合注释看就可以:
package com.wangwren;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.concurrent.CountDownLatch;
public class App {
public static void main( String[] args ) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
/**
* 创建一个zookeeper
*
* zookeeper是有session概念的,没有连接池的概念
*
* new一个zookeeper,第一个参数是集群的ip,并指定端口号,集群之间用逗号隔开;
* 第二个参数代表session创建出的数据的过期时间,单位毫秒,即当session退出时,zookeeper给它创建的数据还保留3秒钟
* 第三个参数创建一个watch监听,这个watch是session级别的,跟path、node没关系,只是在创建zookeeper时监听
*/
ZooKeeper zk = new ZooKeeper("192.168.0.20:2181,192.168.0.21:2181,192.168.0.22:2181,192.168.0.22:2181"
, 3000
, new Watcher() {
/**
* 重写watch的回调方法
*
* 程序都是顺序执行的,这种回调方法像是单独开了一个线程,由别人调用的,即另开了一个线程
* 所以想要等zookeeper正在创建完成,需要在主程序中等待
*/
@Override
public void process(WatchedEvent event) {
//zookeeper创建时的状态
Event.KeeperState state = event.getState();
//zookeeper创建时的类型
Event.EventType type = event.getType();
System.out.println("new zk " + event.toString());
switch (state) {
case Unknown:
break;
case Disconnected:
break;
case NoSyncConnected:
break;
case SyncConnected:
System.out.println("new zk connected...");
latch.countDown();
break;
case AuthFailed:
break;
case ConnectedReadOnly:
break;
case SaslAuthenticated:
break;
case Expired:
break;
}
switch (type) {
case None:
break;
case NodeCreated:
break;
case NodeDeleted:
break;
case NodeDataChanged:
break;
case NodeChildrenChanged:
break;
}
}
});
//等待zk创建,如果这里不等待,那么程序顺序执行,可能输出的结果就是connecting,zk还没创建完呢,怎么继续使用?
latch.await();
//获取zk的创建状态
ZooKeeper.States state = zk.getState();
switch (state) {
case CONNECTING:
System.out.println("connecting...");
break;
case ASSOCIATING:
break;
case CONNECTED:
System.out.println("connected...");
break;
case CONNECTEDREADONLY:
break;
case CLOSED:
break;
case AUTH_FAILED:
break;
case NOT_CONNECTED:
break;
}
//简单使用
/*
创建节点
第一个参数:节点名称
第二个参数:数据,zookeeper也是二进制安全的,与Redis一样
第三个参数:权限相关
第四个参数:创建的节点类型,即 -s -e 这里创建的是-e,临时节点类型
还有一种创建节点,方法名都一样,只是带了另一个参数:回调方法
*/
String pathName = zk.create("/xxoo", "hello".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
//输出的结果就是 /xxoo
System.out.println("创建的节点名称:" + pathName);
/*
获取数据
第一个参数:指定路径
第二个参数:指定监听,watch的注册发生在读的时候和exits(判断是否存在)
而且watch监听只会触发一次,如果想多次触发,那么可以在回调方法中重新注册
第三个参数:指定节点元数据信息
*/
Stat stat = new Stat();
//返回值是该节点上的数据
byte[] data = zk.getData(pathName, new Watcher() {
//获取节点时指定的监听回调方法,该方法只有在节点被修改、删除、子节点被更改时会被调用
@Override
public void process(WatchedEvent event) {
try {
System.out.println("修改节点时被触发...");
/*
重新注册了watch,注意第二个参数,该参数可以传入一个boolean变量的值,true表示使用 new zk 时的那个watch
写this就是表示当前对象,即当前的watch,又被注册了一遍
如果不写下面这行代码,那么在第二次修改数据时watch中的回调方法将不会再被调用
*/
zk.getData(pathName,this,stat);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, stat);
System.out.println("获取节点上的数据:" + new String(data));
//触发节点上的回调,最后一个参数:指定版本,当版本与当前要修改的版本不同时是不允许修改的会报错;但是写-1就随便修改
Stat stat1 = zk.setData(pathName, "hello world".getBytes(), -1);
//再次修改还是可以触发watch回调
Stat stat2 = zk.setData(pathName, "hello world world".getBytes(), -1);
//获取数据时还可以使用回调方法,同样的方法,但是需要写回调方法,数据都在回调方法里
System.out.println("=========async start============");
/*
第一个参数:节点
第二个参数:不适用监听
第三个参数:指定回调方法
第四个参数:上下文,传入的是一个object对象
*/
zk.getData(pathName, false, new AsyncCallback.DataCallback() {
@Override
public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
System.out.println("=========async callback============");
System.out.println("rc = " + rc);
System.out.println("path = " + path);
System.out.println("ctx = " + ctx);
System.out.println("data = " + new String(data));
}
},zk);
System.out.println("=========async over============");
/**
* 通过输出结果可以看到,确实是回调方法,即start over都打印完了才打印的callback
* 即程序还是顺序执行了,这种方式会将CPU用到极致,即只有当有消息来的时候才做某一件事,而不是傻等着,这种getData方法也没有返回值
*
* 这是异步的方式
*
* 所以写了一个死循环阻塞在这
*/
while (true){
}
}
}