Zookeeper系列——4Zookeeper的Watcher机制原理分析
yuyutoo 2024-10-16 15:46 2 浏览 0 评论
CSDN地址:https://blog.csdn.net/Eclipse_2019/article/details/126400812
学习目标
- 理解Zookeeper的watcher机制原理
第1章 客户端注册监听
zookeeper的watcher机制注册监听流程图由于在头条上显示模糊,所以不再上传,请参考CSDN上的同名文章。
先简单看看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 the Xid for the packet yet. It is
// generated later at send-time, by an implementation of ClientCnxnSocket::doIO(),
// where the packet is actually sent.
synchronized (outgoingQueue) {
//构建一个Packet对象
packet = new Packet(h, r, request, response, watchRegistration);
packet.cb = cb;
packet.ctx = ctx;
packet.clientPath = clientPath;
packet.serverPath = serverPath;
if (!state.isAlive() || closing) {
conLossPacket(packet);
} else {
// If the client is asking to close the session then
// mark as closing
if (h.getType() == OpCode.closeSession) {
closing = true;
}
//添加到outgoingQueue,这里很显然又是一个生产者消费者模式。
outgoingQueue.add(packet);
}
}
//唤醒阻塞在selector.select上的线程
sendThread.getClientCnxnSocket().wakeupCnxn();
return packet;
}
1.3.3 SendThread.run
在Zookeeper这个对象初始化的时候,启动了一个SendThread,这个线程会从outgoingQueue中获取任务,然后发送到服务端处理。这块内容实际上上文已经讲过了。
public void run() {
clientCnxnSocket.introduce(this,sessionId);
clientCnxnSocket.updateNow();
clientCnxnSocket.updateLastSendAndHeard();
int to;
long lastPingRwServer = Time.currentElapsedTime();
final int MAX_SEND_PING_INTERVAL = 10000; //10 seconds
InetSocketAddress serverAddress = null;
while (state.isAlive()) {//如果是存活状态
try {
if (!clientCnxnSocket.isConnected()) {//如果不是连接状态,则需要进行连接的建立
if(!isFirstConnect){
try {
Thread.sleep(r.nextInt(1000));
} catch (InterruptedException e) {
LOG.warn("Unexpected exception", e);
}
}
// don't re-establish connection if we are closing
if (closing || !state.isAlive()) {
break;
}
if (rwServerAddress != null) {
serverAddress = rwServerAddress;
rwServerAddress = null;
} else {
serverAddress = hostProvider.next(1000);
}
startConnect(serverAddress);//开启连接
clientCnxnSocket.updateLastSendAndHeard();//更新最近一次发送和心跳的时间
}
...
//这里就是核心的处理逻辑,真正进行网络传输;
//pendingQueue表示已经发送出去的数据需要等待server返回的packet队列
//outgoingQueue是等待发送出去的packet队列
clientCnxnSocket.doTransport(to, pendingQueue, outgoingQueue, ClientCnxn.this);
} catch (Throwable e) {
...
}
}
...
}
1.3.4 doTransport
调用协议层进行数据传输。
void doTransport(int waitTimeOut, List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue,
ClientCnxn cnxn)
throws IOException, InterruptedException {
...
for (SelectionKey k : selected) {
SocketChannel sc = ((SocketChannel) k.channel());
if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) {
if (sc.finishConnect()) {
//建立连接事件
updateLastSendAndHeard();
sendThread.primeConnection();
}
} else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
//读写事件
doIO(pendingQueue, outgoingQueue, cnxn);
}
}
...
}
1.3.5 doIO
void doIO(List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue, ClientCnxn cnxn)
throws InterruptedException, IOException {
...
//写请求
if (sockKey.isWritable()) {
synchronized(outgoingQueue) {
//在队列里面找到可以发送的packet
Packet p = findSendablePacket(outgoingQueue,
cnxn.sendThread.clientTunneledAuthenticationInProgress());
if (p != null) {
updateLastSend();
//如果Packet的byteBuffer没有创建,那么就创建
if (p.bb == null) {
if ((p.requestHeader != null) &&
(p.requestHeader.getType() != OpCode.ping) &&
(p.requestHeader.getType() != OpCode.auth)) {
p.requestHeader.setXid(cnxn.getXid());
}
p.createBB();
}
sock.write(p.bb);
if (!p.bb.hasRemaining()) {
sentCount++;
outgoingQueue.removeFirstOccurrence(p);//从待发送队列中移除
if (p.requestHeader != null//判断数据包的请求,ping以及auth不加入待回复队列
&& p.requestHeader.getType() != OpCode.ping
&& p.requestHeader.getType() != OpCode.auth) {
synchronized (pendingQueue) {//添加到pendingQueue待回复队列
pendingQueue.add(p);
}
}
}
}
...
}
}
}
1.3.6 总结
到目前为止,我们已经分析了客户端请求的发送流程,我们来画一个简单的流程图梳理一下
第2章 服务端接收
服务端接收这块内容在本文中只做简单介绍,详细的讲解都在上文中。
2.1 前置流程
1、通过AcceptedThread去接收请求——>执行run方法——>select——>doAccept——>将当前连接分配给selectThread线程——>selectThread.run——>select——>handleIO——>workerPool.schedule——>通过workThread去执行ScheduledWorkRequest任务——>ScheduledWorkRequest.run——>IOWorkRequest.doWork——>doIO——>readPayload——>readRequest——>processPacket——>submitRequest——>执行业务链路——>最开始调用PreRequestProcessor.run方法——>pRequest
pRequest方法比较长,主要逻辑就是根据不同的请求类型实现不同的操作。在代码中我们可以看到对于 exists 请求,没有特别的逻辑处理。
2、然后逻辑会执行到SyncRequestProcessor.processRequest
这个 processor负责把写request持久化到本地磁盘,为了提高写磁盘的效率,这里使用的是缓冲写,但是会周期性(1000个request)的调用flush操作,flush之后request已经确保写到磁盘了.同时他还要维护本机的txnlog和snapshot,这里的基本逻辑是:
每隔snapCount/2个request会重新生成一个snapshot并滚动一次txnlog,同时为了避免所有的zookeeper server在同一个时间生成snapshot和滚动日志,这里会再加上一个随机数,snapCount的默认值是100000个request
2.2 FinalRequestProcessor
上面的逻辑走完会进入到FinalRequestProcessor,这个是最终的一个处理器,主要负责把已经commit的写操作应用到本机,对于读操作则从本机中读取数据并返回给client
2.3 statNode
按照前面我们讲过的原理,statNode应该会做两个事情
- 获取指定节点的元数据
- 保存针对该节点的事件监听
注意,在这个方法中,将ServerCnxn向上转型为Watcher了。
public Stat statNode(String path, Watcher watcher)
throws KeeperException.NoNodeException {
Stat stat = new Stat();
//根据path获取节点数据
DataNode n = nodes.get(path);
//如果watcher不为空,则将当前的watcher和path进行绑定
if (watcher != null) {
//增加watch
dataWatches.addWatch(path, watcher);
}
if (n == null) {
throw new KeeperException.NoNodeException();
}
synchronized (n) {
//copy属性设置到stat中
n.copyStat(stat);
return stat;
}
}
2.4 addWatch
通过WatchManager来保存指定节点的事件监听,WatchManager维护了两个集合。
private final HashMap<String, HashSet<Watcher>> watchTable =
new HashMap<String, HashSet<Watcher>>();
private final HashMap<Watcher, HashSet<String>> watch2Paths =
new HashMap<Watcher, HashSet<String>>();
watchTable表示从节点路径到watcher集合的映射
而watch2Paths则表示从watcher到所有节点路径集合的映射。
synchronized void addWatch(String path, Watcher watcher) {
//存储指定path对应的watcher,一个path可以存在多个客户端进行watcher,所以保存了一个set集合
HashSet<Watcher> list = watchTable.get(path);
//判断watcherTable中是否存在当前路径对应的watcher
if (list == null) {//如果为空,说明针对当前节点的watcher还不存在,则进行初始化。
///如果节点上的watcher很少,就不要浪费内存,只添加4个长度,后续进行扩容
list = new HashSet<Watcher>(4);
watchTable.put(path, list);
}
list.add(watcher);//把watcher(对应的是一个ServerCnxn)保存到list中。
//watcher到节点的映射关系表
HashSet<String> paths = watch2Paths.get(watcher);
if (paths == null) {//如果为空,则初始化并保存
// cnxns typically have many watches, so use default cap here
paths = new HashSet<String>();
watch2Paths.put(watcher, paths);
}
//3.6版本之后在这里会设置watch的模式
// watch 有三种类型,一种是PERSISTENT、一种是PERSISTENT_RECURSIVE、STANDARD,
// 前者是持久化订阅,后者是持久化递归订阅,所谓递归订阅就是针对监听的节点的子节点的变化都会触发监听
//todo watcherModeManager.setWatcherMode(watcher, path, watcherMode);
//将path保存到集合
paths.add(path);
}
2.5 返回处理结果
在FinalRequestProcessor的processRequest方法中,将处理结果rsp返回给客户端。
try {
//服务端将请求返回,这时客户端会收到服务端响应
cnxn.sendResponse(hdr, rsp, "response");
if (request.type == OpCode.closeSession) {
cnxn.sendCloseSession();
}
} catch (IOException e) {
LOG.error("FIXMSG",e);
}
最终结果被客户端的接收
2.6 总结
调用关系链如下
第3章 客户端收到请求
客户端接收请求的处理是在ClientCnxnSocketNIO的doIO中,之前客户端发起请求是写,现在客户端收到请求,则是一个读操作,也就是当客户端收到服务端的数据时会触发一下代码的执行。其中很关键的是 sendThread.readResponse(incomingBuffer); 来接收服务端的请求。
if (sockKey.isReadable()) {
int rc = sock.read(incomingBuffer);
if (!incomingBuffer.hasRemaining()) {
incomingBuffer.flip();
if (incomingBuffer == lenBuffer) {
recvCount++;
readLength();
} else if (!initialized) {
//之前建立连接的时候执行这个
readConnectResult();
enableRead();
...
} else {
//现在注册监听执行这个
sendThread.readResponse(incomingBuffer);
lenBuffer.clear();
incomingBuffer = lenBuffer;
updateLastHeard();
}
}
}
3.1 readResponse
这个方法里面主要的流程如下
- 首先读取header,如果其xid == -2,表明是一个ping的response,return
- 如果xid是 -4 ,表明是一个AuthPacket的response return
- 如果xid是 -1,表明是一个notification,此时要继续读取并构造一个enent,通过EventThread.queueEvent发送,return
- 其它情况下:从pendingQueue拿出一个Packet,校验后更新packet信息
对于exists请求,返回的xid=1,则进入到其他情况来处理
void readResponse(ByteBuffer incomingBuffer) throws IOException {
ByteBufferInputStream bbis = new ByteBufferInputStream(
incomingBuffer);
BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
ReplyHeader replyHdr = new ReplyHeader();
replyHdr.deserialize(bbia, "header");
if (replyHdr.getXid() == -2) {
// -2 is the xid for pings
...
}
if (replyHdr.getXid() == -4) {
// -4 is the xid for AuthPacket
...
}
if (replyHdr.getXid() == -1) {
// -1 means notification
...
}
...
Packet packet;
//pendingQueue中存储的是客户端传递过去的数据包packet
synchronized (pendingQueue) {
if (pendingQueue.size() == 0) {
throw new IOException("Nothing in the queue, but got "
+ replyHdr.getXid());
}
//表示这个请求包已经处理完成,直接移除
packet = pendingQueue.remove();
}
/*
* Since requests are processed in order, we better get a response
* to the first request!
*/
try {
//确保是同一个id
if (packet.requestHeader.getXid() != replyHdr.getXid()) {
packet.replyHeader.setErr(
KeeperException.Code.CONNECTIONLOSS.intValue());
throw new IOException("Xid out of order. Got Xid "
+ replyHdr.getXid() + " with err " +
+ replyHdr.getErr() +
" expected Xid "
+ packet.requestHeader.getXid()
+ " for a packet with details: "
+ packet );
}
//把服务端返回的头信息设置到packet中
packet.replyHeader.setXid(replyHdr.getXid());
packet.replyHeader.setErr(replyHdr.getErr());
packet.replyHeader.setZxid(replyHdr.getZxid());
if (replyHdr.getZxid() > 0) {
lastZxid = replyHdr.getZxid();
}
//反序列化返回的消息体
if (packet.response != null && replyHdr.getErr() == 0) {
packet.response.deserialize(bbia, "response");
}
if (LOG.isDebugEnabled()) {
LOG.debug("Reading reply sessionid:0x"
+ Long.toHexString(sessionId) + ", packet:: " + packet);
}
} finally {
//调用finishPacket完成消息的处理
finishPacket(packet);
}
}
3.2 finishPacket
通过前面客户端和服务端的交互,可以确定服务端已经成功保存了watcher这个事件,那么受到服务端的确认之后,客户端会把这个watcher保存到本地的事件中。
所以,finishPacket主要功能是把从 Packet 中取出对应的 Watcher 并注册到 ZKWatchManager中去
private void finishPacket(Packet p) {
if (p.watchRegistration != null) {
//将事件注册到zkwatchemanager中
//watchRegistration,在组装请求的时候,我们初始化了这个对象
//把watchRegistration 子类里面的 Watcher 实例放到 ZKWatchManager 的 existsWatches 中存储起来。
p.watchRegistration.register(p.replyHeader.getErr());
}
//cb就是AsnycCallback,如果为null,表明是同步调用的接口,不需要异步回掉,因此,直接notifyAll即可。
//这里唤醒的就是在客户端调用exists方法中,wait()的逻辑,这样表示服务处理完成。
if (p.cb == null) {
synchronized (p) {
p.finished = true;
p.notifyAll();
}
} else {
p.finished = true;
eventThread.queuePacket(p);
}
}
3.3 register
把path对应的watcher本地回调保存到一个集合中。
public void register(int rc) {
if (shouldAddWatch(rc)) {//根据返回的code来决定是否需要添加watch
Map<String, Set<Watcher>> watches = getWatches(rc);
synchronized(watches) {//初始化watches集合
Set<Watcher> watchers = watches.get(clientPath);
if (watchers == null) {
watchers = new HashSet<Watcher>();
watches.put(clientPath, watchers);
}
//把watcher保存到watches集合,此时的watcher对应的就是在exists方法中传入的匿名内部类。
watchers.add(watcher);
}
}
}
3.4 ZkWatchManager
ZkWatchManager是客户端这边用来保存本地节点对应的watcher回调的管理类,提供了三种不同的事件管理机制。
protected Map<String, Set<Watcher>> getWatches(int rc) {
return rc == 0 ? watchManager.dataWatches : watchManager.existWatches;
}
private static class ZKWatchManager implements ClientWatchManager {
private final Map<String, Set<Watcher>> dataWatches =
new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> existWatches =
new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> childWatches =
new HashMap<String, Set<Watcher>>();
}
总的来说,当使用ZooKeeper 构造方法或者使用 getData、exists 和 getChildren 三个接口来向ZooKeeper 服务器注册 Watcher 的时候,首先将此消息传递给服务端,传递成功后,服务端会通知客户端,然后客户端将该路径和Watcher对应关系存储起来备用。
第4章 事件触发
前面这么长的说明,只是为了清晰的说明事件的注册流程,最终的触发,还得需要通过事务型操作来完成,在我们最开始的案例中,通过如下代码去完成了事件的触发
zookeeper.setData("/watch", "1".getBytes(),-1) ;
前面的客户端和服务端对接的流程就不再重复讲解了,交互流程是一样的,唯一的差别在于事件触发了
4.1 服务端的事件响应
服务端收到setData请求时,会进入到FinalRequestProcessor这个类中 ProcessTxnResult rc = zks.processTxn(request);
4.1.1 DataTree.setData
从该方法一直进入到DataTree.setData这个方法。
public Stat setData(String path, byte data[], int version, long zxid,
long time) throws KeeperException.NoNodeException {
Stat s = new Stat();
//得到节点数据
DataNode n = nodes.get(path);
if (n == null) {
throw new KeeperException.NoNodeException();
}
byte lastdata[] = null;
//修改节点数据
synchronized (n) {
lastdata = n.data;
n.data = data;
n.stat.setMtime(time);
n.stat.setMzxid(zxid);
n.stat.setVersion(version);
n.copyStat(s);
}
// now update if the path is in a quota subtree.
String lastPrefix = getMaxPrefixWithQuota(path);
if(lastPrefix != null) {
this.updateBytes(lastPrefix, (data == null ? 0 : data.length)
- (lastdata == null ? 0 : lastdata.length));
}
//触发NodeDataChanged事件
dataWatches.triggerWatch(path, EventType.NodeDataChanged);
return s;
}
4.1.2 triggerWatch
Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
//根据类型、连接状态、路径,构建WatchedEvent
WatchedEvent e = new WatchedEvent(type,
KeeperState.SyncConnected, path);
HashSet<Watcher> watchers;
synchronized (this) {
//根据path获取watcher
watchers = watchTable.remove(path);
if (watchers == null || watchers.isEmpty()) {
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG,
ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"No watchers for " + path);
}
return null;
}
for (Watcher w : watchers) {
HashSet<String> paths = watch2Paths.get(w);
if (paths != null) {
paths.remove(path);
}
}
}
for (Watcher w : watchers) {
if (supress != null && supress.contains(w)) {
continue;
}
//遍历watchers,循环处理事件
w.process(e);
}
return watchers;
}
4.1.3 w.process
还记得我们在服务端绑定事件的时候,watcher绑定是是什么?是ServerCnxn, 所以w.process(e),其实调用的应该是ServerCnxn的process方法。而servercnxn又是一个抽象方法,有两个实现类,分别是:NIOServerCnxn和NettyServerCnxn。那接下来我们扒开NIOServerCnxn这个类的process方法看看究竟
public void process(WatchedEvent event) {
ReplyHeader h = new ReplyHeader(-1, -1L, 0);
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"Deliver event " + event + " to 0x"
+ Long.toHexString(this.sessionId)
+ " through " + this);
}
// Convert WatchedEvent to a type that can be sent over the wire
WatcherEvent e = event.getWrapper();
//把事件对象WatcherEvent返回给客户端
sendResponse(h, e, "notification");
}
4.2 客户端的事件处理
客户端收到请求,仍然执行SendThread.readResponse,此时的消息通知类型的xid=-1,所以需要进入到-1的分支进行判断
if (replyHdr.getXid() == -1) {
// -1 means notification
if (LOG.isDebugEnabled()) {
LOG.debug("Got notification sessionid:0x"
+ Long.toHexString(sessionId));
}
WatcherEvent event = new WatcherEvent();
event.deserialize(bbia, "response");
// convert from a server path to a client path
if (chrootPath != null) {
String serverPath = event.getPath();
if(serverPath.compareTo(chrootPath)==0)
event.setPath("/");
else if (serverPath.length() > chrootPath.length())
event.setPath(serverPath.substring(chrootPath.length()));
else {
LOG.warn("Got server path " + event.getPath()
+ " which is too short for chroot path "
+ chrootPath);
}
}
WatchedEvent we = new WatchedEvent(event);
if (LOG.isDebugEnabled()) {
LOG.debug("Got " + we + " for sessionid 0x"
+ Long.toHexString(sessionId));
}
eventThread.queueEvent( we );
return;
}
4.2.1 queueEvent
SendThread 接收到服务端的通知事件后,会通过调用 EventThread 类的 queueEvent 方法将事件传给 EventThread 线程,queueEvent 方法根据该通知事件,从 ZKWatchManager 中取出所有相关的Watcher,如果获取到相应的Watcher,就会让Watcher移除失效。
public void queueEvent(WatchedEvent event) {
if (event.getType() == EventType.None
&& sessionState == event.getState()) {
return;
}
sessionState = event.getState();
// materialize the watchers based on the event
WatcherSetEventPair pair = new WatcherSetEventPair(
watcher.materialize(event.getState(), event.getType(),
event.getPath()),
event);
// queue the pair (watch set & event) for later processing
waitingEvents.add(pair);
}
4.2.2 materialize
通过dataWatches或者existWatches或者childWatches的remove取出对应的watch,表明客户端watch也是注册一次就移除
同时需要根据keeperState、eventType和path返回应该被通知的Watcher集合
public Set<Watcher> materialize(Watcher.Event.KeeperState state,
Watcher.Event.EventType type,
String clientPath)
{
Set<Watcher> result = new HashSet<Watcher>();
switch (type) {
case None:
result.add(defaultWatcher);
boolean clear = ClientCnxn.getDisableAutoResetWatch() &&
state != Watcher.Event.KeeperState.SyncConnected;
synchronized(dataWatches) {
for(Set<Watcher> ws: dataWatches.values()) {
result.addAll(ws);
}
if (clear) {
dataWatches.clear();
}
}
synchronized(existWatches) {
for(Set<Watcher> ws: existWatches.values()) {
result.addAll(ws);
}
if (clear) {
existWatches.clear();
}
}
synchronized(childWatches) {
for(Set<Watcher> ws: childWatches.values()) {
result.addAll(ws);
}
if (clear) {
childWatches.clear();
}
}
return result;
case NodeDataChanged:
case NodeCreated:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
synchronized (existWatches) {
addTo(existWatches.remove(clientPath), result);
}
break;
case NodeChildrenChanged:
synchronized (childWatches) {
addTo(childWatches.remove(clientPath), result);
}
break;
case NodeDeleted:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
// XXX This shouldn't be needed, but just in case
synchronized (existWatches) {
Set<Watcher> list = existWatches.remove(clientPath);
if (list != null) {
addTo(list, result);
LOG.warn("We are triggering an exists watch for delete! Shouldn't happen!");
}
}
synchronized (childWatches) {
addTo(childWatches.remove(clientPath), result);
}
break;
default:
String msg = "Unhandled watch event type " + type
+ " with state " + state + " on path " + clientPath;
LOG.error(msg);
throw new RuntimeException(msg);
}
return result;
}
4.2.3 waitingEvents.add
waitingEvents是EventThread这个线程中的阻塞队列,很明显,又是在我们第一步操作的时候实例化的一个线程。
从名字可以指导,waitingEvents 是一个待处理 Watcher 的队列,EventThread 的 run() 方法会不断从队列中取数据,交由 processEvent 方法处理:
public void run() {
try {
isRunning = true;
while (true) {
Object event = waitingEvents.take();
if (event == eventOfDeath) {
wasKilled = true;
} else {
processEvent(event);
}
if (wasKilled)
synchronized (waitingEvents) {
if (waitingEvents.isEmpty()) {
isRunning = false;
break;
}
}
}
} catch (InterruptedException e) {
LOG.error("Event thread exiting due to interruption", e);
}
LOG.info("EventThread shut down for session: 0x{}",
Long.toHexString(getSessionId()));
}
4.2.4 processEvent
private void processEvent(Object event) {
try {
if (event instanceof WatcherSetEventPair) {//判断事件类型
// each watcher will process the event
WatcherSetEventPair pair = (WatcherSetEventPair) event;//得到watcherseteventPair
for (Watcher watcher : pair.watchers) {//拿到符合触发机制的所有watcher列表,循环进行调用
try {
watcher.process(pair.event);//调用客户端的回调process
} catch (Throwable t) {
LOG.error("Error while calling watcher ", t);
}
}
}
}
}
下文预告
- Zookeeper集群中的Leader选举
- Zookeeper集群中的数据同步
相关推荐
- jQuery VS AngularJS 你更钟爱哪个?
-
在这一次的Web开发教程中,我会尽力解答有关于jQuery和AngularJS的两个非常常见的问题,即jQuery和AngularJS之间的区别是什么?也就是说jQueryVSAngularJS?...
- Jquery实时校验,指定长度的「负小数」,小数位未满末尾补0
-
在可以输入【负小数】的输入框获取到焦点时,移除千位分隔符,在输入数据时,实时校验输入内容是否正确,失去焦点后,添加千位分隔符格式化数字。同时小数位未满时末尾补0。HTML代码...
- 如何在pbootCMS前台调用自定义表单?pbootCMS自定义调用代码示例
-
要在pbootCMS前台调用自定义表单,您需要在后台创建表单并为其添加字段,然后在前台模板文件中添加相关代码,如提交按钮和表单验证代码。您还可以自定义表单数据的存储位置、添加文件上传字段、日期选择器、...
- 编程技巧:Jquery实时验证,指定长度的「负小数」
-
为了保障【负小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【负小数】的方法。HTML代码<inputtype="text"class="forc...
- 一篇文章带你用jquery mobile设计颜色拾取器
-
【一、项目背景】现实生活中,我们经常会遇到配色的问题,这个时候去百度一下RGB表。而RGB表只提供相对于的颜色的RGB值而没有可以验证的模块。我们可以通过jquerymobile去设计颜色的拾取器...
- 编程技巧:Jquery实时验证,指定长度的「正小数」
-
为了保障【正小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【正小数】的方法。HTML做成方法<inputtype="text"class="fo...
- jquery.validate检查数组全部验证
-
问题:html中有多个name[],每个参数都要进行验证是否为空,这个时候直接用required:true话,不能全部验证,只要这个数组中有一个有值就可以通过的。解决方法使用addmethod...
- Vue进阶(幺叁肆):npm查看包版本信息
-
第一种方式npmviewjqueryversions这种方式可以查看npm服务器上所有的...
- layui中使用lay-verify进行条件校验
-
一、layui的校验很简单,主要有以下步骤:1.在form表单内加上class="layui-form"2.在提交按钮上加上lay-submit3.在想要校验的标签,加上lay-...
- jQuery是什么?如何使用? jquery是什么功能组件
-
jQuery于2006年1月由JohnResig在BarCampNYC首次发布。它目前由TimmyWilson领导,并由一组开发人员维护。jQuery是一个JavaScript库,它简化了客户...
- django框架的表单form的理解和用法-9
-
表单呈现...
- jquery对上传文件的检测判断 jquery实现文件上传
-
总体思路:在前端使用jquery对上传文件做部分初步的判断,验证通过的文件利用ajaxFileUpload上传到服务器端,并将文件的存储路径保存到数据库。<asp:FileUploadI...
- Nodejs之MEAN栈开发(四)-- form验证及图片上传
-
这一节增加推荐图书的提交和删除功能,来学习node的form提交以及node的图片上传功能。开始之前需要源码同学可以先在git上fork:https://github.com/stoneniqiu/R...
- 大数据开发基础之JAVA jquery 大数据java实战
-
上一篇我们讲解了JAVAscript的基础知识、特点及基本语法以及组成及基本用途,本期就给大家带来了JAVAweb的第二个知识点jquery,大数据开发基础之JAVAjquery,这是本篇文章的主要...
- 推荐四个开源的jQuery可视化表单设计器
-
jquery开源在线表单拖拉设计器formBuilder(推荐)jQueryformBuilder是一个开源的WEB在线html表单设计器,开发人员可以通过拖拉实现一个可视化的表单。支持表单常用控件...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)