首先介绍下 Zookeeper 集群,一个 Zookeeper 集群通常由一组机器组成,一般3~5台集群就可以组成一个 Zookeeper 集群。集群拓扑图基本如下:
Zookeeper 集群中每一个节点都会在内存中维护当前的节点状态,并且彼此之间保持着通信。这里说明一点,只要集群中存在过半的节点正常工作,整个集群就能够对外提供服务。
# 在observer节点的配置文件中添加如下配置
peerType=observer
# 在每个节点的配置文件中,给observer节点添加:observer标识
# 例如:
server.1:localhost:2181:3181:observer
Zookeeper 的数据模型是一棵类似 Unix 文件系统的 ZNode Tree 即 ZNode 树,但是没有引入传统文件系统的目录或者文件等概念,而是使用了称为 “数据节点” 的概念,术语叫做 ZNode。ZNode 是 Zookeeper 存储数据的最小单元,每个 ZNode 可以保存数据,也可以挂载子节点,其中根节点是 /。示意图如下:
使用过 Zookeeper 的同学应该都知道,Zookeeper 主要提供了两个核心功能:
管理(存储、读取)客户端提交的数据;
为客户端提供数据节点的监听服务;
这里就涉及到 Zookeeper 的两个重要特性,就是它的 ZNode 模型与 Watcher 机制。
分别是:
持久性节点(PERSISTENT):客户端与 Zookeeper 断开会话后,该节点依旧存在,直到执行删除操作才会清除节点。
持久性顺序节点(PERSISTENT_SEQUENTIAL):另一种持久节点,Zookeeper 会给该节点名称加上一个数字后缀,进行顺序编号。
临时节点(EPHEMERAL):节点的生命周期和客户端的会话绑定在一起,客户端与 Zookeeper 断开会话后,该节点就会被自动删除。各个场景中很多都是利用 Zookeeper 临时节点这个特性的。
临时顺序节点(EPHEMERAL_SEQUENTIAL):概念和上面类似,Zookeeper 也会给该节点进行顺序编号。
前面提及了 ZNode 是存储数据的最小单元,除了存储用户数据外,ZNode 还有以下特点:
包含 ZNode 修改/访问的时间、事务id(zxid),ACL 权限、版本等状态信息;
所有的事务请求在 ZNode 端都是顺序和原子性的;
数据主要存储在内存中,磁盘中保存事务日志、快照数据等;
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.5</version>
</dependency>
创建 ZNode
private final String ZK_ADDRS = "server01:2181,server02:2181,server03:2181";
private final int SESSION_TIMEOUT = 5000;
private String znodePath = "/my_node";
@Test
public void createZNode() throws IOException, KeeperException, InterruptedException {
//创建zookeeper客户端
ZooKeeper zkClient = new ZooKeeper(ZK_ADDRS, SESSION_TIMEOUT, watchedEvent -> {});
//判断节点是否存在
Stat exist = zkClient.exists(znodePath, false);
if (exist == null){
//创建一个持久节点并写入数据,完全放开acl权限
zkClient.create(znodePath, "123".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//关闭zookeeper连接
zkClient.close();
}
[zk: localhost:2181(CONNECTED) 48] get /my_node
123
cZxid = 0xdb6439ef2
ctime = Fri Feb 27 21:08:09 CST 2020
mZxid = 0xdb6439ef2
mtime = Fri Feb 27 21:08:09 CST 2020
pZxid = 0xdb6439ef2
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 0
删除 ZNode 节点,并监听该节点的删除动作
@Test
public void TestWatcher() throws IOException, KeeperException, InterruptedException {
//创建zookeeper客户端,并注册一个监听器
ZooKeeper zkClient = new ZooKeeper(ZK_ADDRS, SESSION_TIMEOUT, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
//监听指定节点删除事件
if (watchedEvent.getType() == Event.EventType.NodeDeleted &&
watchedEvent.getPath().equals(znodePath)){
log.info(String.format("注意:ZNode '%s' is deleted !", znodePath));
}
}
});
//判断节点是否存在,同时注册了一个 watcher
Stat exist = zkClient.exists(znodePath, true);
if (exist != null){
//删除节点
zkClient.delete(znodePath, -1);
}
//关闭zookeeper连接
zkClient.close();
}
(可左右滑动)
21:41:07.870 [main-EventThread] INFO xxx - 注意:ZNode '/my_node' is deleted !
[zk: localhost:2181(CONNECTED) 50] get /my_node
Node does not exist: /my_node
将配置信息写入 ZooKeeper 的一个 ZNode 中;
各个节点在启动阶段从 Zookeeper 中获取配置,并注册一个数据变更的 Watcher 监听器;
当 ZNode 中的数据被修改,ZooKeeper 将通知各个客户端节点,节点收到通知后进行配置更新;
服务注册,服务提供者启动时会在某一个根 ZNode 节点下创建属于自己的子节点,并写入一些服务信息 比如IP:Port信息;
服务解析,服务使用者在请求服务时会先获取根 ZNode 节点的子节点列表,即服务列表,然后通过一定的负载均衡算法 比如hash选取一个服务访问;
提供类似 JNDI 的功能:就是把各种服务的名称,地址及其他信息放到 Zookeeper 中,使用时去读取,实现资源的定位和使用;
利用 Zookeeper 顺序节点的特性,生成分布式的全局唯一 ID;
分布式协调/通知
Master 节点定期检测 Slave 节点的状态,类似于心跳检测机制;
信息推送,相当于一个发布订阅系统,和第一个场景类似;
记录当前集群中有多少个节点在工作,以及节点的运行状态;
对集群中的节点进行上下线方面的操作;
ZooKeeper 的强一致性,保证只有一个客户端能够创建锁成功。
锁的独占性,创建 ZNode 成功的客户端才能得到锁,其他客户端只能等待,当客户端用完释放锁时,其他客户端再次尝试创建 ZNode,获取分布式锁。
当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。 比如一个 job 由多个 task 组成,只有所有 task 完成后,job 才运行完成,可为 job 创建一个 /job 目录,然后在该目录下,为每个完成的 task 创建一个临时的 ZNode,一旦临时节点数目达到 task 总数,则表明 job 运行完成。
利用 Zookeeper 的临时顺序节点特性,实现 FIFO 即先进先出的队列。
往期文章精选: