文章目录

  • 03-zookeeper理论相关——watch及API操作
  • zookeeper中的事件和状态
  • zookeeper客户端与zookeeper server连接的状态
  • zookeeper中的watch事件(当zookeeper客户端监听某个znode节点"/node-x"时)
  • watch机制
  • watch特性1——一次性触发器
  • watch特性2——发送至客户端
  • watch特性3——设置watch的数据内容
  • zookeeper watch的运行机制
  • zookeeper API操作


03-zookeeper理论相关——watch及API操作

zookeeper主要是为了统一分布式系统中各个节点工作状态,在资源冲突的情况下协调提供节点的资源抢占,提供给每个节点了解整个集群所处状态的途径。这一切的实现都依赖于zookeeper中的事件监听和通知机制。

zookeeper中的事件和状态

事件状态构成了zookeeper客户端连接描述的两个维度。通过下面两个表详细介绍zookeeper中的事件和状态。(API中被定义为@Deprecated的事件和状态就不介绍了)。

zookeeper客户端与zookeeper server连接的状态

连接状态

状态含义

KeeperState.Expire

客户端和服务器在ticktime的时间周期内,是要发送心跳通知的。这是租约协议的一个实现。客户端发送request,告诉服务器其上一个租约时间,服务器收到这个请求后,告诉客户端其下一个租约时间是哪个时间点。当客户端时间戳达到最后一个租约时间,而没有收到服务器发来的任何新租约时间,即认为自己下线(此后客户端会废弃这次连接)。这个过期状态就是Expired状态。

KeeperState.Disconnected

就像上面哪个状态所述,当客户端断开一个连接(可能是租约期满,也可能是客户端主动断开)这时客户端和服务器的连接就是Disconnected状态。

KeeperState.SyncConnected

一旦客户端和服务器的某一个节点建立连接(注意,虽然集群有多个节点,但是客户端一次连接到一个节点就行了),并完成一次version、zxid的同步,这时的客户端和服务器的连接状态就是SyncConnected。

KeeperState.AuthFailed

zookeeper客户端进行连接认证失败时,发生该状态。

需要说明的是,这些状态在触发时,所记录的事件类型都是:EventType.None

zookeeper中的watch事件(当zookeeper客户端监听某个znode节点"/node-x"时)

zookeeper事件

事件含义

EventType.NodeCreated

当node-x这个节点被创建时,该事件被触发。

EventType.NodeChildrenChanged

当node-x这个节点的直接子节点被创建、被删除、子节点数据发生变更时,该事件被触发。

EventType.NodeDataChanged

当node-x这个节点的数据发生变更时,该事件触发。

EventType.NodeDeleted

当node-x这个节点被删除时,该事件被触发。

EventType.None

当zookeeper客户端的连接状态发生变更时,即KeeperState.Expired、KeeperState.Disconnected、KeeperState.SyncConnected、KeeperState.AuthFailed状态切换时,描述的事件类型为EventType.None

Zookeeper模糊匹配_zookeeper

watch机制

znode发生变化(znode本身的增加、删除、修改、以及子znode的变化)可以通过watch机制通知到客户端。那么要实现watch,就必须实现org.apache.zookeeper.Watcher接口,并且将实现类的对象传入到可以watch的方法中。zookeeper中所有读操作(getData(),getChildren(),exits())都可以设置watch选项。watch事件具有**one-time trigger(一次性触发)**的特性,如果watch监听的znode有变化,那么就会通知设置该watch的客户端。

在上述说到的所有读操作中,如果需要watch,可以自定义watch,如果是boolean型变量,当为true时,则使用系统默认的watch,系统默认的watch是在zookeeper的构造函数中定义的watch(在new zk时也可以自己指定)。参数中watch为空或者false,表示不启用watch。

watch特性1——一次性触发器

客户端在znode设置了watch时,如果znode内容发生改变,那么客户端就会获得watch事件。例如:客户端设置getData("/znode1",true)后,如果/znode1发生改变或者删除,那么客户端就会得到一个/znode1的watch事件,但是/znode1再次发生变化,那客户端是无法收到watch事件的,除非客户端设置了新的watch。

watch特性2——发送至客户端

watch事件是异步发送到Client。zookeeper可以保证客户端发送过去的更新顺序是有序的。例如:某个znode没有设置watch,那么客户端对这个znode设置watch发送到集群之前,该客户端是感知不到该znode任何的改变情况。换个角度解释:由于watch有一次性触发的特点,所以在服务器端没有watch的情况下,znode的任何变更就不会通知到客户端。

不过,即使某个znode设置了watch,且在znode有变化的情况下通知到了客户端,但是在客户端接收到这个变化事件,还没有再次设置watch之前,如果其他客户端对znode做了修改,这种情况下,znode第二次的变化客户端是无法收到通知的。这可能是由于网络延迟或者其他因素导致,所以在使用zookeeper不能期望能够监控到节点每次的变化。zookeeper只能保证最终的一致性,而无法保证强一致性

watch特性3——设置watch的数据内容

znode改变有很多种方式,例如:节点创建,节点删除,子节点改变等等。zookeeper维护了两个watch列表,一个节点数据watch列表,另一个是子节点watch列表。getData()exits()设置数据watch,getChildren()设置子节点watch。两者选其一,可以让我们根据不同的返回结果选择不同的watch方式,getData()exits()返回节点的内容,getChildren()返回子节点列表。

因此,setData()触发内容watch,create()触发当前节点的内容watch或者是其父节点的子节点watch。delete()同时触发父节点的子节点watch和内容watch,以及子节点的内容watch。

zookeeper watch的运行机制

  • watch是轻量级的,其实就是本地JVM的callback,服务器端只是存了是否有设置watch的布尔类型。(源码见:org.apache.zookeeper.server.FinalRequestProcessor)
  • 在服务端,在FianlRequestProcessor处理对应的znode操作时,会根据客户端传递的watch变量,添加到对应的ZKDatabase(org.apache.zookeeper.server.ZKDatabase)中进行持久化存储,同时将自己NIOServerCnxn作为一个Watcher callback,监听服务端事件变化。
  • leader通过投票通过了某次znode变化的请求后,然后通知对应的follower,follower根据自己内存中的zkDataBase信息,发送notification信息给zookeeper客户端。
  • zookeeper客户端接收到notification信息后,找到对应变化path的watch列表,挨个进行触发回调。

Zookeeper模糊匹配_客户端_02

zookeeper API操作

创建一个maven项目,引入以下依赖:

<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.14</version>
    <!--<type>pom</type>-->
</dependency>

注意:引入的版本需要和你服务器上的zookeeper版本一致

代码如下,代码很简单,配合注释看就可以:

package com.wangwren;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.util.concurrent.CountDownLatch;

public class App {
    public static void main( String[] args ) throws Exception {

        CountDownLatch latch = new CountDownLatch(1);

        /**
         * 创建一个zookeeper
         *
         * zookeeper是有session概念的,没有连接池的概念
         *
         * new一个zookeeper,第一个参数是集群的ip,并指定端口号,集群之间用逗号隔开;
         * 第二个参数代表session创建出的数据的过期时间,单位毫秒,即当session退出时,zookeeper给它创建的数据还保留3秒钟
         * 第三个参数创建一个watch监听,这个watch是session级别的,跟path、node没关系,只是在创建zookeeper时监听
         */
        ZooKeeper zk = new ZooKeeper("192.168.0.20:2181,192.168.0.21:2181,192.168.0.22:2181,192.168.0.22:2181"
                , 3000
                , new Watcher() {
                    /**
                     * 重写watch的回调方法
                     *
                     * 程序都是顺序执行的,这种回调方法像是单独开了一个线程,由别人调用的,即另开了一个线程
                     * 所以想要等zookeeper正在创建完成,需要在主程序中等待
                     */
                    @Override
                    public void process(WatchedEvent event) {
                        //zookeeper创建时的状态
                        Event.KeeperState state = event.getState();
                        //zookeeper创建时的类型
                        Event.EventType type = event.getType();

                        System.out.println("new zk " + event.toString());

                        switch (state) {
                            case Unknown:
                                break;
                            case Disconnected:
                                break;
                            case NoSyncConnected:
                                break;
                            case SyncConnected:
                                System.out.println("new zk connected...");
                                latch.countDown();
                                break;
                            case AuthFailed:
                                break;
                            case ConnectedReadOnly:
                                break;
                            case SaslAuthenticated:
                                break;
                            case Expired:
                                break;
                        }

                        switch (type) {
                            case None:
                                break;
                            case NodeCreated:
                                break;
                            case NodeDeleted:
                                break;
                            case NodeDataChanged:
                                break;
                            case NodeChildrenChanged:
                                break;
                        }
                    }
                });

        //等待zk创建,如果这里不等待,那么程序顺序执行,可能输出的结果就是connecting,zk还没创建完呢,怎么继续使用?
        latch.await();

        //获取zk的创建状态
        ZooKeeper.States state = zk.getState();

        switch (state) {
            case CONNECTING:
                System.out.println("connecting...");
                break;
            case ASSOCIATING:
                break;
            case CONNECTED:
                System.out.println("connected...");
                break;
            case CONNECTEDREADONLY:
                break;
            case CLOSED:
                break;
            case AUTH_FAILED:
                break;
            case NOT_CONNECTED:
                break;
        }


        //简单使用

        /*
          创建节点
          第一个参数:节点名称
          第二个参数:数据,zookeeper也是二进制安全的,与Redis一样
          第三个参数:权限相关
          第四个参数:创建的节点类型,即 -s -e 这里创建的是-e,临时节点类型

          还有一种创建节点,方法名都一样,只是带了另一个参数:回调方法
        */
        String pathName = zk.create("/xxoo", "hello".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        //输出的结果就是 /xxoo
        System.out.println("创建的节点名称:" + pathName);


        /*
          获取数据
          第一个参数:指定路径
          第二个参数:指定监听,watch的注册发生在读的时候和exits(判断是否存在)
                    而且watch监听只会触发一次,如果想多次触发,那么可以在回调方法中重新注册
          第三个参数:指定节点元数据信息
         */
        Stat stat = new Stat();

        //返回值是该节点上的数据
        byte[] data = zk.getData(pathName, new Watcher() {
            //获取节点时指定的监听回调方法,该方法只有在节点被修改、删除、子节点被更改时会被调用
            @Override
            public void process(WatchedEvent event) {
                try {

                    System.out.println("修改节点时被触发...");

                    /*
                      重新注册了watch,注意第二个参数,该参数可以传入一个boolean变量的值,true表示使用 new zk 时的那个watch
                      写this就是表示当前对象,即当前的watch,又被注册了一遍

                      如果不写下面这行代码,那么在第二次修改数据时watch中的回调方法将不会再被调用
                     */
                    zk.getData(pathName,this,stat);
                } catch (KeeperException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, stat);

        System.out.println("获取节点上的数据:" + new String(data));

        //触发节点上的回调,最后一个参数:指定版本,当版本与当前要修改的版本不同时是不允许修改的会报错;但是写-1就随便修改
        Stat stat1 = zk.setData(pathName, "hello world".getBytes(), -1);

        //再次修改还是可以触发watch回调
        Stat stat2 = zk.setData(pathName, "hello world world".getBytes(), -1);


        //获取数据时还可以使用回调方法,同样的方法,但是需要写回调方法,数据都在回调方法里

        System.out.println("=========async start============");
        /*
          第一个参数:节点
          第二个参数:不适用监听
          第三个参数:指定回调方法
          第四个参数:上下文,传入的是一个object对象
         */
        zk.getData(pathName, false, new AsyncCallback.DataCallback() {
            @Override
            public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
                System.out.println("=========async callback============");
                System.out.println("rc = " + rc);
                System.out.println("path = " + path);
                System.out.println("ctx = " + ctx);
                System.out.println("data = " + new String(data));
            }
        },zk);

        System.out.println("=========async over============");

        /**
         * 通过输出结果可以看到,确实是回调方法,即start over都打印完了才打印的callback
         * 即程序还是顺序执行了,这种方式会将CPU用到极致,即只有当有消息来的时候才做某一件事,而不是傻等着,这种getData方法也没有返回值
         *
         * 这是异步的方式
         *
         * 所以写了一个死循环阻塞在这
         */

        while (true){

        }
    }
}