本文是听了老师讲的zookeeper课程之后的课下实验,文章是原创的,思想是抄袭的
目录
基本概念
实验描述
实验步骤
建立zookeeper集群
准备jdk和zookeeper
准备Dockerfile
准备source.list文件
准备集群配置文件 zoo.cfg
编译docker镜像
启动集群
编写client代码
验证实验设想
Dockerfile
maven编译
运行项目
实验原理很简单:
client每次启动后发起向zookeeper的连接,并且去获取位于 /testConf/AppConf 路径下的节点,而 AppConf 节点中保存的是client运行所需要的数据,这里假设都是简单的字符串。
然后大致可以分为这么几种情况:
大致步骤如下:
需要3台或者4台机器来构建zookeeper集群,由于我本地是 M1 架构的 Mac笔记本,所以选择使用 docker 构建实验环境
从oracle官网下载 jdk1.8 以及从 apache 官网下载 zookeeper-3.7.0,老师讲课的时候用的是 zookeeper-3.4.6,不过我去官网没找到老师的版本,就使用了 3.7,目录结构如下:
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 的基础镜像,为了在容器中安装软件,需要先提供一份国内的软件源
# 默认注释了源码仓库,如有需要可自行取消注释
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/
# 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 端口
现在都准备好了,直接在 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了~
这里直接使用 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:
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() ,因为它本身是阻塞的,直到拿到数据给你才回返回。
至此,逻辑应该清晰了,文章开头提到的四种情况也可以拿到代码里来分析了:
终于到了验证的环节~
由于在宿主机无法访问docker内部的ip,所以我把代码部署到docker容器里,而且把这个容器也连接到 zknet 网络。
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 宿主机目录:容器目录 把代码映射到容器里,方便在宿主机改代码
然后,
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
到这里实验就完成啦!
通过这个实验,对响应式编程有了一些理解,写代码不需要再按照常规的逻辑一杆到底,可以通过事件驱动把不同情况下需要处理的事情独立开来,然后通过事件和回调编织成一个功能强大而且优美的应用。