1. 概述
- 可以通过Java的AIP作为zookeeper客户端,实现与zookeeper服务器的交互。
- 客户端应该遵循以下步骤,与服务器进行清晰和干净的交互
- 连接到zookeeper服务器。zookeeper服务器为客户端分配会话ID。
- 定期向服务器发送心跳。否则,zookeeper服务器将过期会话ID,客户端需要重新连接。
- 只要会话ID处于活动状态,就可以获取/设置znode。
- 所有任务完成后,断开与zookeeper服务器的连接。如果客户端长时间不活动,则 zookeeper服务器将自动断开客户端。
2. 准备工作
- 导包
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper --> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.10</version> </dependency> - 默认使用log4j作为日志,写一个log4j的配置文件,叫
log4j.properties# 全局日志配置 log4j.rootLogger=INFO, stdout # 控制台输出 log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n - 写连接及断开连接代码。这段代码一般会通过Spring自动注入,但目前用测试,所以在测试类里面写好。
@Before的代码,会在执行真正的test程序之前执行,@after则会在结束之后再执行。 - 创建
ZooKeeper需要提供ip地址及端口号、超时时间,再使用了一个监视器。由于这是异步执行的开启客户端,因此做一个阻塞操作,防止还没开启就执行后面的操作,在真正打开了客户端之后,发送一个消息,并解掉阻塞。public class GetTest { private final static String IP = "192.168.233.133:2181"; // ip及端口 private final static Integer TIMEOUT = 5000; // 超时时间,毫秒为单位 private final static CountDownLatch countDownLatch = new CountDownLatch(1); private final static Logger log = Logger.getLogger(GetTest.class); private ZooKeeper zooKeeper; @Before public void before() throws IOException, InterruptedException { zooKeeper = new ZooKeeper(IP, TIMEOUT, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { log.info("创建了连接"); countDownLatch.countDown(); } }); countDownLatch.await(); //阻塞当前线程 } @After public void after() throws InterruptedException { zooKeeper.close(); } } - 至此,完成了准备工作,以后的增删改查等操作,只要在类里面新增测试方法即可。
3. 新增节点
// 同步方式
create(String path, byte[] data, List<ACL> acl, CreateMode createMode)
// 异步方式
create(String path, byte[] data, List<ACL> acl, CreateMode createMode, AsyncCallback.StringCallback callBack,Object ctx)
path:znode路径。例如,/node1 /node1/node11data:要存储在指定znode路径中的数据acl:要创建的节点的访问控制列表。zookeeper API提供了一个静态接口ZooDefs.Ids来获取一些基本的acl列表。例如,ZooDefs.Ids.OPEN_ACL_UNSAFE返回打开znode的acl列表。createMode:节点的类型,这是一个枚举。callBack:异步回调接口ctx:传递上下文参数
3.1 ZooDefs 介绍
- 用于保存ACL(权限信息)常量的类
public class ZooDefs { public static final String[] opNames = new String[]{"notification", "create", "delete", "exists", "getData", "setData", "getACL", "setACL", "getChildren", "getChildren2", "getMaxChildren", "setMaxChildren", "ping"}; public ZooDefs() { } public interface Ids { Id ANYONE_ID_UNSAFE = new Id("world", "anyone"); Id AUTH_IDS = new Id("auth", ""); ArrayList<ACL> OPEN_ACL_UNSAFE = new ArrayList(Collections.singletonList(new ACL(31, ANYONE_ID_UNSAFE))); ArrayList<ACL> CREATOR_ALL_ACL = new ArrayList(Collections.singletonList(new ACL(31, AUTH_IDS))); ArrayList<ACL> READ_ACL_UNSAFE = new ArrayList(Collections.singletonList(new ACL(1, ANYONE_ID_UNSAFE))); } public interface Perms { int READ = 1; int WRITE = 2; int CREATE = 4; int DELETE = 8; int ADMIN = 16; int ALL = 31; } public interface OpCode { int notification = 0; int create = 1; int delete = 2; int exists = 3; int getData = 4; int setData = 5; int getACL = 6; int setACL = 7; int getChildren = 8; int sync = 9; int ping = 11; int getChildren2 = 12; int check = 13; int multi = 14; int auth = 100; int setWatches = 101; int sasl = 102; int createSession = -10; int closeSession = -11; int error = -1; } } ZooDefs.Ids.OPEN_ACL_UNSAFE:表示开放权限,所有用户拥有所有权限ZooDefs.Ids.CREATOR_ALL_ACL:表示使用auth权限模式,并且对于满足条件的用户开放所有权限ZooDefs.Ids.READ_ACL_UNSAFE:表示对于所有用户,只开放Read权限ZooDefs.Ids.ANYONE_ID_UNSAFE:是一个常用的Id对象,表示所有用户ZooDefs.Ids.AUTH_IDS:是一个Auth模式的Id对象,具体操作还要在后面演示。- 如上例子也可以看得出来,其实ACL的列表,其实也就是通过多个ACL组成的列表,每个ACL对象的参数也就是Id对象和
ZooDefs.Perms的权限参数组成。 - 我们自己定制自己的权限,也可以通过这些常用量进行。
3.2 ZNode 类型的枚举类
- ZNode可以分为持久无顺序、持久有顺序、临时无顺序、临时有顺序
- 在创建新节点的时候,也需要予以定义,同样的,也有相关的枚举类
public enum CreateMode { PERSISTENT(0, false, false), PERSISTENT_SEQUENTIAL(2, false, true), EPHEMERAL(1, true, false), EPHEMERAL_SEQUENTIAL(3, true, true); } CreateMode.PERSISTENT:表示持久无顺序的节点CreateMode.PERSISTENT_SEQUENTIAL:表示持久有顺序的节点CreateMode.EPHEMERAL:表示临时无顺序的节点CreateMode.EPHEMERAL_SEQUENTIAL:表示临时有顺序的节点
3.3 创建一个持久无顺序的开放权限节点
- 该例子创建一个同步方法
@Test public void testCreate1() throws KeeperException, InterruptedException { // 创建了一个开放权限,持久的节点 // arg3:权限列表 world:anyone:cdrwa // arg4:节点类型 持久化节点 zooKeeper.create("/hadoop/child", "data".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); }
3.4 创建一个持久无顺序的读权限节点
@Test
public void testCreate2() throws KeeperException, InterruptedException {
// Ids.READ_ACL_UNSAFE world:anyone:r
zooKeeper.create("/hadoop/child1", "data".getBytes(), ZooDefs.Ids.READ_ACL_UNSAFE, CreateMode.PERSISTENT);
}
3.5 创建一个持久无顺序的读写权限节点
@Test
public void testCreate3() throws KeeperException, InterruptedException {
Id id = ZooDefs.Ids.ANYONE_ID_UNSAFE; // 所有用户
List<ACL> acl = Arrays.asList(new ACL(ZooDefs.Perms.READ, id), new ACL(ZooDefs.Perms.WRITE, id)); // 自己合成权限,读写权限wr
zooKeeper.create("/hadoop/child2", "data".getBytes(), acl, CreateMode.PERSISTENT);
}
3.6 创建一个持久无顺序有IP权限的节点
@Test
public void testCreate4() throws KeeperException, InterruptedException {
Id ip = new Id("ip", "192.168.233.133"); // 设置ip权限
List<ACL> acl = Collections.singletonList(new ACL(ZooDefs.Perms.ALL, ip)); // 表示对指定ip开放全部权限
zooKeeper.create("/hadoop/child3", "data".getBytes(), acl, CreateMode.PERSISTENT);
}
3.7 创建一个持久有顺序的 auth 模式限制的节点
@Test
public void testCreate5() throws KeeperException, InterruptedException {
zooKeeper.addAuthInfo("digest", "root:root".getBytes()); // 添加授权用户
// 设置为auth授权模式。使用内置的方式,即允许所有权限
// 设置为持久顺序节点,会将授权的用户添加进去
zooKeeper.create("/hadoop/child4", "data".getBytes(), ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT_SEQUENTIAL);
}
值得一提的是,该方法需要调用
addAuthInfo在zookeeper内部先注册一个用户。这里需要注意,不能忘了
3.8 创建一个临时 auth 模式读权限的节点
@Test
public void testCreate6() throws KeeperException, InterruptedException {
// 自定义权限
// 授权模式和授权对象
zooKeeper.addAuthInfo("digest", "root:root".getBytes()); // 添加授权用户
Id id = new Id("auth", "root");
List<ACL> acl = Collections.singletonList(new ACL(ZooDefs.Perms.READ, id));
zooKeeper.create("/hadoop/child5", "data".getBytes(), acl, CreateMode.EPHEMERAL);// 在关闭当前客户端之前,其他客户端可以查得到该节点,关闭客户端之后这个节点就销毁了
Thread.sleep(10000);
}
3.9 创建一个 digest 模式的节点
@Test
public void testCreate7() throws KeeperException, InterruptedException {
// digest授权模式,需要提前计算密文
Id digest = new Id("digest", "root:qiTlqPLK7XM2ht3HMn02qRpkKIE=");
List<ACL> acl = Collections.singletonList(new ACL(ZooDefs.Perms.ALL, digest));
zooKeeper.create("/hadoop/child6", "data".getBytes(), acl, CreateMode.PERSISTENT);// 在关闭当前客户端之前,其他客户端可以查得到该节点,关闭客户端之后这个节点就销毁了
}
值得一提的是,需要提前计算好密文
3.10 异步的方法创建一个全权限节点
@Test
public void testCreate8() throws InterruptedException {
// 创建了一个开放权限,持久的节点
// arg3:权限列表 world:anyone:cdrwa
// arg4:节点类型 持久化节点
zooKeeper.create("/hadoop/child7", "data".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, new AsyncCallback.StringCallback() {
@Override
public void processResult(int rc, String path, Object ctx, String name) {
log.info(rc); // 0代表成功了
log.info(path); // 传进来的,添加的节点
log.info(name); // 真正查到的节点的名字
log.info(ctx.toString()); // 上下文参数,ctx传进来的东西
log.info("终于结束啦!");
countDownLatch.countDown(); // 关闭等待
}
}, "ctx");
countDownLatch.await(); // 拥塞,等待结束
}
4. 更新节点
// 同步方式
setData(String path, byte[] data, int version)
// 异步方式
setData(String path, byte[] data, int version,AsyncCallback.StatCallback callBack, Object ctx)
path:znode路径。例如,/node1 /node1/node11data:要存储在指定znode路径中的数据version:znode的当前版本。值为-1时,表示不需要考虑版本。如果指定版本之后,就可以做成一个乐观锁。callBack:异步回调接口ctx:传递上下文参数
4.1 更新一个节点(同步)
@Test
public void testSet1() throws KeeperException, InterruptedException {
Stat stat = zooKeeper.setData("/hadoop", "newData".getBytes(), -1); // 返回状态信息
log.info(stat.toString()); // 将状态信息打印
log.info("当前版本号" + stat.getVersion());
log.info("节点创建时间" + stat.getCtime());
log.info("节点修改时间" + stat.getMtime());
}
4.2 更新一个节点(异步)
@Test
public void testSet2() throws InterruptedException {
zooKeeper.setData("/hadoop", "newData".getBytes(), 4, new AsyncCallback.StatCallback() { // 乐观锁
@Override
public void processResult(int rc, String path, Object ctx, Stat stat) {
log.info(rc); // 0 代表修改成功
log.info(path); // 输入的节点路径
log.info(stat.getVersion()); // 当前版本
countDownLatch.countDown(); // 解放
}
}, null); // 返回状态信息
countDownLatch.await(); // 阻塞
}
4.3 更新auth或digest权限的节点
@Test
public void testSet3() throws KeeperException, InterruptedException {
zooKeeper.addAuthInfo("digest", "root:root".getBytes()); // 添加授权用户
Stat stat = zooKeeper.setData("/hadoop/child4", "newData".getBytes(), -1); // 对于有权限限制的节点,需要提前添加授权用户
log.info(stat.toString()); // 将状态信息打印
log.info("当前版本号" + stat.getVersion());
log.info("节点创建时间" + stat.getCtime());
log.info("节点修改时间" + stat.getMtime());
}
更新该节点的时候,需要注意,要先添加用户,否则没有权限更新
5. 删除节点
// 同步方式
delete(String path, int version)
// 异步方式
delete(String path, int version, AsyncCallback.VoidCallback callBack, Object ctx)
path:znode路径。例如,/node1 /node1/node11version:znode的当前版本。值为-1时,表示不需要考虑版本。如果指定版本之后,就可以做成一个乐观锁。callBack:异步回调接口ctx:传递上下文参数
5.1 删除一个节点(同步)
@Test
public void testDelete1() throws KeeperException, InterruptedException {
zooKeeper.delete("/hadoop/child1", -1); // 如果节点不存在,会删除失败
}
5.2 删除一个节点(异步)
@Test
public void testDelete2() throws KeeperException, InterruptedException {
zooKeeper.delete("/hadoop/child40000000004", -1, new AsyncCallback.VoidCallback() {
@Override
public void processResult(int rc, String path, Object ctx) {
log.info(rc);
log.info(path);
}
}, null); // 如果节点不存在,会删除失败
}
6. 查看节点
// 同步方式
getData(String path, boolean b, Stat stat)
// 异步方式
getData(String path, boolean b,AsyncCallback.DataCallback callBack, Object ctx)
path:znode路径。例如,/node1 /node1/node11b:是否使用连接对象中注册的监视器stat:返回znode的元数据callBack:异步回调接口ctx:传递上下文参数
6.1 查看一个节点(同步)
@Test
public void testGet1() throws KeeperException, InterruptedException {
Stat stat = new Stat();
byte[] data = zooKeeper.getData("/hadoop", false, stat);
log.info("获取到的数据是:" + new String(data));
log.info("当前节点的版本:" + stat.getVersion());
}
6.2 查看一个节点(异步)
@Test
public void testGet2() throws InterruptedException {
zooKeeper.getData("/hadoop", null, new AsyncCallback.DataCallback() {
@Override
public void processResult(int rc, String path, Object ctx, byte[] bytes, Stat stat) {
log.info(rc);
log.info(path);
log.info(new String(bytes));
log.info(stat.getVersion());
countDownLatch.countDown();
}
}, null);
countDownLatch.await(); // 拥塞
}
7. 查看子节点
// 同步方式
getChildren(String path, boolean b)
// 异步方式
getChildren(String path, boolean b,AsyncCallback.ChildrenCallback callBack,Object ctx)
path:znode路径。例如,/node1 /node1/node11b:是否使用连接对象中注册的监视器callBack:异步回调接口ctx:传递上下文参数
7.1 获取所有子节点的名称(同步)
@Test
public void testChildren() throws KeeperException, InterruptedException {
List<String> children = zooKeeper.getChildren("/hadoop", null); // 此操作,类似于ls
children.forEach(System.out::println); // 获取所有子节点的名称
}
7.2 获取所有子节点的名称(异步)
@Test
public void testChildren2() throws InterruptedException {
zooKeeper.getChildren("/hadoop", null, new AsyncCallback.Children2Callback() { // 此操作类似于ls2
@Override
public void processResult(int rc, String path, Object ctx, List<String> list, Stat stat) {
log.info(rc);
log.info(path);
log.info(stat.getVersion());
log.info(list.toString());
countDownLatch.countDown();
}
}, null);
countDownLatch.await();
}
8. 检查节点是否存在
// 同步方法
exists(String path, boolean b)
// 异步方法
exists(String path, boolean b,AsyncCallback.StatCallback callBack,Object ctx)
path:znode路径。例如,/node1 /node1/node11b:是否使用连接对象中注册的监视器callBack:异步回调接口ctx:传递上下文参数
8.1 检查节点是否存在(同步)
@Test
public void existsTest() throws KeeperException, InterruptedException {
Stat stat = zooKeeper.exists("/hadoop", null); // 这操作,相当于stat
log.info(stat.toString());
}
8.2 检查节点是否存在(异步)
@Test
public void existsTest2() throws InterruptedException {
zooKeeper.exists("/hadoop", null, new AsyncCallback.StatCallback() {
@Override
public void processResult(int rc, String path, Object ctx, Stat stat) {
log.info(rc);
log.info(path);
log.info(stat.getVersion());
log.info(stat.getCtime());
countDownLatch.countDown();
}
}, null);
countDownLatch.await();
}
9. 监听器
9.1 概念
- zookeeper提供了数据的发布/订阅功能,多个订阅者可同时监听某一特定主题对 象,当该主题对象的自身状态发生变化时(例如节点内容改变、节点下的子节点列表改变 等),会实时、主动通知所有订阅者
- zookeeper采用了Watcher机制实现数据的发布/订阅功能。该机制在被订阅对 象发生变化时会异步通知客户端,因此客户端不必在Watcher注册后轮询阻塞,从而减轻了客户端压力。
- watcher机制实际上与观察者模式类似,也可看作是一种观察者模式在分布式场景下的实现方式
9.2 watcher架构
- Watcher实现由三个部分组成:
Zookeeper服务端Zookeeper客户端- 客户端的
ZKWatchManager对象
- 客户端首先将Watcher注册到服务端,同时将Watcher对象保存到客户端的Watch管理器中。当ZooKeeper服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的Watch管理器会触发相关Watcher来回调相应处理逻辑,从而完成整体的数据发布/订阅流程。
9.3 watcher特性
- 一次性:一旦触发就失效了,再次使用需要再次注册
- 客户端顺序回调:watcher回调是顺序串行化执行的,只有回调后客户端才能看到最新的数据状态。一个watcher回调逻辑不应该太多,以免影响别的watcher执行
- 轻量级:WatchEvent是最小的通信单元,结构上只包含通知状态、事件类型和节点路径,并不会告诉数据节点变化前后的具体内容
- 时效性:watcher只有在当前session彻底失效时才会无效,若在session有效期内 快速重连成功,则watcher依然存在,仍可接收到通知
9.4 Watcher接口
- Watcher是一个接口,任何实现了Watcher接口的类就是一个新的Watcher。
- Watcher内部包含了两个枚举类:KeeperState、EventType
9.4.1 通知状态(KeeperState)
- KeeperState是客户端与服务端连接状态发生变化时对应的通知类型。
9.4.2 事件类型(EventType)
EventType是数据节点(znode)发生变化时对应的通知类型。EventType变化时KeeperState永远处于SyncConnected通知状态下;当KeeperState发生变化时,EventType永远为None。
9.5 捕获相应的事件
- 在
get和exists类方法中,可以添加Watcher。 - 以下是调用的注册方法和可监听事件间的关系
- 简单来说,就是
getData()方法的监听器可以监测节点的修改和删除;exists()的监听器可以监测节点的修改和删除外,还可以检测节点的创建;getChildren()的监听器可以监听该节点的子节点的修改与删除。
9.6 注册Watcher
9.6.1 监听客户端与服务端的连接状态
- 当连接状态发生修改的时候,会修改
KeeperState的状态,而EventType会保持为None。 - 该监听器在实例化Zookeeper的客户端的时候就放入,当连接状态发生变化的时候就会触发,并执行
Watcher中的方法。并可以重复工作(究竟为什么不是一次性的,这个还需要考察)。
9.6.1.1 实验
- 首先实现了Watcher接口,在触发方法的时候,首先检查一下事件类型
EventType是不是None(一般都是)。然后根据KeepState的状态,输出不同的日志。其中,当是连接成功的状态时,因为有可能外面的方法还在拥塞,因此顺便唤醒一下。public class ZKConnectionWatcher implements Watcher { private CountDownLatch countDownLatch = null; private static final Logger log = Logger.getLogger(ZKConnectionWatcher.class); public ZKConnectionWatcher() { } public ZKConnectionWatcher(CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; } @Override public void process(WatchedEvent watchedEvent) { if (watchedEvent.getType() == Event.EventType.None) { // 说明没有发生除了连接问题以外的事件 switch (watchedEvent.getState()) { case SyncConnected: log.info("客户端与服务器正常连接"); if (this.countDownLatch != null) this.countDownLatch.countDown(); break; case Disconnected: log.info("客户端与服务器断开连接"); break; case Expired: log.info("会话SESSION超时"); break; case AuthFailed: log.info("身份认证失败"); break; } } } public void setCountDownLatch(CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; } } - 调用时,与正常的调用一样。在初始化客户端的时候,实例化一个之前写的
Watcher即可。最后加一个睡眠,可以有更多的测试。比如说,如果直接关闭zookeeper的服务器,就会触发方法,状态为Disconnected。如果addAuthInfo时digest写错,状态就会变成AuthFailedpublic class ConnectWatcherTest { private final static String IP = "192.168.233.133:2181"; // ip及端口 private final static Integer TIMEOUT = 5000; // 超时时间,毫秒为单位 private final static CountDownLatch countDownLatch = new CountDownLatch(1); private final static Logger log = Logger.getLogger(ConnectWatcherTest.class); private ZooKeeper zooKeeper; @Test public void testWatcher() throws IOException, InterruptedException, KeeperException { zooKeeper = new ZooKeeper(IP, TIMEOUT, new ZKConnectionWatcher(countDownLatch)); countDownLatch.await(); log.info("当前会话ID" + zooKeeper.getSessionId()); zooKeeper.addAuthInfo("digest", "root:root".getBytes()); Stat stat = new Stat(); byte[] data = zooKeeper.getData("/hadoop/child3", false, stat); log.info("接收到的数据是:" + new String(data)); Thread.sleep(5000); zooKeeper.close(); log.info("结束"); } }
值得一提的是,监听器可以重复触发。只要有变化,都会触发。
9.6.2 监听节点的状态
// 使用连接对象的监视器
exists(String path, boolean b)
// 自定义监视器
exists(String path, Watcher w)
// NodeCreated:节点创建
// NodeDeleted:节点删除
// NodeDataChanged:节点内容发生变化
- path:znode路径
- b:是否使用连接对象中注册的监视器
- w:监视器对象
9.6.2.1 实验一:使用连接对象中的监视器
public class ZKCommonWatcher implements Watcher {
private CountDownLatch countDownLatch = null;
private static final Logger log = Logger.getLogger(ZKConnectionWatcher.class);
public ZKCommonWatcher() {
}
public ZKCommonWatcher(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void process(WatchedEvent watchedEvent) {
switch (watchedEvent.getType()) {
case NodeCreated:
log.info("节点创建了");
break;
case NodeDataChanged:
log.info("节点数据被修改了");
break;
case NodeDeleted:
log.info("节点被删除了");
break;
case None: {
Event.KeeperState state = watchedEvent.getState();
log.info(state);
if (this.countDownLatch != null && state == Event.KeeperState.SyncConnected)
this.countDownLatch.countDown();
break;
}
}
}
public void setCountDownLatch(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
}
public class ExistsWatcherTest {
private final static String IP = "192.168.233.133:2181"; // ip及端口
private final static Integer TIMEOUT = 5000; // 超时时间,毫秒为单位
private final static CountDownLatch countDownLatch = new CountDownLatch(1);
private final static Logger log = Logger.getLogger(ConnectWatcherTest.class);
private ZooKeeper zooKeeper;
@Before
public void before() throws IOException, InterruptedException {
zooKeeper = new ZooKeeper(IP, TIMEOUT, new ZKCommonWatcher(countDownLatch)); // 实例化一个watcher
countDownLatch.await(); //阻塞当前线程
}
@Test
public void testExists() throws KeeperException, InterruptedException {
Stat stat = zooKeeper.exists("/hadoop/child100", true); // 使用在创建客户端时的watcher
if (stat != null) log.info(stat.getVersion());
Thread.sleep(100000);
}
@After
public void after() throws InterruptedException {
zooKeeper.close();
}
}
在注册Exists监视器的时候,选择true,使用默认的连接对象的监视器。连接方面的监视可以多次触发,但是节点状态方面的触发只能有一次。
9.6.2.2 实验二:使用自定义的一次性的监视器
- 其实也就是在
exists中注册一个匿名的Watcher实现类,在方法中辨别事件类型,并根据不同的类型做出不同的处理。 - 这个
Watcher只能触发一次,触发完之后就不能再监测了。
@Test
public void testExists2() throws KeeperException, InterruptedException {
// 一次性的,用完就没了
Stat stat = zooKeeper.exists("/hadoop/child100", new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
switch (watchedEvent.getType()) {
case NodeCreated:
log.info("x节点创建了");
break;
case NodeDataChanged:
log.info("x节点数据被修改了");
break;
case NodeDeleted:
log.info("x节点被删除了");
break;
}
}
});
if (stat != null) log.info(stat.getVersion());
Thread.sleep(100000);
}
9.6.2.3 实验三:使用多个自定义的一次性的监视器
- 可以注册多个监视器对同一个节点的状态进行监视。而触发的执行顺序,会根据注册的顺序来执行。
@Test public void testExists3() throws KeeperException, InterruptedException { // 注册多个监听器 zooKeeper.exists("/hadoop/child100", new Watcher() { @Override public void process(WatchedEvent watchedEvent) { log.info("1"); log.info(watchedEvent.getType()); } }); zooKeeper.exists("/hadoop/child100", new Watcher() { @Override public void process(WatchedEvent watchedEvent) { log.info("2"); log.info(watchedEvent.getType()); } }); Thread.sleep(100000); }
9.6.2.4 实验四:使用可以多次使用的监视器
- 其实监视器还是一次性的,只是在监视器处理结束之前,重新再注册一个新的监视器。
@Test public void testExists4() throws KeeperException, InterruptedException { String path = "/hadoop/child100"; // 重复使用,用完再注册一个新的 Stat stat = zooKeeper.exists(path, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { switch (watchedEvent.getType()) { case NodeCreated: log.info("x节点创建了"); break; case NodeDataChanged: log.info("x节点数据被修改了"); break; case NodeDeleted: log.info("x节点被删除了"); break; } try { zooKeeper.exists(path, this); // 走之前,又注册一遍 } catch (KeeperException | InterruptedException e) { e.printStackTrace(); } } }); if (stat != null) log.info(stat.getVersion()); Thread.sleep(100000); } getData方法注册的监视器与上述方法类似,在此就不再赘述。
9.6.3 监听子节点的状态
// 使用连接对象的监视器
getChildren(String path, boolean b)
// 自定义监视器
getChildren(String path, Watcher w)
// NodeChildrenChanged:子节点发生变化
// NodeDeleted:节点删除
- path:znode路径
- b:是否使用连接对象中注册的监视器
- w:监视器对象
9.6.3.1 实验一:使用自定义的子节点监视器
- 使用连接对象的监视器实际上没什么区别,在此就不再演示。
- 值得一提的是,该监测器只能检测到节点的增加或减少,不能监测到节点内容的改变。每次触发的事件类型都是
NodeChildrenChanged@Test public void testChildrenWatcher() throws KeeperException, InterruptedException { String path = "/hadoop"; List<String> children = zooKeeper.getChildren(path, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { log.info("目前的状态是:" + watchedEvent.getState()); log.info("发生了" + watchedEvent.getType() + "事件"); try { zooKeeper.getChildren(path, this); // 重新注册 } catch (KeeperException | InterruptedException e) { e.printStackTrace(); } } }); Thread.sleep(100000); }
9.7 配置中心案例
- zookeeper 保存数据库的配置信息。当配置信息发生修改的时候,提醒客户端的监听器配置值发生了变化,配置信息跟着更新一遍。
- 用于保存数据库信息的配置类,可以通过输入一个Map,通过反射实现属性的配置。
@Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) public class DBConfig { private String ip; private String username; private String password; private static final Logger log = Logger.getLogger(DBConfig.class); public DBConfig(Map<String, String> config) throws NoSuchFieldException, IllegalAccessException { importConfig(config); log.info("创建DBConnection成功: " + this.toString()); } public void importConfig(Map<String, String> config) throws NoSuchFieldException, IllegalAccessException { for (Map.Entry<String, String> entry : config.entrySet()) { String key = entry.getKey(); key = key.substring(key.lastIndexOf("/") + 1); String value = entry.getValue(); Field field = this.getClass().getDeclaredField(key); field.setAccessible(true); field.set(this, value); } } } - 实现监听器的配置中心。负责处理zookeeper与配置信息的关系。在连接成功的时候,会向zookeeper取值,放在config的键值对中,并将值赋予DBConfig对象;当有值发生变化时,会触发监听器,然后监听器会重新初始化DBConfig对象。实现监听值的修改。
public class ConfigCenter implements Watcher, Closeable { private String ip = null; // ip及端口 private static Integer timeOut = 5000; // 超时时间,毫秒为单位 private final static CountDownLatch countDownLatch = new CountDownLatch(1); private final static Logger log = Logger.getLogger(ConfigCenter.class); private ZooKeeper zooKeeper = null; private DBConfig dbConfig = null; private final Map<String, String> config = new HashMap<>(); public ConfigCenter() { } public ConfigCenter(String ip) throws IOException, InterruptedException { this(ip, timeOut); } public ConfigCenter(String ip, Integer timeOut) throws IOException, InterruptedException { this.ip = ip; ConfigCenter.timeOut = timeOut; initZK(ip, timeOut); } /** * 初始化zookeeper * @param ip * @param timeOut * @throws IOException * @throws InterruptedException */ private void initZK(String ip, Integer timeOut) throws IOException, InterruptedException { zooKeeper = new ZooKeeper(ip, timeOut, this); countDownLatch.await(); } /** * 编辑config的键值对,修改或增加某个键值对 * * @param path * @param data * @return */ private Map<String, String> editConfig(String path, byte[] data) { config.put(path, new String(data)); return config; } /** * 初始化config键值对,在zookeeper中读出来 * * @throws KeeperException * @throws InterruptedException */ private void initConfig() throws KeeperException, InterruptedException { byte[] ip = zooKeeper.getData("/config/ip", true, null); // 每次触发之后,会重新初始化监听器 editConfig("/config/ip", ip); byte[] username = zooKeeper.getData("/config/username", true, null); editConfig("/config/username", username); byte[] password = zooKeeper.getData("/config/password", true, null); editConfig("/config/password", password); } /** * 读取zookeeper的配置值,并且放入到DBConnection中 */ private void initDB() { try { initConfig(); if (dbConfig == null) { dbConfig = new DBConfig(config); } else { dbConfig.importConfig(config); } log.info("初始化DBConnection成功!"); } catch (KeeperException | InterruptedException | NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); } } @Override public void process(WatchedEvent watchedEvent) { // 如果是None 说明是连接方面的变化 if (watchedEvent.getType() == Event.EventType.None) { switch (watchedEvent.getState()) { case SyncConnected: log.info("连接 " + ip + "成功"); initDB(); // 初始化db countDownLatch.countDown(); break; case Disconnected: log.error("连接 " + ip + "已断开"); break; case Expired: log.error("连接 " + ip + "已超时,需要重新连接服务器端"); try { // 重新连接 this.initZK(ip, timeOut); } catch (IOException | InterruptedException e) { e.printStackTrace(); } break; case AuthFailed: log.error("身份验证失败"); break; } } else { // 如果不是连接方面的变化,那就是监视的节点发生了变化 switch (watchedEvent.getType()) { case NodeDataChanged: log.info("配置的节点信息发生了变化,赶紧重新配置!"); initDB(); // 因为配置发生了变化,因此需要重新初始化db break; case NodeCreated: log.info("添加了配置信息,目前的配置信息是"); break; } } } @Override public void close() throws IOException { try { zooKeeper.close(); log.info("zooKeeper已关闭"); } catch (InterruptedException e) { e.printStackTrace(); } } }