学习目标
-
理解Zookeeper的watcher机制原理
第1章 客户端注册监听
二话不说,先贴上总体流程图
再来看看Watcher的使用
ZooKeeper 的 Watcher 机制,总的来说可以分为三个过程:客户端注册 Watcher、服务器处理Watcher 和客户端回调 Watcher
客户端注册watcher有3种方式,getData、exists、getChildren;以如下代码为例来分析整个触发机制的原理
public class Demo01 {
public static void main(String[] args) throws KeeperException, InterruptedException, IOException {
ZooKeeper zookeeper=new ZooKeeper("127.0.0.1:2181",4000,new Watcher(){
@Override
public void process(WatchedEvent event) {
System.out.println("event.type:"+event.getType());
}
});
zookeeper.create("/watch","0".getBytes(), ZooDefs.Ids. OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT); //创建节点
zookeeper.exists("/watch",true); //注册监听
Thread.sleep(1000);
zookeeper.setData("/watch", "1".getBytes(),-1) ; //修改节点的值触发监听
}
}
持久化监听
从zookeeper3.6开始,提供了持久化监听以及递归监听机制,演示如下(我本地的zookeeper版本为3.4.13的和3.5.6,所以这块就不做演示了)
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.1.0</version>
</dependency>
<dependency><!--分布式锁、leader选举、队列...-->
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.1.0</version>
</dependency>
ZooKeeper zooKeeper=new ZooKeeper("localhost:2181", 5000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
//表示连接成功之后,会产生的回调时间
}
});
zooKeeper.addWatch("/first1",watchedEvent -> {
System.out.println(watchedEvent.getPath());
},AddWatchMode.PERSISTENT);
其实很多流程在前面两篇文章已经讲过了,为了大家印象更深刻以及思路更清晰,有些流程在本文不做太详细的介绍。
1.1 建立连接
ZooKeeper zookeeper=new ZooKeeper("127.0.0.1:2181") , Zookeeper在初始化的时候,会构建一个Watcher,我们可以先看看Zookeeper初始化做了什么事情。
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
boolean canBeReadOnly) throws IOException{
//在这里将watcher设置到ZKWatchManager
watchManager.defaultWatcher = watcher;
ConnectStringParser connectStringParser = new ConnectStringParser(
connectString);
HostProvider hostProvider = new StaticHostProvider(
connectStringParser.getServerAddresses());
//初始化了ClientCnxn,在这里会创建一个sendThread和eventThread线程
cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
hostProvider, sessionTimeout, this, watchManager,
getClientCnxnSocket(), canBeReadOnly);
//启动一个sendThread线程进行通信还有eventThread进行事件通知
cnxn.start();
}
cnxn.start();应该比较熟悉了吧,这个方法里面会启动一个sendThread线程进行通信还有eventThread进行事件通知,然后通过sendThread线程去建立连接,这一步的逻辑在上文中已经着重讲过,这里不再赘述。
1.2 创建节点
这一步是通过create方法区完成,在create方法中我们会发现,实际上他就是调用的sumbitRequest,然后在底层也是调用sendThread发送请求到服务端。
public String create(final String path, byte data[], List<ACL> acl,
CreateMode createMode)
throws KeeperException, InterruptedException
{
...
request.setAcl(acl);
//这一步也眼熟的很,是上文中介绍过的请求处理,实际上进入底层你会发现依然是通过sendThread去发送请求了。
ReplyHeader r = cnxn.submitRequest(h, request, response, null);
if (r.getErr() != 0) {
throw KeeperException.create(KeeperException.Code.get(r.getErr()),
clientPath);
}
if (cnxn.chrootPath == null) {
return response.getPath();
} else {
return response.getPath().substring(cnxn.chrootPath.length());
}
}
1.3 注册监听
exists是用来判断一个节点是否存在,同时,还会针对这个节点注册一个watcher事件。
public Stat exists(final String path, Watcher watcher)
throws KeeperException, InterruptedException
{
final String clientPath = path;
PathUtils.validatePath(clientPath);
// the watch contains the un-chroot path
WatchRegistration wcb = null;
if (watcher != null) {
wcb = new ExistsWatchRegistration(watcher, clientPath);
}
final String serverPath = prependChroot(clientPath);
//构建请求头
RequestHeader h = new RequestHeader();
//表示当前请求的操作类型是exists
h.setType(ZooDefs.OpCode.exists);
//构建发送请求和响应的response
ExistsRequest request = new ExistsRequest();
request.setPath(serverPath);
request.setWatch(watcher != null);
SetDataResponse response = new SetDataResponse();
//通过submitRequest发送请求
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
if (r.getErr() != 0) {
if (r.getErr() == KeeperException.Code.NONODE.intValue()) {
return null;
}
throw KeeperException.create(KeeperException.Code.get(r.getErr()),
clientPath);
}
//返回stat元数据
return response.getStat().getCzxid() == -1 ? null : response.getStat();
}
发现了吧,其实不管是什么操作在zk里面都是这么操作的,调用submitRequest,接下来我们注重分析一下这个方法
1.3.1 submitRequest
这里面的处理逻辑比较简单
-
调用queuePacket,把请求数据添加到队列
-
通过packet.wait使得当前线程一直阻塞,直到请求完成
public ReplyHeader submitRequest(RequestHeader h, Record request,
Record response, WatchRegistration watchRegistration)
throws InterruptedException {
ReplyHeader r = new ReplyHeader();
//把请求数据添加到队列
Packet packet = queuePacket(h, r, request, response, null, null, null,
null, watchRegistration);
// 等到请求执行完成
synchronized (packet) {
while (!packet.finished) {
packet.wait();
}
}
return r;
}
1.3.2 queuePacket
在这个方法里面实际上就干了两件事,1、创建一个Packet对象并加入到队列;2、唤醒一个sendThread线程去执行
Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
Record response, AsyncCallback cb, String clientPath,
String serverPath, Object ctx, WatchRegistration watchRegistration)
{
Packet packet = null;
// Note that we do not generate