目录
ZooKeeper 安装和使用
使用Docker 安装 zookeeper
连接 ZooKeeper 服务
常用命令演示
查看常用命令(help 命令)
创建节点(create 命令)
更新节点数据内容(set 命令)
获取节点的数据(get 命令)
查看某个目录下的子节点(ls 命令)
查看节点状态(stat 命令)
查看节点信息和状态(ls2 命令)
删除节点(delete 命令)
ZooKeeper Java客户端 Curator简单使用
连接 ZooKeeper 客户端
数据节点的增删改查
创建节点
删除节点
获取/更新节点数据内容
获取某个节点的所有子节点路径
异步调用
事件监听
Master选举
分布式锁
分布式计数器
分布式Barrier
注意:本文参考 ZooKeeper 实战 | JavaGuide
Curator框架实现Zookeeper基本操作 - 百里浅暮 - 博客园
a.使用 Docker 下载 ZooKeeper
docker pull zookeeper:3.5.8
b.运行 ZooKeeper
docker run -d --name zookeeper -p 2181:2181 zookeeper:3.5.8
a.进入ZooKeeper容器中
先使用 docker ps 查看 ZooKeeper 的 ContainerID,然后使用 docker exec -it ContainerID /bin/bash 命令进入容器中。
b.先进入 bin 目录,然后通过 ./zkCli.sh -server 127.0.0.1:2181命令连接ZooKeeper 服务
root@eaf70fc620cb:/apache-zookeeper-3.5.8-bin cd bin
如果你看到控制台成功打印出如下信息的话,说明你已经成功连接 ZooKeeper 服务。
通过 help 命令查看 ZooKeeper 常用命令
通过 create 命令在根目录创建了 node1 节点,与它关联的字符串是"node1"
[zk: 127.0.0.1:2181(CONNECTED) 34] create /node1 “node1”
通过 create 命令在node1 创建了 node1.1节点,与它关联的内容是数字 123
[zk: 127.0.0.1:2181(CONNECTED) 1] create /node1/node1.1 123
Created /node1/node1.1
[zk: 127.0.0.1:2181(CONNECTED) 11] set /node1 "set node1"
get 命令可以获取指定节点的数据内容和节点的状态,可以看出我们通过 set 命令已经将节点数据内容改为 "set node1"。
set node1
cZxid = 0x47
ctime = Sun Jan 20 10:22:59 CST 2019
mZxid = 0x4b
mtime = Sun Jan 20 10:41:10 CST 2019
pZxid = 0x4a
cversion = 1
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 9
numChildren = 1
通过 ls 命令查看根目录下的节点
[zk: 127.0.0.1:2181(CONNECTED) 37] ls /
[dubbo, ZooKeeper, node1]
通过 ls 命令查看 node1 目录下的节点
[zk: 127.0.0.1:2181(CONNECTED) 5] ls /node1
[node1.1]
ZooKeeper 中的 ls 命令和 linux 命令中的 ls 类似, 这个命令将列出绝对路径 path 下的所有子节点信息(列出 1 级,并不递归)
通过 stat 命令查看节点状态
[zk: 127.0.0.1:2181(CONNECTED) 10] stat /node1
cZxid = 0x47
ctime = Sun Jan 20 10:22:59 CST 2019
mZxid = 0x47
mtime = Sun Jan 20 10:22:59 CST 2019
pZxid = 0x4a
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 1
上面显示的一些信息比如 cversion、aclVersion、numChildren 等等
ls2 命令更像是 ls 命令和 stat 命令的结合。 ls2 命令返回的信息包括 2 部分:
子节点列表
当前节点的 stat 信息。
[zk: 127.0.0.1:2181(CONNECTED) 7] ls2 /node1
[node1.1]
cZxid = 0x47
ctime = Sun Jan 20 10:22:59 CST 2019
mZxid = 0x47
mtime = Sun Jan 20 10:22:59 CST 2019
pZxid = 0x4a
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 1
这个命令很简单,但是需要注意的一点是如果你要删除某一个节点,那么这个节点必须无子节点才行。
[zk: 127.0.0.1:2181(CONNECTED) 3] delete /node1/node1.1
在后面我会介绍到 Java 客户端 API 的使用以及开源 ZooKeeper 客户端 ZkClient 和 Curator 的使用。
Curator 是Netflix公司开源的一套 ZooKeeper Java客户端框架,相比于 Zookeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。
下面我们就来简单地演示一下 Curator 的使用吧!
Curator4.0+版本对ZooKeeper 3.5.x支持比较好。开始之前,请先将下面的依赖添加进你的项目。
org.apache.curator
curator-framework
4.2.0
org.apache.curator
curator-recipes
4.2.0
通过 CuratorFrameworkFactory 创建 CuratorFramework 对象,然后再调用 CuratorFramework 对象的 start() 方法即可!
private static final int BASE_SLEEP_TIME = 1000;
private static final int MAX_RETRIES = 3;
// Retry strategy. Retry 3 times, and will increase the sleep time between retries.
RetryPolicy retryPolicy = new ExponentialBackoffRetry(BASE_SLEEP_TIME, MAX_RETRIES);
CuratorFramework zkClient = CuratorFrameworkFactory.builder()
// the server to connect to (can be a server list)
.connectString("127.0.0.1:2181")
.retryPolicy(retryPolicy)
.build();
zkClient.start();
对于一些基本参数的说明:
baseSleepTimeMs:重试之间等待的初始时间
maxRetries :最大重试次数
connectString :要连接的服务器列表
retryPolicy :重试策略
说一下retryPolicy,重连策略,建议用其中两种:
//重连3次,每次休息3秒
new RetryNTimes(3,3000);
//重连3次,每次休息大约是1秒
new ExponentialBackoffRetry(1000,3);
//初始化一个大概的等待时间1秒,然后开始重连,最多重连3次,每次最多休息2秒
new ExponentialBackoffRetry(1000,3,2000);
//计算通过这个初始化的大约时间,计算实际需要睡眠多久
long sleepMs = baseSleepTimeMs * Math.max(1, random.nextInt(1 << (retryCount + 1)));
namespace代表命名空间,注意的是,curator会自动创建
我们在 ZooKeeper常见概念解读 中介绍到,我们通常是将 znode 分为 4 大类:
持久(PERSISTENT)节点 :一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
临时(EPHEMERAL)节点 :临时节点的生命周期是与 客户端会话(session) 绑定的,会话消失则节点消失 。并且,临时节点 只能做叶子节点 ,不能创建子节点。
持久顺序(PERSISTENT_SEQUENTIAL)节点 :除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 /node1/app0000000001 、/node1/app0000000002 。
临时顺序(EPHEMERAL_SEQUENTIAL)节点 :除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。
你在使用的ZooKeeper 的时候,会发现 CreateMode 类中实际有 7种 znode 类型 ,但是用的最多的还是上面介绍的 4 种。
a.创建持久化节点
你可以通过下面两种方式创建持久化的节点。
//注意:下面的代码会报错,下文说了具体原因
zkClient.create().forPath("/node1/00001");
zkClient.create().withMode(CreateMode.PERSISTENT).forPath("/node1/00002");
但是,你运行上面的代码会报错,这是因为的父节点node1还未创建。
你可以先创建父节点 node1 ,然后再执行上面的代码就不会报错了。
zkClient.create().forPath("/node1");
更推荐的方式是通过下面这行代码, creatingParentsIfNeeded() 可以保证父节点不存在的时候自动创建父节点,这是非常有用的。
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath("/node1/00001");
b.创建临时节点
需要注意,只有叶节点可以做临时节点,所以叶节点的父节点必须是永久节点,也就是creatingParentsIfNeeded这个方法创建的父节点必须是永久节点。
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001");
连续的参考下面
public enum CreateMode {
PERSISTENT(0, false, false),
PERSISTENT_SEQUENTIAL(2, false, true),
EPHEMERAL(1, true, false),
EPHEMERAL_SEQUENTIAL(3, true, true);
}
c.创建节点并指定数据内容
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001","java".getBytes());
zkClient.getData().forPath("/node1/00001");//获取节点的数据内容,获取到的是 byte数组
d.检测节点是否创建成功
zkClient.checkExists().forPath("/node1/00001");//不为null的话,说明节点创建成功
a.删除一个子节点
zkClient.delete().forPath("/node1/00001");
b.删除一个节点以及其下的所有子节点
zkClient.delete().deletingChildrenIfNeeded().forPath("/node1");
c.删除指定版本的节点
client.delete().withVersion(0).forPath("/comm_msg_nd");
d.保证删除,失败后继续执行
client.delete().guaranteed().forPath("/comm_msg_nd");
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001","java".getBytes());
zkClient.getData().forPath("/node1/00001");//获取节点的数据内容
zkClient.setData().forPath("/node1/00001","c++".getBytes());//更新节点数据内容
Stat stat = new Stat();
byte[] ctx = cc.getData().storingStatIn(stat).forPath("/comm_msg_nd");
//获取节点内容为ctx
System.out.println(new String(ctx));
//获取该节点stat
//78,78,1573789366124,1573789366124,0,0,0,0,3,0,78
System.out.println(stat);
Stat stat = new Stat();
stat = cc.setData().forPath("/comm_msg_nd","new ctx".getBytes());
System.out.println(stat);
List
ExecutorService es = Executors.newFixedThreadPool(2);
//带线程池的异步接口
client.create().inBackground((client,event)->{
//pool-3-thread-1,CuratorEventImpl{type=CREATE, resultCode=0, path='/abc', name='/abc', children=null, context=null,stat=114,114,1573797468244,1573797468244,0,0,0,0,13,0,114, data=null, watchedEvent=null, aclList=null, opResults=null}
System.out.println(Thread.currentThread().getName()+","+event);
},es).forPath("/abc");
//不带线程池的异步接口
client.delete().inBackground((client,event)->{
//main-EventThread,CuratorEventImpl{type=DELETE, resultCode=0, path='/abc', name='null', children=null, context=null, stat=null, data=null, watchedEvent=null, aclList=null, opResults=null}
System.out.println(Thread.currentThread().getName()+","+event);
}).forPath("/abc");
Thread.sleep(Integer.MAX_VALUE);
1)inBackground() 该方法就是添加一个异步的回调方法,参数是BackgroundCallback接口,是一个函数式接口。
2)BackgroundCallback的接口参数为client(当前客户端实例)及event(服务端事件)
3)事件类型,CuratorEventType,包含如下信息 {type=DELETE, resultCode=0, path='/abc', name='null', children=null, context=null, stat=null, data=null, watchedEvent=null, aclList=null, opResults=null}。type对应的就是操作类型,比如delete对应的delete(),create对应create()等,resultCode是响应码,0代表成功
4)线程池es的作用,通过名称可以看到,默认情况下都是使用main-EventThread线程来串行执行,如果耗时较长会有影响,可以通过定制线程池来缓解这种情况。
zookeeper原生支持通过注册Watcher来进行事件监听,但是其使用并不是特别方便,需要开发人员自己反复注册Watcher,比较繁琐。curator引入了cache来实现zookeeper服务端事件的监听。Cache是Curator中对时间监听的包装,其对事件的监听其实可以近似看作是一个本地缓存视图和远程zookeeper视图的对比过程。
cache分为两类监听类型,节点监听和子节点监听。
NodeCache(监听和缓存根节点变化) 只监听单一个节点(变化 添加,修改,删除)。
PathChildrenCache(监听和缓存子节点变化) 监听这个节点下的所有子节点(变化 添加,修改,删除)。
TreeCache(监听和缓存根节点变化和子节点变化) NodeCache+ PathChildrenCache 监听当前节点及其下的所有子节点的变化。
1、NodeCache
//创建节点数据监听对象
final NodeCache nodeCache = new NodeCache(client, "/hello");
//开始缓存
/**
* start参数为true:可以直接获取监听的节点,System.out.println(nodeCache.getCurrentData());为ChildData{path='/aa', stat=607,765,1580205779732,1580973376268,2,1,0,0,5,1,608
, data=[97, 98, 99, 100, 101]}
* 参数为false:不可以获取监听的节点,System.out.println(nodeCache.getCurrentData());为null
*/
nodeCache.start(true);
System.out.println(nodeCache.getCurrentData());
//添加监听对象
nodeCache.getListenable().addListener(new NodeCacheListener() {
//如果节点数据有变化,会回调该方法
public void nodeChanged() throws Exception {
String data = new String(nodeCache.getCurrentData().getData());
System.out.println("数据Watcher:路径=" + nodeCache.getCurrentData().getPath()
+ ":data=" + data);
}
});
2、PathChildrenCache
//监听指定节点的子节点变化情况包括新增子节点 子节点数据变更 和子节点删除
//true表示用于配置是否把节点内容缓存起来,如果配置为true,客户端在接收到节点列表变更的同时,也能够获取到节点的数据内容(即:event.getData().getData())ͺ如果为false 则无法取到数据内容(即:event.getData().getData())
PathChildrenCache childrenCache = new PathChildrenCache(client,"/hello",true);
/**
* NORMAL: 普通启动方式, 在启动时缓存子节点数据
* POST_INITIALIZED_EVENT:在启动时缓存子节点数据,提示初始化
* BUILD_INITIAL_CACHE: 在启动时什么都不会输出
* 在官方解释中说是因为这种模式会在start执行执行之前先执行rebuild的方法,而rebuild的方法不会发出任何事件通知。
*/
childrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
System.out.println(childrenCache.getCurrentData());
//添加监听
childrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
if(event.getType() == PathChildrenCacheEvent.Type.CHILD_UPDATED){
System.out.println("子节点更新");
System.out.println("节点:"+event.getData().getPath());
System.out.println("数据" + new String(event.getData().getData()));
}else if(event.getType() == PathChildrenCacheEvent.Type.INITIALIZED ){
System.out.println("初始化操作");
}else if(event.getType() == PathChildrenCacheEvent.Type.CHILD_REMOVED ){
System.out.println("删除子节点");
System.out.println("节点:"+event.getData().getPath());
System.out.println("数据" + new String(event.getData().getData()));
}else if(event.getType() == PathChildrenCacheEvent.Type.CHILD_ADDED ){
System.out.println("添加子节点");
System.out.println("节点:"+event.getData().getPath());
System.out.println("数据" + new String(event.getData().getData()));
}else if(event.getType() == PathChildrenCacheEvent.Type.CONNECTION_SUSPENDED ){
System.out.println("连接失效");
}else if(event.getType() == PathChildrenCacheEvent.Type.CONNECTION_RECONNECTED ){
System.out.println("重新连接");
}else if(event.getType() == PathChildrenCacheEvent.Type.CONNECTION_LOST ){
System.out.println("连接失效后稍等一会儿执行");
}
}
});
3、TreeCache
TreeCache treeCache = new TreeCache(client,"/hello");
treeCache.start();
System.out.println(treeCache.getCurrentData("/hello"));
treeCache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
if(event.getType() == TreeCacheEvent.Type.NODE_ADDED){
System.out.println(event.getData().getPath() + "节点添加");
}else if (event.getType() == TreeCacheEvent.Type.NODE_REMOVED){
System.out.println(event.getData().getPath() + "节点移除");
}else if(event.getType() == TreeCacheEvent.Type.NODE_UPDATED){
System.out.println(event.getData().getPath() + "节点修改");
}else if(event.getType() == TreeCacheEvent.Type.INITIALIZED){
System.out.println("初始化完成");
}else if(event.getType() ==TreeCacheEvent.Type.CONNECTION_SUSPENDED){
System.out.println("连接过时");
}else if(event.getType() ==TreeCacheEvent.Type.CONNECTION_RECONNECTED){
System.out.println("重新连接");
}else if(event.getType() ==TreeCacheEvent.Type.CONNECTION_LOST){
System.out.println("连接过时一段时间");
}
}
});
注:在一些Curator版本中以上几个类被标识过期,被CuratorCache取代。使用方法也会有所不同。
// 创建CuratorCache实例,基于路径/father/son/grandson1(这里说的路径都是基于命名空间下的路径)
// 缓存构建选项是SINGLE_NODE_CACHE
CuratorCache cache = CuratorCache.build(curator, "/father/son/grandson1",
CuratorCache.Options.SINGLE_NODE_CACHE);
// 创建一系列CuratorCache监听器,都是通过lambda表达式指定
CuratorCacheListener listener = CuratorCacheListener.builder()
// 初始化完成时调用
.forInitialized(() -> System.out.println("[forInitialized] : Cache initialized"))
// 添加或更改缓存中的数据时调用
.forCreatesAndChanges(
(oldNode, node) -> System.out.printf("[forCreatesAndChanges] : Node changed: Old: [%s] New: [%s]\n",
oldNode, node)
)
// 添加缓存中的数据时调用
.forCreates(childData -> System.out.printf("[forCreates] : Node created: [%s]\n", childData))
// 更改缓存中的数据时调用
.forChanges(
(oldNode, node) -> System.out.printf("[forChanges] : Node changed: Old: [%s] New: [%s]\n",
oldNode, node)
)
// 删除缓存中的数据时调用
.forDeletes(childData -> System.out.printf("[forDeletes] : Node deleted: data: [%s]\n", childData))
// 添加、更改或删除缓存中的数据时调用
.forAll((type, oldData, data) -> System.out.printf("[forAll] : type: [%s] [%s] [%s]\n", type, oldData, data))
.build();
// 给CuratorCache实例添加监听器
cache.listenable().addListener(listener);
// 启动CuratorCache
cache.start();
分布式执行一些不需要同时执行的复杂任务,curator利用zk的特质,实现了这个选举过程。其实就是利用了多个zk客户端在同一个位置建节点,只会有一个客户端建立成功这个特性。来实现同一时间,只会选择一个客户端执行任务
LeaderSelector selector = new LeaderSelector(cc, "/tmp/leader/master", new LeaderSelectorListener() {
@Override
public void takeLeadership(CuratorFramework client) throws Exception {
//成为leader了
System.out.println("do leader work");
Thread.sleep(5000);
System.out.println("end work");
}
@Override
public void stateChanged(CuratorFramework client, ConnectionState newState) {
System.out.println("stateChanged:"+newState);
}
});
selector.autoRequeue();
selector.start();
Thread.sleep(Integer.MAX_VALUE);
String lockPath = "/123/111";
InterProcessMutex lock = new InterProcessMutex(client,lockPath);
lock.acquire();
//do something
lock.release();
curator直接给出了分布式锁的实现。原理是客户端创建锁节点,执行完毕后再删除锁节点。一个客户端先检查是否有锁节点,如果没有,说明可以执行,则创建锁节点去执行。如果有锁节点,则说明现在锁在别的客户端那里,自己则需要等待。
分布式计数器的一个典型应用场景是统计在线人数。
指定一个zookeeper节点作为计数器,多个应用实例在分布式锁的控制下,通过更新该数据节点的内容来实现技术功能。
通过类DistributedAtomicInteger来实现。
//分布式计数器
DistributedAtomicInteger atomicInteger = new DistributedAtomicInteger(client,"/distributed_atomic_counter",new ExponentialBackoffRetry(1000,3));
AtomicValue av = atomicInteger.add(10);
System.out.println(av.succeeded());//true
System.out.println(av.preValue());//0
System.out.println(av.postValue());//10
每个DistributedAtomicXXX里面都有一个AtomicValue,这个是分布式的核心实现类。
AtomicValue trySet(MakeValue makeValue) throws Exception
{
MutableAtomicValue result = new MutableAtomicValue(null, null, false);
//尝试下乐观锁
tryOptimistic(result, makeValue);
if ( !result.succeeded() && (mutex != null) )
{
//失败的话再使用排他锁
tryWithMutex(result, makeValue);
}
return result;
}
Barrier是一种用来控制多线程之间同步的经典方式,在JDK中也自带了CyclicBarrier实现。
curator用DistributeBarrier类来实现。
1)分布式的Barrier(主线程触发)
for (int i = 0; i < 5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" is ready ");
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("ip:port")
.sessionTimeoutMs(2000)
.connectionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.namespace("test")
.build();
client.start();
DistributedBarrier barrier = new DistributedBarrier(client,"/distributed_barrier");
try {
barrier.setBarrier();
barrier.waitOnBarrier();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" run ");
}).start();
}
Thread.sleep(5000);
DistributedBarrier barrier = new DistributedBarrier(cc,"/distributed_barrier");
barrier.removeBarrier();
2)分布式的Barrier(根据等待线程数量触发,同时进入 and 同时退出)
for (int i = 0; i < 5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" is ready ");
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("ip:port")
.sessionTimeoutMs(2000)
.connectionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.namespace("test")
.build();
client.start();
DistributedDoubleBarrier barrier = new DistributedDoubleBarrier(client,"/distributed_barrier",5);
try {
//进入时会等待,5个才会同时进入
barrier.enter();
Thread.sleep(3000);
//退出时依然要等待,5个才会同时退出
barrier.leave();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" run ");
}).start();
}