我们可以通过ZooKeeper通知客户端感兴趣的具体事件来避免客户端通过轮询来获取数据的变动信息。ZooKeeper为znode节点设置监视点(watch),当节点变动的时候会主动通知到客户端。这个特性可以用在很多地方,例如一个拥有主备架构的系统,正常情况下主对外提供服务,当主服务器崩溃或者失去联系后其他备用服务器会自动变成主服务器。这个主备切换的功能就可以通过zookeeper来实现,主服务器在zookeeper上注册一个临时节点,然后其他备用服务器监控该节点,一旦主服务器崩溃或者断开连接,临时节点会自动删除,此时备用服务器就能够第一时间收到通知,进而进行切换操作。
4.1 单次触发器
当应用程序注册了一个监视点来接收通知,匹配该监视点条件的会触发监视点的通知,并且最多只触发一次。客户端设置的每个监视点与会话关联,如果会话过期,等待中的监视点会被删除。不过监视点可以跨越不同服务端的连接而保持。例如,当一个zookeeper客户端与一个ZooKeeper服务端的连接断开后,会连接到集群中的另外一个服务器上,客户端会将未触发的监视点列表发送到新的服务器上,然后再新的服务器上注册监视点,服务端将要检查已监视的znode节点在之前注册监视点之后是否已经变化,如果znode节点已经发生变化,一个监视点的事件的事件就会被发送到客户端,否则在新的服务端上注册监视点。
由于触发器是单次的,也就是说客户端注册了一个监视点,当接收到一个通知后,如果不重新注册该监视点,那么当监视点发生改变时不会再发送通知到客户端,要再次收到通知需要重新注册,而在接收到通知到再次注册监视点的这个过程中如果监视点发生改变客户端就会丢失该事件。不过这种情况下丢失事件不会有什么影响,因为可以通过读取ZooKeeper的状态信息来获得,举个例子:
import java.io.IOException;4.2 如何设置监视点
NodeCreated
通过exists调用设置一个监视点。
通过exists或getData调用设置监视点。
通过exists或getData调用设置监视点。
通过getChildren调用设置监视点。
上面的监视点需要注意,通过getChildren设置的监视点也能够收到NodeDeleted的通知,这是测试后发现的,其他类似情况估计还有,不过我没有进行测试,在开发代码的时候要注意这点。
拿getData举例子,有两种调用方法:
public byte[] getData(final String path, Watcher watcher, Stat stat);
public byte[] getData(String path, boolean watch, Stat stat);
第一种方式可以采用自己实现的Watcher对象,第二种方法通过将watch设置为true(设置为false就不会进项节点监控)来采用默认的Watcher。
这里面的Stat对象需要自己来创建,函数调用结束后该对象中会保存节点相应的数据,比如该节点上次更新时间,当前子节点个数等。getData函数的返回值为节点内容。我们来看一个实例:
import java.io.IOException;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
public class TestMasterTwo {
ZooKeeper zk;
public TestMasterTwo() {
try {
MyWatcher wt = new MyWatcher();
zk = new ZooKeeper("192.168.253.129:2181,192.168.253.130:2181,192.168.253.131:2181", 5000, wt);
} catch (IOException e) {
e.printStackTrace();
}
}
public ZooKeeper GetZooKeeper() {
return zk;
}
public static void main(String args[])throws Exception {
TestMasterTwo tm = new TestMasterTwo();
//先判断/zhulong节点是否存在
Stat exitstat = tm.GetZooKeeper().exists("/zhulong", false);
if(exitstat != null) {
Stat stat = new Stat();
tm.GetZooKeeper().getData("/zhulong", true,stat);
Thread.sleep(600000);
}else {
System.out.println("该节点不存在");
}
}
class MyWatcher implements Watcher{
@Override
public void process(WatchedEvent watchedevent) {
System.out.println("事件触发了"+watchedevent.toString());
try {
if(!watchedevent.getType().equals(Event.EventType.NodeDeleted) && !watchedevent.getType().equals(Event.EventType.None)) {
Stat stat = new Stat();
MyWatcherTwo wttwo = new MyWatcherTwo();
//即使在获取到通知到下面的重新注册之间节点发生改变,也能够通过下面的getChildren获取到最新的节点信息,从而解决了丢失事件导致获取不到最新数据的问题
byte[] bydata = zk.getData("/zhulong", wttwo,stat);
System.out.println("MyWatcher下该节点内容为:"+new String(bydata)+",stat内容为"+stat.toString());
}
} catch (KeeperException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class MyWatcherTwo implements Watcher{
@Override
public void process(WatchedEvent watchedevent) {
System.out.println("事件触发了第二个Watcher"+watchedevent.toString());
try {
if(!watchedevent.getType().equals(Event.EventType.NodeDeleted) && !watchedevent.getType().equals(Event.EventType.None)) {
Stat stat = new Stat();
//即使在获取到通知到下面的重新注册之间节点发生改变,也能够通过下面的getChildren获取到最新的节点信息,从而解决了丢失事件导致获取不到最新数据的问题
byte[] bydata = zk.getData("/zhulong", true,stat);
System.out.println("MyWatcherTwo下该节点内容为:"+new String(bydata)+",stat内容为"+stat.toString());
}
} catch (KeeperException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
一开始我们采用的是默认的Watcher对象,实现类为MyWatcher,在接收到事件通知后将监视器改成了MyWatcherTwo,然后再次接受事件通知,又采用了默认的监视器,所以在修改节点内容的时候两个监视器会交替接收到事件通知,如下图:
目前来看(当前版本为3.4.0),一旦设置了监视点就无法移除,要想移除一个监视点只能通过下面两个方法:
第一个,触发这个监视点
第二个,让该会话被关闭或者过期。
4.4 主-从模式的例子
4.4.1 管理权变化
通过异步调用的回调函数来进行相应的逻辑处理,例如下面的示例:
import java.io.IOException;
import java.util.List;
import org.apache.zookeeper.AsyncCallback.StatCallback;
import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
public class TestMasterThree {
ZooKeeper zk;
public TestMasterThree() {
try {
MyWatcher wt = new MyWatcher();
zk = new ZooKeeper("192.168.253.129:2181,192.168.253.130:2181,192.168.253.131:2181", 5000, wt);
} catch (IOException e) {
e.printStackTrace();
}
}
public ZooKeeper GetZooKeeper() {
return zk;
}
class MyWatcher implements Watcher{
@Override
public void process(WatchedEvent watchedevent) {
System.out.println("事件触发了"+watchedevent.toString());
try {
if(!watchedevent.getType().equals(Event.EventType.NodeDeleted) && !watchedevent.getType().equals(Event.EventType.None)) {
Stat stat = new Stat();
//即使在获取到通知到下面的重新注册之间节点发生改变,也能够通过下面的getChildren获取到最新的节点信息,从而解决了丢失事件导致获取不到最新数据的问题
byte[] bydata = zk.getData("/zhulong", true,stat);
System.out.println(new String(bydata));
}
} catch (KeeperException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void main(String args[])throws Exception {
TestMasterThree tm = new TestMasterThree();
MyWatcher mywtc = tm.new MyWatcher();
StatCallback existsCallback = new StatCallback() {
public void processResult(int rc, String path, Object ctx, Stat stat) {
System.out.println("结果返回了!!rc="+rc+",path="+path+",ctx is "+ctx.toString()+",stat"+stat.toString());
}
};
AsyncCallback.ChildrenCallback childCallback = new AsyncCallback.ChildrenCallback() {
@Override
public void processResult(int rc, String path, Object ctx, List
//System.out.println("结果返回了!!rc="+rc+",path="+path+",ctx is "+ctx.toString()+",childlist is "+childlist.toString());
System.out.println("结果返回了!!rc="+rc+",path="+path+",ctx is "+ctx+",childlist is "+childlist);
}
};
tm.GetZooKeeper().getChildren("/zhulong", mywtc, childCallback, "zzzz");
Thread.sleep(600000);
}
}
注意:
(1)、processResult中的ctx通过在getChildren函数的第四个参数传递
(2)、ctx和childlist都有可能为null,所以在程序处理的时候一定要考虑到这一点,否则针对null的对象进行相应操作将会出现问题。
最终结果如下
4.4.2 主节点等待从节点列表的变化
示例代码如下:
import java.io.IOException;
import java.util.List;
import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
public class TestMasterFive {
ZooKeeper zk;
public TestMasterFive() {
try {
MyWatcher wt = new MyWatcher();
zk = new ZooKeeper("192.168.253.129:2181,192.168.253.130:2181,192.168.253.131:2181", 5000, wt);
} catch (IOException e) {
e.printStackTrace();
}
}
public ZooKeeper GetZooKeeper() {
return zk;
}
class MyWatcher implements Watcher{
@Override
public void process(WatchedEvent watchedevent) {
System.out.println("事件触发了"+watchedevent.toString());
try {
if(!watchedevent.getType().equals(Event.EventType.NodeDeleted) && !watchedevent.getType().equals(Event.EventType.None)) {
Stat stat = new Stat();
//即使在获取到通知到下面的重新注册之间节点发生改变,也能够通过下面的getChildren获取到最新的节点信息,从而解决了丢失事件导致获取不到最新数据的问题
byte[] bydata = zk.getData("/zhulong", true,stat);
System.out.println(new String(bydata));
}
} catch (KeeperException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void getWorkers(TestMasterFive tm,MyWatcher mywtc,AsyncCallback.ChildrenCallback childCallback) {
tm.GetZooKeeper().getChildren("/zhulong", mywtc, childCallback, null);
}
public static void main(String args[])throws Exception {
TestMasterFive tm = new TestMasterFive();
MyWatcher mywtc = tm.new MyWatcher();
AsyncCallback.ChildrenCallback childCallback = new AsyncCallback.ChildrenCallback() {
@Override
public void processResult(int rc, String path, Object ctx, List
switch (Code.get(rc)) {
case CONNECTIONLOSS:
//如果因为连接断开导致获取子节点信息失败了,这里需要重试一下
tm.getWorkers(tm,mywtc,this);}
4.4.3 主节点等待新任务进行分配
与等待从节点列表变化类似,主要节点等待添加到/tasks节点中的新任务。主节点首先获得当前的任务集,并设置变化情况的监视点。在ZooKeeper中,/tasks的子节点表示任务集,每个子节点对应一个任务,一旦主节点获得还未分配的任务信息,主节点会随机选择一个从节点,将这个任务分配给从节点。
对于新任务,主节点选择一个从节点分配任务之后,主节点就会在/assign/work-id节点下创建一个新的znode节点,其中id为从节点标识符,之后主节点从任务列表中删除该任务节点。注意,主节点在成功分配任务后,会删除/tasks节点下对应的任务。这种简化了主节点角色接收新任务并分配的设计,如果任务列表中混合的已分配和未分配的任务,主节点还需要区分这些任务。
4.4.4 从节点等待分配新任务
void register() {
zk.create("/workers/worker-" + serverId,new byte[0],Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL,createWorkerCallback, null);
}
StringCallback createWorkerCallback = new StringCallback() {
public void processResult(int rc, String path, Object ctx, String name) {
switch (Code.get(rc)) {
case CONNECTIONLOSS:
register();
break;
case OK:
System.out.println("Registered successfully: " + serverId);
break;
case NODEEXISTS:
System.out.println("Already registered: " + serverId);
break;
default:
System.out.println("Something went wrong: " + KeeperException.create(Code.get(rc), path));
}
}
};
Watcher newTaskWatcher = new Watcher() {
public void process(WatchedEvent e) {
if(e.getType() == EventType.NodeChildrenChanged) {
assert new String("/assign/worker-"+ serverId).equals( e.getPath() );
getTasks();
}
}
};
void getTasks() {
zk.getChildren("/assign/worker-" + serverId,newTaskWatcher,tasksGetChildrenCallback,null);
}
ChildrenCallback tasksGetChildrenCallback = new ChildrenCallback() {
public void processResult(int rc,String path,Object ctx,List
switch(Code.get(rc)) {
case CONNECTIONLOSS:
getTasks();
break;
case OK:
if(children != null) {
executor.execute(new Runnable() {
List
DataCallback cb;
public Runnable init (List
this.children = children;
this.cb = cb;
return this;
}
public void run() {
System.out.println("Looping into tasks");
synchronized(onGoingTasks) {
for(String task : children) {
if(!onGoingTasks.contains( task )) {
System.out.println("New task: {}"+task);
zk.getData("/assign/worker-" +serverId + "/" + task,false,cb,task);
onGoingTasks.add( task );
}
}
}
}
}
.init(children, taskDataCallback));
}
break;
default:
System.out.println("getChildren failed: " +KeeperException.create(Code.get(rc), path));
}
}
};
4.4.5 客户端等待任务的执行结果
void submitTask(String task, TaskObject taskCtx) {
taskCtx.setTask(task);
zk.create("/tasks/task-",
task.getBytes(),
Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT_SEQUENTIAL,
createTaskCallback,
taskCtx);
}
StringCallback createTaskCallback = new StringCallback() {
public void processResult(int rc, String path, Object ctx, String name) {
switch (Code.get(rc)) {
case CONNECTIONLOSS:
submitTask(((TaskObject) ctx).getTask(),
(TaskObject) ctx);
break;
case OK:
System.out.println("My created task name: " + name);
((TaskObject) ctx).setTaskName(name);
watchStatus("/status/" + name.replace("/tasks/", ""),
ctx);
break;
default:
System.out.println("Something went wrong" +KeeperException.create(Code.get(rc), path));
}
}
};
ConcurrentHashMap
new ConcurrentHashMap
void watchStatus(String path, Object ctx) {
ctxMap.put(path, ctx);
zk.exists(path,
statusWatcher,
existsCallback,
ctx); 1
}
Watcher statusWatcher = new Watcher() {
public void process(WatchedEvent e) {
if(e.getType() == EventType.NodeCreated) {
assert e.getPath().contains("/status/task-");
zk.getData(e.getPath(),
false,
getDataCallback,
ctxMap.get(e.getPath()));
}
}
};
StatCallback existsCallback = new StatCallback() {
public void processResult(int rc, String path, Object ctx, Stat stat) {
switch (Code.get(rc)) {
case CONNECTIONLOSS:
watchStatus(path, ctx);
break;
case OK:
if(stat != null) {
zk.getData(path, false, getDataCallback, null); 2
}
break;
case NONODE:
break; 3
default:
LOG.error("Something went wrong when " +
"checking if the status node exists: " +
KeeperException.create(Code.get(rc), path));
break;
}
}
};
4.5 另一种调用方式: Multiop
Multiop可以原子性地执行多个ZooKeeper的操作,例如用multiop删除一个父节点以及其子节点,结果只有两种情况,要么父节点和子节点都被删除,要么都删除失败,不会出现部分节点删除,部分节点失败的情况。
使用multiop特性:
(1) 创建一个op对象,该对象表示你想通过multiop方法执行的每个ZooKeeper操作,ZooKeeper提供了每个修改状态操作的Op对象的实现:create、delete和setData。
(2)通过Op对象中提供的一个静态方法调用进行操作。
(3)将Op对象添加到java的Iterable类型对象中,如列表(List)。
(4)使用列表对象调用multi方法。
Op deleteZnode(String z) { return Op.delete(z, -1); } List
results = zk.multi(Arrays.asList(deleteZnode("/a/b"),deleteZnode("/a"));
4.7 顺序的保障
4.7.1 写操作的顺序
ZooKeeper状态会在所有服务端所组成的全部安装中进行复制。服务端对状态变化的顺序达成一致,并使用相同的顺序执行状态的更新。例如,如果一个ZooKeeper的服务端先建立一个/z节点之后再删除/z节点,所有的集合中的服务端均需以相同的顺序执行这些变化。每一个服务器都会维护一个节点集合,它们保持同步,也就是说是一模一样的,某个服务端对节点的修改都会同步到其他服务器,而且执行的顺序是一样的,这样才能保证这些服务器上的节点数据相同。所有的服务端并不需要同时执行这些更新,而且事实上也很少这样操作。服务端更可能在不同时间执行状态变化,这些都没有关系,只要执行顺序一样就可以了。
4.7.2 读操作的顺序
·客户端c1更新了/z节点的数据,并收到应答。
·客户端c1通过TCP的直接连接告知客户端c2,/z节点状态发⽣了变
化。
·客户端c2读取/z节点的状态,但是在c1更新之前就观察到了这个状
态。
为了避免这个问题,c2在/z节点设置监视点来代替从c1直接接受消息,通过监控点可以知道该节点变化,从而消除了这个隐患。