ZooKeeper应用---分布式配置更新

本文是听了老师讲的zookeeper课程之后的课下实验,文章是原创的,思想是抄袭的

目录

基本概念

实验描述

实验步骤

建立zookeeper集群

准备jdk和zookeeper

准备Dockerfile

准备source.list文件

准备集群配置文件 zoo.cfg 

编译docker镜像

启动集群

编写client代码

验证实验设想

Dockerfile

maven编译

运行项目


基本概念

  • zookeeper监听机制,客户端在调用 getData 和 exists 方法时可以指定监听watcher,用于后续该数据发生变化自动感知当前数据的状态
  • zookeeper回调机制,当需要异步操作数据时可以指定回调方法callback,类似响应式编程
  • 响应式编程:程序员不需要按照应用的处理逻辑去组织代码,而是规定什么事件发生后做什么事情(逻辑帝?代码的上帝?哈哈哈,我也不知道怎么形容,老师说的。。。

实验描述

ZooKeeper应用---分布式配置更新_第1张图片

实验原理很简单

client每次启动后发起向zookeeper的连接,并且去获取位于 /testConf/AppConf 路径下的节点,而 AppConf 节点中保存的是client运行所需要的数据,这里假设都是简单的字符串。

然后大致可以分为这么几种情况:

  • client初次启动时,需要的节点不存在,这时client应该阻塞等待需要的数据被创建
  • client运行过程中,节点的数据发生变更,client需要实时获取最新的数据
  • client运行过程中,节点被删除,client需要被阻塞,等待数据恢复
  • client阻塞等待数据时,节点被创建,client立即获得数据并开始运行业务代码

实验步骤

大致步骤如下:

  • 建立zookeeper集群
  • 编写client代码
  • 验证实验设想

建立zookeeper集群

需要3台或者4台机器来构建zookeeper集群,由于我本地是 M1 架构的 Mac笔记本,所以选择使用 docker 构建实验环境

准备jdk和zookeeper

从oracle官网下载 jdk1.8 以及从 apache 官网下载 zookeeper-3.7.0,老师讲课的时候用的是 zookeeper-3.4.6,不过我去官网没找到老师的版本,就使用了 3.7,目录结构如下:

ZooKeeper应用---分布式配置更新_第2张图片

准备Dockerfile

FROM ubuntu:18.04

COPY ./jdk1.8.0_321 /usr/local/jdk1.8.0_321
COPY ./apache-zookeeper-3.7.0-bin /opt/zookeeper-3.7.0
COPY ./zoo.cfg /opt/zookeeper-3.7.0/conf/zoo.cfg

RUN echo export ZOOKEEPER_HOME=/opt/zookeeper-3.7.0 >> /root/.bashrc \
 && echo export PATH=$PATH:$ZOOKEEPER_HOME/bin >> /root/.bashrc \
 && echo export PATH=$PATH:/usr/local/jdk1.8.0_321/bin/ >> /root/.bashrc \
 && . /root/.bashrc

RUN mkdir -p /var/zookeeper/ \
 && echo 1 > /var/zookeeper/myid

COPY ./sources.list.bionic /etc/apt/sources.list
RUN apt-get update && apt-get install -y \
        libssl-dev \
        iputils-ping \
        vim \
        net-tools \
        lsof \
        procps

直接使用官方 ubuntu:18.04 的基础镜像,为了在容器中安装软件,需要先提供一份国内的软件源

准备source.list文件

# 默认注释了源码仓库,如有需要可自行取消注释
deb http://mirrors.ustc.edu.cn/ubuntu-ports/ bionic main restricted universe multiverse
# deb-src http://mirrors.ustc.edu.cn/ubuntu-ports/ bionic main main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates main restricted universe multiverse
# deb-src http://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-backports main restricted universe multiverse
# deb-src http://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-backports main restricted universe multiverse
deb http://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-security main restricted universe multiverse
# deb-src http://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-security main restricted universe multiverse

# 预发布软件源,不建议启用
# deb http://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-proposed main restricted universe multiverse
# deb-src http://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-proposed main restricted universe multiverse

这里要注意一下,这个文件是针对 ARM 架构(M1) 的 ubuntu容器,其他架构可以去这里找:

ubuntu | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirrorubuntu 使用帮助 | 镜像站使用帮助 | 清华大学开源软件镜像站,致力于为国内和校内用户提供高质量的开源软件镜像、Linux 镜像源服务,帮助用户更方便地获取开源软件。本镜像站由清华大学 TUNA 协会负责运行维护。https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/

准备集群配置文件 zoo.cfg 

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/var/zookeeper/
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1

## Metrics Providers
#
# https://prometheus.io Metrics Exporter
#metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
#metricsProvider.httpPort=7000
#metricsProvider.exportJvmInfo=true

server.1=zk01:2888:3888
server.2=zk02:2888:3888
server.3=zk03:2888:3888
server.4=zk04:2888:3888

这里主要注意两个配置:dataDir 和 最后四行

dataDir 是配置集群节点的数据目录 zookeeper 启动时会在这个目录里找 myid 的文件,这个文件里面只有一个数字,代表当前节点的id,这个id也会影响选主时的节点之间优先级,如果 id 重复了,后面启动的节点是起不来的,Dockerfile 里面为了方便给每个节点都分配了1的id,所以在集群节点启动前,要手动去修改一下myid文件里的值。

集群节点配置:最后四行是集群节点配置,格式是 

        server.{集群节点id}={节点ip}:{数据同步端口}:{选主投票端口}

这里指定数据同步走 2888 端口,选主通信走 3888 端口

编译docker镜像

现在都准备好了,直接在 Dockerfile 所在的目录执行编译镜像命令:

docker build -t zookeeper:1.0 .

这会生成一个 zookeeper:1.0 的镜像,就是我们的zookeeper集群的镜像,在启动镜像之前,为了方便管理,我们先创建一个网络 zknet,集群所有的节点都连到这个网络,以便互相通信:

docker network create -d bridge zknet

好了,现在可以启动集群了

启动集群

docker run -it -d --rm --name zk01 -h zk01 --network zknet zookeeper:1.0
docker run -it -d --rm --name zk02 -h zk02 --network zknet zookeeper:1.0
docker run -it -d --rm --name zk03 -h zk03 --network zknet zookeeper:1.0
docker run -it -d --rm --name zk04 -h zk04 --network zknet zookeeper:1.0

启动时指定网络,并且制定hostname,这样这几台机器就可以互相通过机器名访问彼此了

然后登录到每一个节点去修改 /var/zookeeper/myid 里的值

root@zk01:~# cat /var/zookeeper/myid
1
root@zk01:~#

root@zk02:~# cat /var/zookeeper/myid
2
root@zk02:~#

root@zk03:~# cat /var/zookeeper/myid
3
root@zk03:~#

root@zk04:~# cat /var/zookeeper/myid
4
root@zk04:~#

然后使用 $ZOOKEEPER_HOME/bin/zkServer.sh 启动服务

$ZOOKEEPER_HOME/bin/zkServer.sh start

如果想在命令行观察服务启动时的输出就把 start 换成 start-foreground

然后查看下集群状态:

root@zk01:/opt/zookeeper-3.7.0# ./bin/zkServer.sh status
/usr/local/jdk1.8.0_321/bin/java
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper-3.7.0/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower
root@zk01:/opt/zookeeper-3.7.0#


root@zk02:/opt/zookeeper-3.7.0# ./bin/zkServer.sh status
/usr/local/jdk1.8.0_321/bin/java
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper-3.7.0/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower
root@zk02:/opt/zookeeper-3.7.0#


root@zk03:/opt/zookeeper-3.7.0# ./bin/zkServer.sh status
/usr/local/jdk1.8.0_321/bin/java
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper-3.7.0/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: leader
root@zk03:/opt/zookeeper-3.7.0#


root@zk04:/opt/zookeeper-3.7.0# ./bin/zkServer.sh status
/usr/local/jdk1.8.0_321/bin/java
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper-3.7.0/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower
root@zk04:/opt/zookeeper-3.7.0#

好了,这个时候集群已经成功建立好了。可以开始写client了~

编写client代码

这里直接使用 IDEA 创建一个 maven quick-start 项目,pom.xml就是这样




  4.0.0

  org.example
  zookeeper
  1.0-SNAPSHOT

  zookeeper
  
  http://www.example.com

  
    UTF-8
    1.7
    1.7
  

  
    
      junit
      junit
      4.11
      test
    
    
    
      org.apache.zookeeper
      zookeeper
      3.7.0
    
  

  
    
      
        
        
          maven-clean-plugin
          3.1.0
        
        
        
          maven-resources-plugin
          3.0.2
        
        
          maven-compiler-plugin
          3.8.0
        
        
          maven-surefire-plugin
          2.22.1
        
        
          maven-jar-plugin
          3.0.2
        
        
          org.apache.maven.plugins
          maven-shade-plugin
          2.1
          
            
              package
              
                shade
              
              
                
                  
                  
                
              
            
          
        
        
          maven-install-plugin
          2.5.2
        
        
          maven-deploy-plugin
          2.8.2
        
        
        
          maven-site-plugin
          3.7.1
        
        
          maven-project-info-reports-plugin
          3.0.0
        
      
    
  

注意 dependency 里引入 zookeeper的版本也要是 3.7.0

创建一个测试类 TestConfig

package org.example.config;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;

public class TestConfig {
    // zk 实例
    public static ZooKeeper zk;
    // 配置数据对象
    public static MyConf myConf = new MyConf();
    public static void main(String[] args) throws InterruptedException, KeeperException {
        // 通过工具类获取zk实例
        zk = ZkUtils.getZk();
        System.out.println(zk.toString());
        // 定义配置节点路径
        String path = "/AppConf";

        // 主要逻辑在这里
        WatcherCallback watcherCallback = new WatcherCallback(zk, path, myConf);
        // 等待配置加载
        watcherCallback.await();

        while(true) {
            if("".equals(myConf.getConf())) {
                System.out.println("AppConf not exists !!!");
                // 阻塞 - 直到配置恢复
                watcherCallback.await();
            } else {
                System.out.println("AppConf: " + myConf.getConf());
            }

            Thread.sleep(2000);
        }
    }
}

TestConfig 比较简单,定义了配置数据对象 MyConf ,通过工具类 ZkUtils 去获取 zookeeper 连接对象,然后定义自己需要从哪个节点获取配置数据,然后就调用了一个 WatcherCallback 的对象的 await 方法 ,最后不断的循环从 MyConf对象中拿数据并且打印数据,如果数据为空它又去调用了一次 WatcherCallback.await() 方法。

显然,WatcherCallback.await() 方法一定是一个阻塞的方法,并且它肯定会修改 MyConf 对象的值。

先看下 MyConf 类

public class MyConf {
    public String getConf() {
        return conf;
    }

    public void setConf(String conf) {
        this.conf = conf;
    }

    private String conf;
}

平平无奇

然后是 ZkUtils 类

public class ZkUtils {

    private static ZooKeeper zk;

    private static String address = "zk01:2181,zk02:2181,zk03:2181,zk04:2181/testConf";
    private static CountDownLatch connectLatch = new CountDownLatch(1);
    private static DefaultWatcher defaultWatcher = new DefaultWatcher();

    public static ZooKeeper getZk() {
        defaultWatcher.setConnectLatch(connectLatch);
        try {
            zk = new ZooKeeper(address, 3000, defaultWatcher);
            connectLatch.await();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

        return zk;
    }
}

ZkUtils 主要就是获取 zookeeper 连接对象,它定义了连接地址并且指定了 testConf 作为本次实验的根目录。

由于在连接zookeeper的时候并不是立即返回 zk 实例所以这里使用了 CountDownLatch 来等待,并且在 defaultWatcher 对象中判断连接成功的时候给 CountDownLatch 执行 countDown()

DefaultWatcher 实现如下:

public class DefaultWatcher implements Watcher {
    CountDownLatch connectLatch;
    public void setConnectLatch(CountDownLatch connectLatch) {
        this.connectLatch = connectLatch;
    }

    @Override
    public void process(WatchedEvent event) {
        System.out.println(event.toString());
        switch (event.getState()) {
            case Unknown:
                break;
            case Disconnected:
                break;
            case NoSyncConnected:
                break;
            case SyncConnected:
                // 连接成功
                System.out.println("defaultWatcher: syncConnected!");
                connectLatch.countDown();
                break;
            case AuthFailed:
                break;
            case ConnectedReadOnly:
                break;
            case SaslAuthenticated:
                break;
            case Expired:
                break;
            case Closed:
                break;
        }
    }
}

当连接成功的时候,打印信息并且执行 countDown() 方法,这样 ZkUtils.getZk()方法就会返回 zk对象给 TestConfig 类了。到这里就已经拿到了 zookeeper 的连接,对吧?

下面重点看 WatcherCallback 类的实现:


public class WatcherCallback implements Watcher, AsyncCallback.StatCallback, AsyncCallback.DataCallback {

    private ZooKeeper zk;
    private MyConf myConf;
    private String path;
    private CountDownLatch latch = new CountDownLatch(1);

    public WatcherCallback(ZooKeeper zk, String path, MyConf conf) {
        this.zk = zk;
        this.path = path;
        this.myConf = conf;
    }
    /**
     * 节点是否存在监听
     * @param event e
     */
    @Override
    public void process(WatchedEvent event) {
        switch (event.getType()) {
            case None:
                break;
            case NodeCreated:
                // 节点从无到有
                zk.getData(path, this, this, "ABC");
                break;
            case NodeDeleted:
                // 节点被删除
                latch = new CountDownLatch(1);
                myConf.setConf("");
                break;
            case NodeDataChanged:
                // 节点数据修改
                zk.getData(path, this, this, "ABC");
                break;
            case NodeChildrenChanged:
                break;
            case DataWatchRemoved:
                break;
            case ChildWatchRemoved:
                break;
            case PersistentWatchRemoved:
                break;
        }
    }

    /**
     * 节点是否存在回调
     * @param rc
     * @param path
     * @param ctx
     * @param stat
     */
    @Override
    public void processResult(int rc, String path, Object ctx, Stat stat) {
        // 节点存在
        if(stat != null) {
            zk.getData(path, this, this, ctx);
        }
    }

    /**
     * 获取数据回调
     * @param rc
     * @param path
     * @param ctx
     * @param data
     * @param stat
     */
    @Override
    public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
        if(data != null) {
            myConf.setConf(new String(data));
            latch.countDown();
        }
    }

    public void await() {
        zk.exists(path, this, this, "ABC");
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这是我们本次实验的最后一个类,主要的逻辑也都在这里。

先了解一下 zookeeper 两个 API:

ZooKeeper应用---分布式配置更新_第3张图片

 exists方法是判断某个节点是否存在

getData方法是获取节点的信息

这两个方法都有四个实现,其中两个同步的实现直接返回,两个异步的实现没有返回值,我们本实验要用的就是异步中带 Watcher 的实现,也就是图中的第三个和第七个方法。

这两个方法中的 Watcher 是指在调用方法的同时对节点设置监听,一旦节点状态有变化就会调用 Watcher 的 process(WatchedEvent event) 方法,可以用来判断节点发生了怎样的变化,并且可以针对不同的状态进行不同的后续操作。

StatCallback 和 DataCallback 则是异步调用方法中的回调方法,也就是方法执行后并不等待结果,如果结果到达了请去调用我注册的 callback 的相应方法,我会在那里处理结果。这里的回调方法就是:processResult()

所以,我们的 WatcherCallback 类中的第一个方法 process 是一个监听方法,会接收到节点 /testConf/AppConf 状态的变化,并且它可能是 exists() 方法或者 getData() 方法注册的,不管谁注册的处理逻辑都一样,在节点被创建/被删除/被修改时才会做出动作。

/**
     * 节点是否存在监听
     * @param event e
     */
    @Override
    public void process(WatchedEvent event) {
        switch (event.getType()) {
            case None:
                break;
            case NodeCreated:
                // 节点从无到有
                zk.getData(path, this, this, "ABC");
                break;
            case NodeDeleted:
                // 节点被删除
                latch = new CountDownLatch(1);
                myConf.setConf("");
                break;
            case NodeDataChanged:
                // 节点数据修改
                zk.getData(path, this, this, "ABC");
                break;
            case NodeChildrenChanged:
                break;
            case DataWatchRemoved:
                break;
            case ChildWatchRemoved:
                break;
            case PersistentWatchRemoved:
                break;
        }
    }

第二个方法:processResult() 是 exists() 方法的回调,它只有四个入参 stat 表示节点到底存在不存在,可以通过 判断 stat == null 确定

    /**
     * 节点是否存在回调
     * @param rc
     * @param path
     * @param ctx
     * @param stat
     */
    @Override
    public void processResult(int rc, String path, Object ctx, Stat stat) {
        // 节点存在
        if(stat != null) {
            zk.getData(path, this, this, ctx);
        }
    }

第三个方法:processResult() 是 getData() 方法的回调,它的第四个入参就是获取到节点的数据,不为空的话就可以直接取出来使用

    /**
     * 获取数据回调
     * @param rc
     * @param path
     * @param ctx
     * @param data
     * @param stat
     */
    @Override
    public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
        if(data != null) {
            myConf.setConf(new String(data));
            latch.countDown();
        }
    }

第四个方法:await() 就是暴露给外部使用的方法,它首先通过 exists() 方法判断节点存不存在,同时注册监听对象为 this, 回调对象也是 this    第四个参数是上下文对象,与试验无关,不说了

   public void await() {
        zk.exists(path, this, this, "ABC");
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

因为 exists() 是异步方法,所以它会立即返回往下执行,但是 await() 方法被外部调用所以它不能立即返回,因为这个时候数据还没返回,所以使用 CountDownLatch.await() 拦截一下,这也是为什么这个方法叫 await() ,因为它本身是阻塞的,直到拿到数据给你才回返回。

至此,逻辑应该清晰了,文章开头提到的四种情况也可以拿到代码里来分析了:

  • client初次启动时:exists()方法通过注册监听感知节点的创建并调用getData(), 而getData()的回调方法拿到数据后使 countDownLatch减1来结束 await() 方法的阻塞,方法返回后,TestConfig 类就拿到了 /testConf/AppConf 里的数据。另外一种情况,如果节点已经存在,那么 exists()的回调也会触发 getData()拿到数据并结束阻塞
  • client运行过程中,节点的数据发生变更:由于 getData() 方法注册了监听方法process(),一旦数据发生改变立即又会调用 getData() 实时获取最新的数据,并再次注册监听以便下次变更。这里还是会重复调用 countDown()方法,虽然没什么用,后面再优化吧~
  • client运行过程中,节点被删除:在监听方法中如果节点被删除,首先重置 latch ,然后把数据对象的值置为空字符串,外部 TestConfig 的循环中发现配置为空字符串的时候会调用 await() 方法阻塞等待并报告错误信息,所以 latch 要重置。而 await() 方法被调用会调用 exists() 方法,再次注册了监听方法,等待节点被创建,直到数据恢复,类似第一种情况
  • client阻塞等待数据时,节点被创建:监听方法立即获取数据并 countDown(),client立即获得数据并开始运行业务代码

验证实验设想

终于到了验证的环节~

由于在宿主机无法访问docker内部的ip,所以我把代码部署到docker容器里,而且把这个容器也连接到 zknet 网络。

Dockerfile

FROM ubuntu:18.04

COPY ./jdk1.8.0_321 /usr/local/jdk1.8.0_321
COPY ./apache-maven-3.8.5 /usr/local/apache-maven-3.8.5

RUN echo export PATH=$PATH:/usr/local/jdk1.8.0_321/bin/:/usr/local/apache-maven-3.8.5/bin/ >> /root/.bashrc \
 && . /root/.bashrc

COPY ./sources.list.bionic /etc/apt/sources.list
RUN apt-get update && apt-get install -y \
        libssl-dev \
        iputils-ping \
        vim \
        net-tools \
        lsof \
        procps

用这个 Dockerfile 构建一个 jdk 的镜像,同时也包含 maven,因为等会儿要在容器里运行项目,

maven需要提前下载到 Dockerfile 所在的目录,然后 source.list.bionic 文件和上面那个 zookeeper集群镜像使用的一样,复制过来即可。

假设这个镜像的名字叫 :jdk:1.0,下面是启动镜像容器的命令:

docker run -it -d --name zkclient --rm --network zknet -v ~/Document/workdata/projects/zookeeper:/data -w /data jdk:1.0

--name 指定容器的名字是 zkclient

--network 连接到 zknet 网络

-v 宿主机目录:容器目录 把代码映射到容器里,方便在宿主机改代码

然后,

maven编译

mvn package -Dmaven.test.skip=true

运行项目

java -cp target/zookeeper-1.0-SNAPSHOT.jar:/data/zookeeper/target/classes:/root/.m2/repository/org/apache/zookeeper/zookeeper/3.7.0/zookeeper-3.7.0.jar:/root/.m2/repository/org/apache/zookeeper/zookeeper-jute/3.7.0/zookeeper-jute-3.7.0.jar:/root/.m2/repository/org/apache/yetus/audience-annotations/0.12.0/audience-annotations-0.12.0.jar:/root/.m2/repository/io/netty/netty-handler/4.1.59.Final/netty-handler-4.1.59.Final.jar:/root/.m2/repository/io/netty/netty-common/4.1.59.Final/netty-common-4.1.59.Final.jar:/root/.m2/repository/io/netty/netty-resolver/4.1.59.Final/netty-resolver-4.1.59.Final.jar:/root/.m2/repository/io/netty/netty-buffer/4.1.59.Final/netty-buffer-4.1.59.Final.jar:/root/.m2/repository/io/netty/netty-transport/4.1.59.Final/netty-transport-4.1.59.Final.jar:/root/.m2/repository/io/netty/netty-codec/4.1.59.Final/netty-codec-4.1.59.Final.jar:/root/.m2/repository/io/netty/netty-transport-native-epoll/4.1.59.Final/netty-transport-native-epoll-4.1.59.Final.jar:/root/.m2/repository/io/netty/netty-transport-native-unix-common/4.1.59.Final/netty-transport-native-unix-common-4.1.59.Final.jar:/root/.m2/repository/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar:/root/.m2/repository/org/slf4j/slf4j-log4j12/1.7.30/slf4j-log4j12-1.7.30.jar:/root/.m2/repository/log4j/log4j/1.2.17/log4j-1.2.17.jar org.example.config.TestConfig

我把 zookeeper解压包里的 conf/log4j.properties 文件放到项目的 resources 目录,所以可以打印出连接 zookeeper 的日志,截取部分效果:

2022-03-21 11:07:02,256 [myid:] - INFO  [main:X509Util@77] - Setting -D jdk.tls.rejectClientInitiatedRenegotiation=true to disable client-initiated TLS renegotiation
2022-03-21 11:07:02,263 [myid:] - INFO  [main:ClientCnxnSocket@239] - jute.maxbuffer value is 1048575 Bytes
2022-03-21 11:07:02,304 [myid:] - INFO  [main:ClientCnxn@1726] - zookeeper.request.timeout value is 0. feature enabled=false
2022-03-21 11:07:02,317 [myid:zk04:2181] - INFO  [main-SendThread(zk04:2181):ClientCnxn$SendThread@1171] - Opening socket connection to server zk04/172.18.0.2:2181.
2022-03-21 11:07:02,317 [myid:zk04:2181] - INFO  [main-SendThread(zk04:2181):ClientCnxn$SendThread@1173] - SASL config status: Will not attempt to authenticate using SASL (unknown error)
2022-03-21 11:07:02,320 [myid:zk04:2181] - INFO  [main-SendThread(zk04:2181):ClientCnxn$SendThread@1005] - Socket connection established, initiating session, client: /172.18.0.6:45588, server: zk04/172.18.0.2:2181
2022-03-21 11:07:02,338 [myid:zk04:2181] - INFO  [main-SendThread(zk04:2181):ClientCnxn$SendThread@1438] - Session establishment complete on server zk04/172.18.0.2:2181, session id = 0x40000d1e8d10006, negotiated timeout = 4000
WatchedEvent state:SyncConnected type:None path:null
defaultWatcher: syncConnected!
State:CONNECTED Timeout:4000 sessionid:0x40000d1e8d10006 local:/172.18.0.6:45588 remoteserver:zk04/172.18.0.2:2181 lastZxid:0 xid:1 sent:1 recv:1 queuedpkts:0 pendingresp:0 queuedevents:0
AppConf: xxxxx
AppConf: xxxxx
AppConf: xxxxx
AppConf: xxxxx
AppConf: xxxxx
AppConf: xxxxx
AppConf: xxxxx

可以看到项目启动后直接打印了数据,查看 zookeeper 当前的数据:

[zk: localhost:2181(CONNECTED) 2] get /testConf/AppConf
xxxxx
[zk: localhost:2181(CONNECTED) 3]

修改一下 zookeeper 节点的值:

[zk: localhost:2181(CONNECTED) 5]
[zk: localhost:2181(CONNECTED) 5] set /testConf/AppConf "aaaaaaaaa"
[zk: localhost:2181(CONNECTED) 6]

查看客户端输出:

AppConf: xxxxx
AppConf: xxxxx
AppConf: xxxxx
AppConf: xxxxx
AppConf: xxxxx
AppConf: aaaaaaaaa
AppConf: aaaaaaaaa
AppConf: aaaaaaaaa
AppConf: aaaaaaaaa
AppConf: aaaaaaaaa
AppConf: aaaaaaaaa

把节点删掉试试:

[zk: localhost:2181(CONNECTED) 6]
[zk: localhost:2181(CONNECTED) 6] delete /testConf/AppConf
[zk: localhost:2181(CONNECTED) 7]

客户端打印了错误,并且阻塞了: 

AppConf: aaaaaaaaa
AppConf: aaaaaaaaa
AppConf: aaaaaaaaa
AppConf not exists !!!

重新创建节点:

[zk: localhost:2181(CONNECTED) 7]
[zk: localhost:2181(CONNECTED) 7] create /testConf/AppConf "asdfasfa"
Created /testConf/AppConf
[zk: localhost:2181(CONNECTED) 8]
[zk: localhost:2181(CONNECTED) 8]

客户端恢复并且打印了新的值: 

AppConf: aaaaaaaaa
AppConf not exists !!!
AppConf: asdfasfa
AppConf: asdfasfa
AppConf: asdfasfa
AppConf: asdfasfa
AppConf: asdfasfa
AppConf: asdfasfa

到这里实验就完成啦!

通过这个实验,对响应式编程有了一些理解,写代码不需要再按照常规的逻辑一杆到底,可以通过事件驱动把不同情况下需要处理的事情独立开来,然后通过事件和回调编织成一个功能强大而且优美的应用。

你可能感兴趣的:(ZooKeeper,zookeeper)