1、案例描述
这是一个从从zookeeper官网获取的简单案例。请先确保你已经安装好一个zookeeper机器环境,如果没有请参考我的文章《zookeeper入门》http://blog.csdn.net/koflance/article/details/78586235。
案例基本设计要求是,监听一个zookeeper的节点路径,比如/test,如果节点路径上保存的数据发生了变更,则将数据写入到指定的本地文件中,同时启动一个本地脚本命令,比如用cat将该本地文件读取出来,显示在终端。
基本步骤如下:
- 实例化一个zookeeper客户端,并将一个哨兵(watcher)注册到该节点下;
- 实例化一个节点数据监听器(datamointor),用异步方式(zk.exists)获取节点数据的变更状态;
- 实例化一个脚本执行器(Executor),并将其注册为数据监听器(datamointor)的观察者(listener);
- 数据监听器(datamointor)收到zk客户端的变更消息,立即用异步方式(zk.exists)获取节点数据,并判断是否变更(b != preData),如果变更,则通知观察者(listener);
- 脚本执行器收到datamointor的数据变更通知,立即将获取到的节点数据写入文件(filename),写入之后启用本地脚本任务process,将内容打印出来。
2、案例代码
- Executor 脚本执行器及zookeeper客户端和哨兵
package com.xxxx.xxxx.zookeeper;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.io.*;
/**
* 在指定的znode路径节点上,使用DataMonitor获取节点数据或状态变更情况。
* 这个类会观察指定znode节点并保存数据在该路径上,当znode存在时,启动指定的程序;当znode节点不存在时,关闭指定的程序。
*
* Created by wushiweijun on 2017/11/20.
*/
public class Executor implements Watcher, Runnable, DataMonitor.DataMonitorListener {
private final DataMonitor dm;
private final ZooKeeper zk;
private final String filename;
private final String[] exec;
/*需要执行命令的程序*/
private Process child;
public static void main(String[] args) {
args = "localhost:2181,localhost:2182,localhost:2183 /test /Users/wushiweijun/Documents/test/zookeeper/executor.txt cat /Users/wushiweijun/Documents/test/zookeeper/executor.txt".split(" ");
// if (args.length < 4) {
// System.err
// .println("USAGE: Executor hostPort znode filename program [args ...]");
// System.exit(2);
// }
String hostPort = args[0];
String znode = args[1];
String filename = args[2];
String exec[] = new String[args.length - 3];
System.arraycopy(args, 3, exec, 0, exec.length);
try {
new Executor(hostPort, znode, filename, exec).run();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
* @param hostPort 集群的host地址列表,多个用逗号分隔,例如host:port,host:port,host:port/app/a
* @param znode 访问的节点路径
* @param filename
* @param exec 执行的命令程序,例如create xxx
* @throws KeeperException
* @throws IOException
*/
public Executor(String hostPort, String znode, String filename,
String exec[]) throws KeeperException, IOException {
this.filename = filename;
this.exec = exec;
/*
* 创建ZK客户端,但要注意,该对象实例化时,并不会进行连接服务端,而只是初始化连接,真正的连接是异步。
* zookeeper会随机挑选(不是按照顺序)一个hostPort进行尝试,直到找到一个可以连接成功的host或者sessionTimeout
* 入参格式如下:
* hostport:
* host:port,host:port,host:port -- 针对集群
* host:port,host:port,host:port/app/a --针对需要初始默认的根目录情况,这个被称为chroot suffix
* sessionTimeout:
* 链接超时时间,单位毫秒
* watcher:
* 一个哨兵回调对象,用于监听节点状态的变更
*/
zk = new ZooKeeper(hostPort, 3000, this);
dm = new DataMonitor(zk, znode, null, this);
}
public void run() {
try {
synchronized (this) {
while (!dm.dead) {
wait();
}
}
} catch (InterruptedException e) {
}
}
/***************************************************************************
* WatchedEvent可以告诉你三个信息:
* 1、发生了什么
* 2、在那个znode路径发生的
* 3、节点当前状态是什么
* @see org.apache.zookeeper.Watcher#process(WatchedEvent)
*/
@Override
public void process(WatchedEvent event) {
/*znode路径下节点状态变更*/
dm.process(event);
}
/**
* 节点数据发生变更
* @param data
*/
public void exists(byte[] data) {
if (data == null) {
/*数据不存在,关闭任务*/
if (child != null) {
System.out.println("Killing process");
child.destroy();
try {
child.waitFor();
} catch (InterruptedException e) {
}
}
child = null;
} else {
if (child != null) {
/*关闭任务*/
System.out.println("Stopping child");
child.destroy();
try {
child.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
/*将数据写入指定文件*/
FileOutputStream fos = new FileOutputStream(filename);
fos.write(data);
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
/*启动任务*/
System.out.println("Starting child");
child = Runtime.getRuntime().exec(exec);
println(child.getInputStream(), System.out);
println(child.getErrorStream(), System.err);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void println(InputStream inputStream, PrintStream printStream) {
new Thread(new Runnable() {
@Override
public void run() {
InputStreamReader inputStreamReader = null;
try {
inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line = null;
while ((line = bufferedReader.readLine()) != null) {
printStream.println(line);
}
} catch (Exception e) {
;
} finally {
if (inputStreamReader != null) {
try {
inputStreamReader.close();
} catch (Exception e) {
;
}
}
}
}
}).start();
}
/**
* 节点无权、过期、不存在等情况
* @param rc the ZooKeeper reason code
*/
public void closing(int rc) {
synchronized (this) {
notifyAll();
}
}
}
- DataMonitor 节点数据变更监视器
package com.xxxx.xxxx.zookeeper;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.Arrays;
import static org.apache.zookeeper.KeeperException.Code;
/**
* 采用异步回调方式(zk.exists)判断获取节点的数据,并进行判断是否和之前的一致,
* 不一致说明变更过,会触发listener.exist,
* 如果发现节点不存在了或者节点无权限或sessiontimeout,则触发listener.close
*
* Created by wushiweijun on 2017/11/20.
*/
public class DataMonitor implements Watcher, AsyncCallback.StatCallback {
private ZooKeeper zk;
private String znode;
private DataMonitorListener listener;
private Watcher chainedWatcher = null;
public boolean dead;
private byte[] prevData;
public DataMonitor(ZooKeeper zk, String znode, Watcher chainedWatcher,
DataMonitorListener listener) {
this.zk = zk;
this.znode = znode;
this.chainedWatcher = chainedWatcher;
this.listener = listener;
// 异步回调方式,判断节点是否存在,如果存在则获取数据,
// 如果对比之前数据发现不一致,则触发listener
zk.exists(znode, true, this, null);
}
/**
* 判断节点状态的异步回调函数
*
* @param rc 返回节点状态
* @param path 节点路径
* @param ctx 上下文,即zk.exists传入的ctx参数
* @param stat 节点当前状态元数据
*/
@Override
public void processResult(int rc, String path, Object ctx, Stat stat) {
boolean exists;
switch (Code.get(rc)) {
case OK:
exists = true;
break;
case NONODE:
exists = false;
break;
case SESSIONEXPIRED:
case NOAUTH:
dead = true;
/*没有权限访问或者session过期,即没有ACL权限,直接关闭*/
listener.closing(rc);
return;
default:
// 重新监听,直至有效,这样就迭代循环了
// 异步回调方式,判断节点是否存在,如果存在则获取数据,
// 如果对比之前数据发现不一致,则触发listener
zk.exists(znode, true, this, null);
return;
}
byte b[] = null;
if (exists) {
try {
/*
* 如果存在, 则获取指定znode路径的数据,
* 如果watch=true,则会在获取数据的同时,放一个哨兵到znode,下次有变更,则会触发
*/
b = zk.getData(znode, false, null);
} catch (KeeperException e) {
// We don't need to worry about recovering now. The watch
// callbacks will kick off any exception handling
e.printStackTrace();
} catch (InterruptedException e) {
return;
}
}
if ((b == null && b != prevData)
|| (b != null && !Arrays.equals(prevData, b))) {
/*如果数据和上次不一样,改变了*/
listener.exists(b);
prevData = b;
}
}
@Override
public void process(WatchedEvent event) {
String path = event.getPath();
/*判断时间类型*/
if (event.getType() == Event.EventType.None) {
// 链接状态发生改变
switch (event.getState()) {
case SyncConnected:
// In this particular example we don't need to do anything
// here - watches are automatically re-registered with
// server and any watches triggered while the client was
// disconnected will be delivered (in order of course)
break;
case Expired:
// 链接失效了
dead = true;
listener.closing(Code.SESSIONEXPIRED.intValue());
break;
}
} else {
if (path != null && path.equals(znode)) {
// 异步回调方式,判断节点是否存在,如果存在则获取数据,
// 如果对比之前数据发现不一致,则触发listener
zk.exists(znode, true, this, null);
}
}
if (chainedWatcher != null) {
chainedWatcher.process(event);
}
}
public interface DataMonitorListener {
/**
* 节点数据发生变更
*/
void exists(byte data[]);
/**
* zookeeper的会话过期或者没有节点访问权限
*
* @param rc the ZooKeeper reason code
*/
void closing(int rc);
}
}
3、案例测试
- 启动Executor
- 用终端启动一个zk客户端
zkCli.sh -server localhost:2181
- 创建节点
[zk: localhost:2181(CONNECTED) 1] create /test test
Created /test
响应
Stopping child
Starting child
test
- 修改节点数据
[zk: localhost:2181(CONNECTED) 2] set /test test1
cZxid = 0x200000005
ctime = Tue Nov 21 15:17:49 CST 2017
mZxid = 0x200000008
mtime = Tue Nov 21 15:19:50 CST 2017
pZxid = 0x200000005
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
响应
Stopping child
Starting child
test1