ZooKeeper Watcher监听机制(数据变更的通知)(二)(分析)

版权声明: https://blog.csdn.net/Dongguabai/article/details/82986476

紧接着上一篇博客:https://blog.csdn.net/Dongguabai/article/details/82970852

在输出内容中有这样两个结果:

在ZooKeeper中,接口类Watcher用于表示一个标准的事件处理器,其定义了事件通知相关的逻辑,包含KeeperState和EventType两个枚举类,分别代表了通知状态和事件类型,同时定义了事件的回调方法:process(WatchedEvent event)。

那么什么样的操作会产生什么类型的事件呢:

  event For “/path” event For “/path/child”
create(“/path”) EventType.NodeCreatedexists/getData
delete(“/path”) EventType.NodeDeletedexists/getData
setData(“/path”) EventType.NodeDataChangedexists/getData
create(“/path/child”) EventType.NodeChildrenChanged(getChildren) EventType.NodeCreated
delete(“/path/child”) EventType.NodeChildrenChanged(getChildren) EventType.NodeDeleted
setData(“/path/child”) EventType.NodeDataChanged

这里可以简单测试一下:

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;

import java.util.concurrent.CountDownLatch;

/**
 * @author Dongguabai
 * @date 2018/10/9 19:18
 */
public class ZooKeeperWatcherDemo2 {
    static final String CONNECT_ADDR = "192.168.220.135:2181,192.168.220.136:2181,192.168.220.137:2181";
    static final int SESSION_OUTTIME = 2000;//ms
    /**
     * 阻塞程序执行,等待zookeeper连接成功
     */
    static final CountDownLatch connectedSemaphore = new CountDownLatch(1);

    static final String PATH = "/dongguabai";
    static final String PATH_CHILD = PATH + "/child";

    public static void main(String[] args) {
        try {
            //连接
            ZooKeeper zk = new ZooKeeper(CONNECT_ADDR, SESSION_OUTTIME, event -> {
                System.out.println("事件是:" + event.getType());
                //如果收到了服务端的响应事件,连接成功
                if (Watcher.Event.KeeperState.SyncConnected == event.getState()) {
                    connectedSemaphore.countDown();
                }
            });
            connectedSemaphore.await();
            System.out.println("连接成功!");

            //binding
            zk.exists(PATH,event -> {
                System.out.println("create---"+event.getType());
            });
            //create
            zk.create(PATH,"1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

            zk.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果:

再简单测试下:

import org.apache.zookeeper.*;

import java.util.concurrent.CountDownLatch;

/**
 * @author Dongguabai
 * @date 2018/10/9 19:18
 */
public class ZooKeeperWatcherDemo2 {
    static final String CONNECT_ADDR = "192.168.220.135:2181,192.168.220.136:2181,192.168.220.137:2181";
    static final int SESSION_OUTTIME = 2000;//ms
    /**
     * 阻塞程序执行,等待zookeeper连接成功
     */
    static final CountDownLatch connectedSemaphore = new CountDownLatch(1);

    static final String PATH = "/dongguabai";
    static final String PATH_CHILD = PATH + "/child";

    public static void main(String[] args) {
        try {
            //连接
            ZooKeeper zk = new ZooKeeper(CONNECT_ADDR, SESSION_OUTTIME, event -> {
                System.out.println("事件是:" + event.getType());
                //如果收到了服务端的响应事件,连接成功
                if (Watcher.Event.KeeperState.SyncConnected == event.getState()) {
                    connectedSemaphore.countDown();
                }
            });
            connectedSemaphore.await();
            System.out.println("连接成功!");

            //binding
            zk.exists(PATH_CHILD, event -> {
                System.out.println("create path child---" + event.getType());
            });

            zk.exists(PATH, event -> {
                System.out.println("create path---" + event.getType());
            });

            System.out.println("create path");
            //create
            zk.create(PATH, "1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            zk.getChildren(PATH, event -> {
                System.out.println("create getChild---" + event.getType());
            });

            System.out.println("create path child");
            zk.create(PATH_CHILD, "1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

            zk.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里测试的时候遇到了一个关于getChildren()的问题,具体可参看:

https://blog.csdn.net/Dongguabai/article/details/82987931

从网上其他博客中看到的比较好的总结的表格:

事件机制的原理

在上一篇博客也介绍过了,exists()方法是可以注册事件的。这里以exists()方法为例,查看org.apache.zookeeper.ZooKeeper#exists(java.lang.String, org.apache.zookeeper.Watcher):

构建了一个WatchRegistration对象,一个header,一个request和一个response,随后客户端会进行request提交,看看submitRequest()方法:

这个方法会组装一个Packet的数据包,随后会加锁,调用wait()方法进行一个阻塞操作。再来看看queuePacket()方法:

这个Packet是客户端和服务端进行通信的一个最小的数据单元。这里会把相关的参数再组织成为一个Packet对象。随后会将Packet输出队列中去:

最后会触发selector的执行:

简单流程如下:

但是明显这个流程是不完整的,因为这里只有发送,那怎么接收呢。一定会有某种机制对这个队列进行某种数据的处理,即肯定会有个东东去消费OutgoingQueue,入队就肯定有个出队。

先来看看ZooKeeper的构造方法:

经过一层层的调用,最终会到这里,注意这里的start()方法:

会将这个watcher赋值给一个全局的defaultWatcher:

后面会new一个ClientCnxn。之前也介绍过一个cnxn会调用submitRequest方法,那个cnxn就是在这里初始化的。来看看这个:

这里初始化了一些信息,但是也做了两个事情,构造并启动SendThread和EventThread两个线程。那么什么时候启动的呢,在上面提到过的start()方法就是用来启动的:

目前整体流程图为:

貌似没有什么关联,但是随便一想,启动两个线程肯定是要搞点事情的,接下来就看看这两个线程到底做了什么。看看SendThread:

一般我们启动一个线程肯定是调用它的run()方法,这个时候毫不犹豫的去看看这个方法,首先可以猜想这个方法肯定是做一些outgoingQueue出队的相关操作。

这个run()方法特别长,没必要过于关注细节,可以先看看判断:

这里是一个循环,即当状态是存活的时候会进入循环:

再会判断连接状态,如果没有开启连接,就开启连接,同时更新最后一次发送的Heard。

最后一定会生成一个to:

这个to相当于是客户端和服务端一个ping的心跳去维持连接。

如果to小于0的话,意味着会话已经超时了:

往下看,有一个很关键的传输方法,有两种实现,基于NIO或者基于Netty:

那到底走哪一个呢,其实只要把握一点,所有的构造初始化一定是在开始的时候做好的,可以再回到ZooKeeper的构造:

看看这个方法到底做了什么:

首先会从配置文件中读取:

如果是null就会是基于NIO的方式,否则会根据读取到的clientCnxnSocketName通过反射去实例化ClientCxnSocket。即默认是走NIO的方式。

可以看看如果是基于Netty的话是怎么做的,主要就是传输数据包:

可以发现一个很重要的方法:

会从之前介绍的outgoingQueue中取出数据包保存到head中。后面就是一些判断以及错误处理机制等。

再往下看会看到一个关键的方法:

也就是说当前从outgoingQueue中获取到的数据如果不为空的话,就会执行这个doWrite()方法:

又会调用一个sendPkt()方法:

先通过p.createBB(),创建缓冲字节,再通过Netty的Channel进行传输,先看看createBB()方法:

主要会序列两个参数:

然后会把bb(ByteBuffer)封装好。最后发送数据包,自此流程已经走通了:

刚刚只是客户端的数据包发送流程,那服务端是怎么接收数据包的呢,先看看这个类:NettyServerCnxn:

这是处理请求的一个类,里面有这样一个方法receiveMessage():

这里如果bb(ByteBuffer)读完了,会执行一个packetReceived()方法:

看看这个方法,记录了消息递增的一个次数:

接下来会进行消息的相关处理:

这是处理数据包的一个方法:

最后会构建一个服务端的Request,再submitRequest():

看看这个submitRequest()方法:

服务端会有很多的processor,它会通过一个链式的方式把这些processor组装起来。这里firstProcessor为空的那段代码看都不用看,肯定不为空啊,看后面那一段代码:

首先会验证数据包,如果数据包验证成功了,会通过firstProcessor.processRequest()做一个处理,而这里会有很多的实现:

具体是哪一个调用我们就要看firstProsessor对应的是哪个实例。可以看看ZooKeeperServer中firstProsessor是在哪里实例化的:

在PrepRequestProcessor中又传递了一个RequestProcessor:

总得来说,相当于是组装了一个链式的调用。

所以这个时候就先去找PrepRequestProcessor:

把当前的request加到了一个submittedRequests里面:

而这个submittedRequests就是一个队列:

可以发现在ZooKeeper中大量的通过多线程的方式去异步化流程,所以在PreRequestProcessor中一定有一个对应的线程去启动。PreRequestProcessor本身就是一个线程:

一定会有一个run()方法:

会从submittedRequests中去take()请求,随后会经过一系列的判断,循环去处理请求,看看pRequest()方法:

根据一系列的switch语句做一些判断。在这个方法的最后:

会发现有一个nextProcessor又去调用了processRequest,根据前面的规律,肯定回想,这个nextProcessor是在哪里实例化的呢:

这个nextProcessor是通过构造传入的,在刚刚也介绍过,这时候实际上是实例化的一个SyncRequestProcessor对应的processor:

也就是说下一个processor就是SyncRequestProcessor,再去看看SyncRequestProcessor的实现:

跟上面的很类似有木有,也是加到一个队列中:

再通过循环的方式去处理:

这个思想可以去借鉴,即链式调用Thread内部维护的一个Queue,通过循环的方式处理Queue里面的内容,后面会简单写一个小Demo演示这个思想。

在这个run()方法的最后:

又发现了nextProcessor,跟上面是类型的,再看看这个nextProcessor是从哪里构造的:

也是通过构造传入的,这里套路都是一样的,通过构造去传入下一个引用。在刚刚也介绍过,这时候实际上是实例化的一个FinalRequestProcessor对应的processor:

照葫芦画瓢,再看看FinalRequestProcessor的实现:

根据名字也可以知道,这是最后一个processor了。先找我们属性的那一段代码:

这个是处理exists请求的。这里通过socket,通过netty最后把request传到了processor,然后request反序列化后获取到了path。获得了path就好说了,剩下的常规操作,获取节点的Stat:

最后会设置成resp:

最后有一个sendResponse()的方法:

其实在刚刚的获取节点的Stat中还有一个关键点,即绑定Watcer:

注意这里传入的不是Watcher,而是cnxn:

传的是服务端的ServerCnxn。来看看这个statNode()方法:

这里做了一个向上转型,因为ServerCnxn实际上是实现了Watcher:

回过头来继续分析statNode()方法:

如果watcher不为空,就会将path和watcher(实际上是ServerCnxn)添加到dataWatches中,这个就是核心:绑定事件:

看看WatchManager中的addWatch()方法:

之前介绍过,在FinalRequestProcessor的最后会调用一个sendResponse()方法:

有多个实现:

来看看Netty的实现:

看看sendbuffer()方法:

也就是通过channel.write()去发送ByteBuffer,至此,服务端完成了响应。

总体流程图如下:

服务端完成了响应,必然客户端也会去接收,再来看看这个类ClientCnxnSocketNetty,有这样一个方法messageReceived():

这个方法是接收服务端的请求,也是一个异步的过程。

看看readResponse()方法,这个是读取服务端返回的header和response:

首先会反序列化header,然后会判断xid,这里有-1、-2和-4:

官方注解也说的很明白了,-2是一个ping的请求,-4是一个授权的请求,-1表示服务端触发事件的时候才会调用。

接着往后面看,会从pendingQueue中移除这个packet,为什么要移除呢,因为packet发送完成后它会加入到pendingQueue这个集合中,直到服务端返回response。

之后会进行一系列服务端的判断校验,并将服务端的信息设置到客户端拿到的packet中,同时会把服务端返回的response反序列化:

这里就是Stat信息:

比如在ZooKeeper的exists方法中,就是通过这个来获取Stat的:

之前也说过了,这些过程都是异步的,那么从response获取到的Stat会不会是空的,这个不必担心,在前面也介绍过了,在提交请求的时候实际上是有一个阻塞的过程的:

以上就是客户端接收响应的过程,到这里还没有完,因为这里还有一个finally:

看看finishPacket()方法,也比较简单,这里就不详细说明了:

客户端接收消息的主要流程如下:

事件注册后,那究竟是怎么触发的呢?在WatchManager中有一个triggerWatch()方法:

以setData()为例,我们知道setData()方法是可以触发Wather的,那就直接看DataTree的setData()方法:

补充说明下:DataTree类维护整个目录树结构ConcurrentHashMap<String, DataNode> nodes保存了从完整路径到DataNode的hashtable,而DataNode中的Set<String> children保存了父子关系即子节点的相对路径。通过某DataNode可以获知其任意子节点的相对路径,然后拼装成完整路径,再去DataTree的nodes中查找。所有对节点路径的访问都是通过nodes完成的。

这个dataWatches就是WatchManager:

在这里通过dataWatches触发节点(path)和事件类型(EventType.NodeDataChanged),再回过头来看这个truggerWatch()方法到底做了哪些事情:

又调用了一个重载的triggerWatch()方法,首先会封装一个WatchdEvent对象,参数是事件类型,同步状态和path。

这里有个关键点是,它首先会从watchTable中异常这个path(这也就是为什么事件只会触发一次的原因):

这个watchTable就是之前介绍过的那个HashMap:

接着triggerWatch()方法,,紧接着又会循环删除watch2Paths这个Map中的这个path,因为在WatchManager中还维护者一个key是Watcher,value是path的Map,即watch2Paths,这个之前也介绍过:

最后会循环出发Watch:

来看下这个process()方法,这里是不是有点小激动,多么关键的方法总算来了。

这个方法的实现有很多,那到底调用的是哪一个呢?

再反推这个w是从哪里来的,这个w是从watchers中来的:

那watchers是从哪里来的呢:

是从watchTable.remove(path)返回的。那watchTable是从哪里来的呢:

在DataTree的statNode()方法中:

再回想一下之前的调用过程,在FinalRequestProcessor中,我们以exists类型为例:

这里传递的是不是就是cnxn,之前也介绍过了,这里存在一个向上转型:

因此说明调用process()方法的是ServerCnxn,可以看看它的实现:

而这又是个模板的抽象方法,可以看看有哪些实现:

看看NettyServerCnxn的实现:

最终会调用sendResponse()方法,要注意的是这个h中的xid是-1,前面也介绍过了,-1是一个引导流程的非常重要的参数。

会对header和response做一个序列化,之后通过channel再去发送。至此服务端的事件触发已经打通,总的来说服务端会发送一个WatchedEvent给客户端。

客户端的接收流程就还是不变,毕竟代码是公共的嘛,先看看CientCnxnSocketNetty:

收到消息也是会调用整个readResponse方法,这里又回到之前介绍过的地方了:

这里用-1、-2、-4代表执行什么流程,这里也没有强制性的使用设计模式,如果服务端返回的是事件类型,也就是-1,就会进入下面的流程:

经过反序列化获取服务端传过来的WatchedEvent,最终会将WatchedEvent作为参数执行queueEvent():

这里materializedWatchers是空的,就会执行:

来看看materialize()方法:

看到了很熟悉的existWatches,客户端会把所有的事件放到existWatches中去:

简单点说,ZKWatchManager是客户端管理Watcher的,WatchManager是服务端管理Watcher的。

也就是说这里的这个方法返回的watchers的得到的是所有的exists的watcher事件:

最终在ClientCnxn中会添加到队列中:

而这个waitingEvents是在哪里被调用呢,是在EventThread里卖弄调用的,看看EventThread的run()方法:

的确也是从队列里面取到数据然后搞事情,搞什么事情呢,执行processEvent()方法:

会循环遍历调用process()方法,而真正执行的不就是我们在客户端实现Watcher重写的process()方法嘛。就这样最终完成了事件的回调。

更多相关资料:

https://blog.csdn.net/liu857279611/article/details/70495413

 https://www.cnblogs.com/programlearning/archive/2017/05/10/6834963.html

https://blog.csdn.net/Dongguabai/article/details/82953133

https://blog.csdn.net/Dongguabai/article/details/82907095

https://www.jianshu.com/p/d6301f07ad39

http://www.cnblogs.com/sunddenly/articles/4087251.html

猜你喜欢

转载自blog.csdn.net/Dongguabai/article/details/82986476