Zookeeper学习
1. ZooKeeper 概念
- Zookeeper时ApacheHadoop项目下的一个子项目,是一个树形目录服务
- Zookeeper 翻译过来就是 动物园管理员,它是用来管Hadoop(大象)、Hive、Pig的管理员。简称zk
- Zookeeper 是一个分布式的、开源的分布式应用程序的协调服务。
- Zookeeper 提供的主要功能包括:
- 配置管理
- 分布式锁
- 集群管理
2. ZooKeeper 安装


3. ZooKeeper 命令操作
1. Zookeeper 数据模型
- Zookeeper是一个树形目录服务,其数据模型和Unix的文件系统很类似,拥有一个层次化结构
- 里面的每一个几点都被称为:ZNode,每个节点都会保存自己的数据和节点信息
- 几点可以拥有子节点,同时也允许少量(1MB)数据存储在该节点之下
- 节点可以分为四大类:
- PRESISTENT 持久化节点
- EPHEMERAL 临时节点:-e
- PERSISTENT_SEQUENTIAL 持久化顺序节点:-s
- EPHEMERAL_SEQUENTIAL 临时顺序节点: -es
2. Zookeeper 服务端常用命令
启动Zookeeper服务
./zkServer.sh start
停止Zookeeper服务
./zkServer.sh stop
重启Zookeeper服务
./zkServer.sh restart
查看状态
./zkServer.sh status
3. Zookeeper 客户端常用命令
连接Zookeeper服务端
# ./zkCli.sh -server ip:port
./zkCli.sh -server 101.37.31.159:2181
断开连接
quit
查看帮助命令
help
显示指定目录下节点
# ls 目录
ls /
创建节点
# create /节点path value
create /app1 123
获取节点值
# get /节点path
get /app1
设置节点值
# set /节点path value
create /app2
set /app2 456
删除单个节点
# delete /节点path
delete app2
删除带有子节点的节点
# deleteall /节点path
create /app1/p1 000
deleteall /app1
创建临时节点
create -e /节点path value
创建顺序节点
create -s /节点path value
查询节点详细信息
ls -s /节点path
4. ZooKeeper JavaAPI操作
1. Curator 介绍
Curator 是 Apache ZooKeeper 的Java客户端库
常见的ZooKeeper Java API:
- 原生的Java API
- ZkClient
- Curator
Curator 项目的目标是简化ZooKeeper 客户端的使用
2. Curator API 常用操作
-
建立连接
public class CuratorTest { /** * 建立连接 */ @Test public void testConnect(){ /** * @param connectiString 连接字符串。zk Server 地址和端口"101.37.31.159:2181" * @param sessionTimeOutMs 会话超时时间 单位ms * @param connectionTimeoutMs 连接超时时间 单位ms * @param retryPolicy 重试 */ // 重试策略 RetryPolicy rp = new ExponentialBackoffRetry(3000,10); // 第一种方式 //CuratorFramework client = CuratorFrameworkFactory.newClient("101.37.31.159:2181", 60 * 1000, 15 * 1000, rp); // 第二种方式 CuratorFramework client = CuratorFrameworkFactory.builder().connectString("101.37.31.159:2181") .sessionTimeoutMs(60 * 1000) .connectionTimeoutMs(15 * 1000) .retryPolicy(rp).namespace("caoshikui").build(); // 开启连接 client.start(); } } -
添加节点
public class CuratorTest { // 提升作用域 private CuratorFramework client; /** * 建立连接 */ @BeforeEach public void testConnect(){ /** * @param connectiString 连接字符串。zk Server 地址和窗口"101.37.31.159:2181" * @param sessionTimeOutMs 会话超时时间 单位ms * @param connectionTimeoutMs 连接超时时间 单位ms * @param retryPolicy 重试 */ // 重试策略 RetryPolicy rp = new ExponentialBackoffRetry(3000,10); // 第一种方式 //CuratorFramework client = CuratorFrameworkFactory.newClient("101.37.31.159:2181", 60 * 1000, 15 * 1000, rp); // 第二种方式 client = CuratorFrameworkFactory.builder().connectString("101.37.31.159:2181") .sessionTimeoutMs(60 * 1000) .connectionTimeoutMs(15 * 1000) .retryPolicy(rp).namespace("caoshikui").build(); // 开启连接 client.start(); } /** * 创建节点: create 持久、临时、顺序 数据 * 1. 基本创建 : create.forPath("") * 2. 创建节点,带有数据 : create.forPath("", data) * 3. 设置节点的类型: create.withMode().forPath(""); * 4. 创建多级节点 /app1/p1 : create.creatingParentsIfNeeded().forPath("", data) */ @Test public void testCreateNode() throws Exception { // 1. 基本创建 // 如果创建节点时没有指定数据,则默认将当前客户端的ip作为数据存储 String path = client.create().forPath("/app1"); System.out.println(path); } @Test public void testCreateNode2() throws Exception { // 2. 创建节点,带有数据 // 如果创建节点时没有指定数据,则默认将当前客户端的ip作为数据存储 String path = client.create().forPath("/app2", "shikuicao".getBytes(StandardCharsets.UTF_8)); System.out.println(path); } @Test public void testCreateNode3() throws Exception { // 3. 设置节点的类型 // 默认类型:持久化 String path = client.create().withMode(CreateMode.EPHEMERAL).forPath("/app3"); System.out.println(path); } @Test public void testCreateNode4() throws Exception { // 4. 创建多级节点 // creatingParentsIfNeeded():如果父节点不存在,则创建父节点 String path = client.create().creatingParentsIfNeeded().forPath("/app4/p1"); System.out.println(path); } @AfterEach public void close(){ if(client != null){ client.close(); } } } -
删除节点
/** * delete 删除节点: delete deleteall(递归删除节点) * 1. 删除单个节点 client.delete().forPath() * 2. 删除带有子节点的节点 client.delete().deletingChildrenIfNeeded().forPath() * 3. 必须成功的删除 client.delete().guaranteed().forPath() * 4. 回调 inBackground() */ @Test public void testDeleteNode() throws Exception { // 1. 删除单个节点 client.delete().forPath("/app1"); } @Test public void testDeleteNode2() throws Exception { // 2. 递归删除节点 client.delete().deletingChildrenIfNeeded().forPath("/app4"); } @Test public void testDeleteNode3() throws Exception { // 3. 必须成功删除 client.delete().guaranteed().forPath("/app2"); } @Test public void testDeleteNode4() throws Exception { // 4. 回调 client.delete().guaranteed().inBackground(new BackgroundCallback() { @Override public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception { System.out.println("I have been deleted!"); System.out.println(curatorEvent); } }).forPath("/app1"); } -
修改节点
/** * 修改节点:set * 1. 修改数据: client.setData().forPath() * 2. 根据版本修改: client.setData().withVersion().forPath() * * version 是通过查询出来的。目的就是为了让其他客户端或者线程不干扰我。 * * 保证修改操作是原子性操作 */ @Test public void testUpdateNode() throws Exception { client.setData().forPath("/app1", "zhangyue".getBytes(StandardCharsets.UTF_8)); } @Test public void testUpdateNodeForVersion() throws Exception { Stat status = new Stat(); client.getData().storingStatIn(status).forPath("/app1"); int version = status.getVersion(); // 查询出来的 System.out.println(version); client.setData().withVersion(version).forPath("/app1", "zhangyue".getBytes()); } -
查询节点
/** * 查询节点: * 1. 查询数据:get: client.getData().forPath() * 2. 查询子节点: ls client.getChildren.forPath() * 3. 查询节点状态信息: ls -s client.getData().storingStatIn(Stat对象).forPath() */ @Test public void testGetNodeInfo() throws Exception { // 1. 查询数据:get byte[] bytes = client.getData().forPath("/app1"); System.out.println(new String(bytes)); } @Test public void testGetNodeInfo2() throws Exception { // 2. 查询子节点: ls List<String> path = client.getChildren().forPath("/"); System.out.println(path); } @Test public void testGetNodeInfo3() throws Exception { // 3. 查询节点状态信息: ls -s Stat status = new Stat(); System.out.println(status); byte[] bytes = client.getData().storingStatIn(status).forPath("/app1"); System.out.println(new String(bytes)); System.out.println(status); } -
Watch事件监听
-
ZooKeeper 允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper实现分布式协调服务的重要特性
-
ZooKeeper中引入了Watcher机制来实现了发布/订阅功能,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者
-
ZooKeeper原生支持通过注册Watcher来进行事件监听,但是其使用并不是特别方便,需要开发人员自己反复注册Watcher,比较繁琐
-
Curator引入了Cache来实现对ZooKeeper服务端时间的监听
-
ZooKeeper提供了三种Watcher:
- NodeCache:只是监听某个一个特定的节点
- PathChildrenCache:监控一个ZNode的子节点
- TreeCache:可以监听整个树上的所有节点,类似于前两者的组合。
public class CuratorWatcher { private CuratorFramework client; /** * 建立连接 */ @BeforeEach public void testConnect(){ /** * @param connectiString 连接字符串。zk Server 地址和窗口"101.37.31.159:2181" * @param sessionTimeOutMs 会话超时时间 单位ms * @param connectionTimeoutMs 连接超时时间 单位ms * @param retryPolicy 重试 */ // 重试策略 RetryPolicy rp = new ExponentialBackoffRetry(3000,10); // 第一种方式 //CuratorFramework client = CuratorFrameworkFactory.newClient("101.37.31.159:2181", 60 * 1000, 15 * 1000, rp); // 第二种方式 client = CuratorFrameworkFactory.builder().connectString("101.37.31.159:2181") .sessionTimeoutMs(60 * 1000) .connectionTimeoutMs(15 * 1000) .retryPolicy(rp).namespace("caoshikui").build(); // 开启连接 client.start(); } @AfterEach public void close(){ if(client != null){ client.close(); } } /** * 演示 NodeCache:给指定的一个节点注册监听器 */ @Test public void testNodeCache() throws Exception { // 1. 创建NodeCache对象 NodeCache nodeCache = new NodeCache(client, "/app1"); // 2. 注册监听 nodeCache.getListenable().addListener(new NodeCacheListener() { @Override public void nodeChanged() throws Exception { System.out.println("node had been changed!"); // 获取修改节点后的数据 byte[] data = nodeCache.getCurrentData().getData(); System.out.println(new String(data)); } }); // 3. 开启监听,如果设置为true,则开启监听时,加载缓冲数据 nodeCache.start(true); // 方法不停止,便于监听 while(true){} } /** * 演示 PathChildrenCache:监听某个节点的所有子节点们 */ @Test public void testPathChildrenCache() throws Exception { // 1. 创建PathChildrenPath对象 PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/app2", true); // 2. 注册监听 pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() { @Override public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception { System.out.println("子节点变化了---"); System.out.println(pathChildrenCacheEvent); // 监听子节点的数据变更,并且拿到变更后的数据 // 1. 获取类型 PathChildrenCacheEvent.Type type = pathChildrenCacheEvent.getType(); // 2. 判断类型是否为update if(type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){ byte[] data = pathChildrenCacheEvent.getData().getData(); System.out.println(new String(data)); } } }); // 3. 开启监听,如果设置为true,则开启监听时,加载缓冲数据 pathChildrenCache.start(); // 方法不停止,便于监听 while(true){} } /** * 演示 TreeCache:监听某个节点及所有子节点们 */ @Test public void testTreeCache() throws Exception { TreeCache treeCache = new TreeCache(client, "/app2"); treeCache.getListenable().addListener(new TreeCacheListener() { @Override public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception { System.out.println("节点改变了!阿秋~~"); } }); treeCache.start(); while (true){} } } -
-
分布式锁实现
分布式锁:
- 在我们进行单机应用开发,设计并发同步的时候,我们往往采用synchronized或者Lock方法是来解决多线程间的代码同步问题,这时多线程的运行都是在同一个JVM之下,没有任何问题。
- 但当我们的应用是分布式集群工作的情况下,属于多JVM下的工作环境,跨JVM之间已经无法通过多项成的锁解决同步问题
- 那么就需要一种更加高级的锁机制,来处理跨机器的进程之间的数据同步问题——这就是分布式锁。
3. 分布式锁
ZooKeeper分布式锁原理

-
核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点
![img]()
- 客户端获取锁时,在lock节点下创建临时顺序节点
- 然后获取lock虾米俺的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁,使用完锁后,将该节点删除。
- 如果发现自己创建的节点并非lock所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,同时对其这侧事件监听器,监听删除事件。
- 如果发现比自己小的那个节点被删除,则客户端的Watcher会受到相应通知,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。
Curator实现分布式锁API
在Curator中有5种锁方案:
- InterProcessSemaphoreMutex:分布式排他锁(非可重入锁)
- InterProcessMutes:分布式可重入锁
- InterProcessReadWriteLock:分布式读写锁
- InterProcessMultiLock:将多个锁作为单个实体管理的容器
- InterProcessSemaphoreV2:共享信号量
4. 模拟12306售票案例
Ticket12306类
public class Ticket12306 implements Runnable{
private int tickets = 10; // 数据库中的票数
private InterProcessMutex lock; // 分布式锁
public Ticket12306(){
RetryPolicy rp = new ExponentialBackoffRetry(3000, 10);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("101.37.31.159:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(rp)
.build();
client.start();
lock = new InterProcessMutex(client, "/lock");
}
@Override
public void run() {
while(true){
// 获取锁
try {
lock.acquire(3, TimeUnit.SECONDS);
if(tickets > 0){
System.out.println(Thread.currentThread().getName() + ":" + tickets);
Thread.sleep(100);
tickets--;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
// 释放锁
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
测试
public static void main(String[] args) {
Ticket12306 ticket12306 = new Ticket12306();
// 创建客户端
Thread t1 = new Thread(ticket12306, "携程");
Thread t2 = new Thread(ticket12306, "飞猪");
t1.start();
t2.start();
}
}
5. ZooKeeper 集群搭建
5.1 ZooKeeper 集群介绍

Leader选举:
-
Serverid:服务器ID
比如有三台服务器,编号分别是1,2,3.编号越大在选择算法中的权重越大
-
Zxid:数据ID
服务器种存放的最大数据ID,值越大说明数据越新,在选举算法中数据越新,权重越大
-
在Leader选举的过程中,如果某台ZooKeeper获得了超过半数的选票,则此ZooKeeper就可以成为Leader了。
5.2 ZooKeeper 集群搭建
5.2.1 搭建要求
真实的集群是需要部署在不同的服务器上的,但是在我们测试时同时启动很多个虚拟机内存会吃不消,所以我们通常会搭建伪集群,也就是把所有的服务都搭建在一台虚拟机上,用端口进行区分。
我们这里要求搭建一个三个节点的Zookeeper集群(伪集群)。
5.2.2 准备工作
使用阿里云服务器作为搭建集群的测试服务器
(1)安装JDK 【此步骤省略】。
(2)Zookeeper压缩包上传到服务器
(3)将Zookeeper解压 ,建立/usr/local/zookeeper-cluster目录,将解压后的Zookeeper复制到以下三个目录
/usr/local/zookeeper-cluster/zookeeper-1
/usr/local/zookeeper-cluster/zookeeper-2
/usr/local/zookeeper-cluster/zookeeper-3
[root@localhost ~]# mkdir /usr/local/zookeeper-cluster
[root@localhost ~]# cp -r apache-zookeeper-3.5.6-bin /usr/local/zookeeper-cluster/zookeeper-1
[root@localhost ~]# cp -r apache-zookeeper-3.5.6-bin /usr/local/zookeeper-cluster/zookeeper-2
[root@localhost ~]# cp -r apache-zookeeper-3.5.6-bin /usr/local/zookeeper-cluster/zookeeper-3
(4)创建data目录 ,并且将 conf下zoo_sample.cfg 文件改名为 zoo.cfg
mkdir /usr/local/zookeeper-cluster/zookeeper-1/data
mkdir /usr/local/zookeeper-cluster/zookeeper-2/data
mkdir /usr/local/zookeeper-cluster/zookeeper-3/data
mv /usr/local/zookeeper-cluster/zookeeper-1/conf/zoo_sample.cfg /usr/local/zookeeper-cluster/zookeeper-1/conf/zoo.cfg
mv /usr/local/zookeeper-cluster/zookeeper-2/conf/zoo_sample.cfg /usr/local/zookeeper-cluster/zookeeper-2/conf/zoo.cfg
mv /usr/local/zookeeper-cluster/zookeeper-3/conf/zoo_sample.cfg /usr/local/zookeeper-cluster/zookeeper-3/conf/zoo.cfg
(5)配置每一个Zookeeper 的dataDir 和 clientPort 分别为2182 2183 2184
修改/usr/local/zookeeper-cluster/zookeeper-1/conf/zoo.cfg
vim /usr/local/zookeeper-cluster/zookeeper-1/conf/zoo.cfg
clientPort=2182
dataDir=/usr/local/zookeeper-cluster/zookeeper-1/data
修改/usr/local/zookeeper-cluster/zookeeper-2/conf/zoo.cfg
vim /usr/local/zookeeper-cluster/zookeeper-2/conf/zoo.cfg
clientPort=2183
dataDir=/usr/local/zookeeper-cluster/zookeeper-2/data
修改/usr/local/zookeeper-cluster/zookeeper-3/conf/zoo.cfg
vim /usr/local/zookeeper-cluster/zookeeper-3/conf/zoo.cfg
clientPort=2184
dataDir=/usr/local/zookeeper-cluster/zookeeper-3/data
5.2.3 配置集群
(1)在每个zookeeper的 data 目录下创建一个 myid 文件,内容分别是1、2、3 。这个文件就是记录每个服务器的ID
echo 1 >/usr/local/zookeeper-cluster/zookeeper-1/data/myid
echo 2 >/usr/local/zookeeper-cluster/zookeeper-2/data/myid
echo 3 >/usr/local/zookeeper-cluster/zookeeper-3/data/myid
(2)在每一个zookeeper 的 zoo.cfg配置客户端访问端口(clientPort)和集群服务器IP列表。
集群服务器IP列表如下
vim /usr/local/zookeeper-cluster/zookeeper-1/conf/zoo.cfg
vim /usr/local/zookeeper-cluster/zookeeper-2/conf/zoo.cfg
vim /usr/local/zookeeper-cluster/zookeeper-3/conf/zoo.cfg
# 在三个配置文件的最后都加上如下配置
#### 特殊说明:如果是在同一台机器上(例如阿里云服务器),ip地址用 0.0.0.0
server.1=192.168.149.135:2881:3881
server.2=192.168.149.135:2882:3882
server.3=192.168.149.135:2883:3883
解释:server.服务器ID=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口
5.2.4 启动集群
启动集群就是分别启动每个实例。
/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh start
/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh start
/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh start
启动后我们查询一下每个实例的运行状态
/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh status
/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh status
/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh status
查看mode字段就可以得出该服务是 主(Leader) 还是 从(follower) 机了
5.2.5 模拟集群异常
(1)首先我们先测试如果是从服务器挂掉,会怎么样
把3号服务器停掉,观察1号和2号,发现状态并没有变化
/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh stop
/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh status
/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh status
由此得出结论,3个节点的集群,从服务器挂掉,集群正常
(2)我们再把1号服务器(从服务器)也停掉,查看2号(主服务器)的状态,发现已经停止运行了。
/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh stop
/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh status
由此得出结论,3个节点的集群,2个从服务器都挂掉,主服务器也无法运行。因为可运行的机器没有超过集群总数量的半数。
(3)我们再次把1号服务器启动起来,发现2号服务器又开始正常工作了。而且依然是领导者。
/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh start
/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh status
(4)我们把3号服务器也启动起来,把2号服务器停掉,停掉后观察1号和3号的状态。
/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh start
/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh stop
/usr/local/zookeeper-cluster/zookeeper-1/bin/zkServer.sh status
/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh status
发现新的leader产生了~
由此我们得出结论,当集群中的主服务器挂了,集群中的其他服务器会自动进行选举状态,然后产生新得leader
(5)我们再次测试,当我们把2号服务器重新启动起来启动后,会发生什么?2号服务器会再次成为新的领导吗?我们看结果
/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh start
/usr/local/zookeeper-cluster/zookeeper-2/bin/zkServer.sh status
/usr/local/zookeeper-cluster/zookeeper-3/bin/zkServer.sh status
我们会发现,2号服务器启动后依然是跟随者(从服务器),3号服务器依然是领导者(主服务器),没有撼动3号服务器的领导地位。
由此我们得出结论,当领导者产生后,再次有新服务器加入集群,不会影响到现任领导者。
6. ZooKeeper 集群角色

在ZooKeeper集群服务中有三个角色:
- Leader 领导者:
- 处理事务请求
- 集群内布各服务器的调度者
- Follower 跟随着:
- 处理客户端非事务请求,转发事务请求给Leader服务器
- 参与Leader选举投票
- Observer 观察者:
- 处理客户端非事务请求,转发事务请求给leader服务器

浙公网安备 33010602011771号