Zookeeper
概念
Zookeeper是一个分布式的、开源的分布式应用程序的协调服务
Zookeeper是Apache Hadoop项目下的一个子项目,是一个树形目录服务
其提供的主要功能包括
- 配置管理
- 分布式锁
- 集群管理
数据模型
Zookeeper是一个树形目录服务,其数据模型与Unix的文件系统目录树类似,是层次化结构,数据结构的B树
-
每一个节点称为ZNode
-
每一个节点都会保存自己的数据和节点信息
-
节点可以拥有子节点,同时允许少量数据(1MB)存储在该节点下
-
节点可分为四大类:
-
PERSISTENT 持久化节点
-
EPHEMERAL 临时节点: -e
-
PERSISTENT_SEQUENTIAL 持久化顺序节点: -s
-
EPHEMERAL _SEQUENTIAL 临时顺序节点: -es
-
结构:
命令操作
服务端常用命令
进入目录 : /opt/zookeeper/apache-zookeeper-版本-bin/bin/
cd /opt/ZooKeeper/apache-zookeeper-3.8.0-bin/bin/
启动Zookeeper服务:
./zkServer.sh start
查看Zookeeper服务状态:
./zkServer.sh status
停止Zookeeper服务:
./zkServer.sh stop
重启Zookeeper服务:
./zkServer.sh restart
客户端和java api都可以连接服务端:
客户端常用命令
连接Zookeeper服务端:
- 远程连接:
./zkCli.sh -server ip:port
- 本地连接:
./zkCli.sh -server
断开连接:
quit
查看命令帮助:
help
显示指定目录下节点:
ls 目录
创建节点:
create /节点pate value
获取节点值:
get /节点path
设置节点值:
set /节点path value
删除单个节点:
delete /节点path
删除带有子节点的节点:
deleteall /节点path
创建临时节点:(quit退出后消失)
create -a /节点path value
创建顺序节点:
create -s /节点path value
查询节点详细信息:
ls -s /节点path
- czxid:节点被创建的事务ID
- ctime:创建时间
- mzxid:最后一次被更新的事务ID
- mtime:修改时间
- pzxid:子节点列表最后一次被更新的事务ID
- cversion:子节点的版本号
- dataversion:数据版本号
- aclversion:权限版本号
- ephemeralOwner:用于临时节点,代表临时节点的事务ID,如果为持久节点则为0
- dataLength:节点存储的数据长度
- numChildren:当前节点的子节点个数
例:
JavaAPI操作
常见的Zookeeper Java API:
- 原生Java API
- ZkClient
- Curator
Curator是Apache Zookeeper的java客户端库,目标是简化Zookeeper客户端的使用
官网:http://curator.apache.org/
导入依赖
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.0</version>
</dependency>
Curator API常用操作
建立连接
public class CuratorTest {
/*
* 建立连接
* */
@Test
public void testConnect(){
/**
* @Param connectString 连接字符串 zk server地址和端口
* sessionTimeoutMs 会话超时时间 单位ms
* connectionTimeoutMs 连接超时时间
* retryPolicy 重试策略
*/
//重试策略
RetryPolicy retryPolicy=new ExponentialBackoffRetry(3000,10);
// //1.第一种方式
// CuratorFramework client= CuratorFrameworkFactory.newClient("your_ip:2181",
// 60*1000,15*1000,retryPolicy);
// client.start(); //开启连接
//2.第二种方式
CuratorFramework client = CuratorFrameworkFactory.builder().connectString("your_ip:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy).namespace("lanest666").build(); //命名空间可以默认的加上目录,所有的操作都在此目录下进行
client.start(); //开启连接
}
}
添加节点
1.基本创建
@Test
public void testCreate() throws Exception { //1.基本创建
//如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据存储
String path = client.create().forPath("/test01");
System.out.println(path);
}
2.创建带有数据的节点
@Test
public void testCreate2() throws Exception { //2.创建带有数据的节点
String path = client.create().forPath("/test02","hello".getBytes());
System.out.println(path);
}
3.设置节点的类型
@Test
public void testCreate3() throws Exception { //3.设置节点的类型
//默认类型:持久化
String path = client.create().withMode(CreateMode.EPHEMERAL).forPath("/test03"); //创建临时节点
System.out.println(path);
}
4.创建多级节点
@Test
public void testCreate4() throws Exception { //4.创建多级节点
String path = client.create().creatingParentsIfNeeded().forPath("/test04/p1"); //创建临时节点
System.out.println(path);
}
查询节点
1.查询数据
@Test
public void testGet1() throws Exception{ //1.查询数据: get
byte[] data=client.getData().forPath("/app2");
System.out.println(new String(data));
}
2.查询子节点
@Test
public void testGet2() throws Exception{ //2.查询子节点: ls
List<String> path = client.getChildren().forPath("/");
System.out.println(path);
}
3.查询节点状态信息
@Test
public void testGet3() throws Exception{ //3.查询节点状态信息: ls -s
Stat stat = new Stat();
System.out.println(stat);
client.getData().storingStatIn(stat).forPath("/app2");
System.out.println(stat);
}
修改节点
1.基本修改数据
@Test
public void testSet1() throws Exception{ // 1.基本修改数据
client.setData().forPath("/app2","6666".getBytes());
}
2.根据版本修改
@Test
public void testSet2() throws Exception{ // 2.根据版本修改
Stat stat = new Stat();
client.getData().storingStatIn(stat).forPath("/app2");
int version=stat.getVersion();
System.out.println(version);
client.setData().withVersion(version).forPath("/app2","8888".getBytes());
}
删除节点
1.删除单个节点
@Test
public void testDelete1() throws Exception{ // 1.删除单个节点
client.delete().forPath("/test01");
}
2.删除带有子节点的节点
@Test
public void testDelete2() throws Exception{ // 2.删除带有子节点的节点
client.delete().deletingChildrenIfNeeded().forPath("/test02");
}
3.必须成功的删除: 为了防止网络抖动,本质是重试
@Test
public void testDelete3() throws Exception{ // 3.必须成功的删除
client.delete().guaranteed().forPath("/test02");
}
4.回调
@Test
public void testDelete4() throws Exception{ // 4.回调
client.delete().guaranteed().inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
System.out.println("成功删除...");
System.out.println(curatorEvent);
}
}).forPath("/test04");
}
Watch事件监听
-
ZooKeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务器会将事件通知到感兴趣的客户端上去,该机制是ZooKeeper实现分布式协调服务的重要特性
-
ZooKeeper引入了Watcher机制来实现发布/订阅功能,能让多个订阅者同时监听某一对象,当一个对象状态变化时,会通知所有订阅者
-
ZooKeeper原生支持通过注册Watcher来进行事件监听,但需要反复注册,比较繁琐
-
Curator引入Cache来实现对ZooKeeper服务端事件的监听
-
ZooKeeper提供了三种Watcher:
-
NodeCache:只是监听某一特定的节点
-
PathChildrenCache:监控一个ZNode的子节点
-
TreeCache:可以监控整个树上的所有节点,类似于以上两个的组合
-
NodeCache:给指定一个节点注册监听器
@Test
public void testNodeCache() throws Exception {
//1.创建NodeCache对象
final NodeCache nodeCache = new NodeCache(client,"/app2");
//2.注册监听
nodeCache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
System.out.println("节点变化了....");
//获取修改节点后的数据
byte[] data = nodeCache.getCurrentData().getData();
System.out.println(new String(data));
}
});
//3.开启监听
nodeCache.start(true);
while(true){ //测试用,保证一直运行
}
}
PathChildrenCache:监听某个节点的所有子节点
@Test
public void testPathChildrenCache() throws Exception {
//1.创建PathChildrenCache对象
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);
PathChildrenCacheEvent.Type type = pathChildrenCacheEvent.getType();
if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){
System.out.println("数据更新~");
byte[] data = pathChildrenCacheEvent.getData().getData();
System.out.println(new String(data));
}
}
});
//3.开启监听
pathChildrenCache.start();
while(true){ //测试用,保证一直运行
}
}
TreeCache:监听某个节点自己和所有子节点
@Test
public void testTreeCache() throws Exception {
//1.创建NodeCache对象
TreeCache treeCache = new TreeCache(client,"/app2");
//2.注册监听
treeCache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
System.out.println("节点变化了~");
System.out.println(treeCacheEvent);
}
});
//3.开启监听
treeCache.start();
while(true){ //测试用,保证一直运行
}
}
分布式锁
在进行单机应用开发,涉及并发同步时,往往采用synchronized或Lock的方式来解决多线程间的代码同步问题,此时多线程的运行都是在同一JVM下,不会出现问题
但应用是分布式集群工作的情况下,是多个JVM的工作环境,跨JVM之间已经无法通过多线程的锁来解决同步问题
那么就需要更高级的锁机制–分布式锁:解决跨机器的进程之间的数据同步问题
ZooKeeper分布式锁原理
核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点
- 客户端获取锁时,在lock节点下创建临时顺序节点
- 然后客户端获取lock下的所有字节点,如果发现自己的创建的子节点序号最小,那么就认为该客户端获取到了锁,使用完锁后,将该节点删除
- 如果发现自己创建的节点不是最小的,说明自己还没有获取到锁,此时客户端需找到比自己小的那么节点,同时对其注册事件监听器,监听删除事件
- 如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是lock子节点种序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听
模拟12306售票案例–分布式锁
Curator有五种锁方案:
- InterProcessSemaphoreMutex: 分布式排它锁(非可重入锁)
- InterProcessMutex:分布式可重入排它锁
- InterProcessReadWriteLock:分布式读写锁
- InterProcessMultiLock:将多个锁作为单个实体管理的容器
- InterProcessSemaphoreV2:共享信号量
public class Ticket12306 implements Runnable{ //线程操作资源类
private int ticket =10;
private InterProcessMutex lock;
public Ticket12306(){
RetryPolicy retryPolicy=new ExponentialBackoffRetry(3000,10);
CuratorFramework client = CuratorFrameworkFactory.newClient("your_ip:端口号",
60 * 1000, 15 * 1000, retryPolicy);
client.start();
lock=new InterProcessMutex(client,"/lock");
}
@Override
public void run() {
while (true){
//获取锁
try {
lock.acquire(3, TimeUnit.SECONDS);
if (ticket>0){
System.out.println(Thread.currentThread() + ":" + ticket);
ticket--;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
lock.release(); //释放锁
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Ticket12306 ticket12306 = new Ticket12306();
//创建客户端
Thread t1 = new Thread(ticket12306, "携程");
Thread t2 = new Thread(ticket12306, "飞猪");
Thread t3 = new Thread(ticket12306, "去哪儿旅行");
t1.start();
t2.start();
t3.start();
}
}
集群
Leader选举
- Serverid:服务器ID
比如有三台服务器,编号分别是1,2,3,编号越大在选择算法中的权重越大
- Zxid:数据ID
服务器中存放的最大数据ID值越大说明数据越新,在选举算法中的数据越新权重越大
- 在Leader选举的过程中,如果某台ZooKeeper获得了超过半数的选票,则此ZooKeeper就可以成为Leader了
搭建集群
搭建要求:
真实的集群是需要部署在不同服务器上,在这里我们搭建伪集群,即把所有的服务都搭建在一台虚拟机上,用端口进行区分
搭建一个三个节点的ZooKeeper集群(伪集群):
- 复制三份zookeeper
cp -r apache-zookeeper-3.8.0-bin /usr/zk-cluster/zookeeper01
cp -r apache-zookeeper-3.8.0-bin /usr/zk-cluster/zookeeper02
cp -r apache-zookeeper-3.8.0-bin /usr/zk-cluster/zookeeper03
- 创建data目录,并将conf下zoo_sample.cfg文件改名为zoo.cfg
mkdir /usr/zk-cluster/zookeeper01/data
mkdir /usr/zk-cluster/zookeeper02/data
mkdir /usr/zk-cluster/zookeeper03/data
mv /usr/zk-cluster/zookeeper01/apache-zookeeper-3.8.0-bin/conf/zoo_sample.cfg /usr/zk-cluster/zookeeper01/apache-zookeeper-3.8.0-bin/conf/zoo.cfg
mv /usr/zk-cluster/zookeeper02/apache-zookeeper-3.8.0-bin/conf/zoo_sample.cfg /usr/zk-cluster/zookeeper02/apache-zookeeper-3.8.0-bin/conf/zoo.cfg
mv /usr/zk-cluster/zookeeper03/apache-zookeeper-3.8.0-bin/conf/zoo_sample.cfg /usr/zk-cluster/zookeeper03/apache-zookeeper-3.8.0-bin/conf/zoo.cfg
- 配置每个ZooKeeper的dataDir和clientPort分别为2281,2282,2283
vim /usr/zk-cluster/zookeeper01/apache-zookeeper-3.8.0-bin/conf/zoo.cfg
clientPort=2281
dataDir=/usr/zk-cluster/zookeeper01/data
vim /usr/zk-cluster/zookeeper02/apache-zookeeper-3.8.0-bin/conf/zoo.cfg
clientPort=2282
dataDir=/usr/zk-cluster/zookeeper02/data
vim /usr/zk-cluster/zookeeper03/apache-zookeeper-3.8.0-bin/conf/zoo.cfg
clientPort=2283
dataDir=/usr/zk-cluster/zookeeper03/data
- 在每个ZooKeeper的data目录下创建一个myid文件,内容分别是1、2、3,这个文件就是记录每个服务器的ID
echo 1 >/usr/zk-cluster/zookeeper01/data/myid
echo 2 >/usr/zk-cluster/zookeeper02/data/myid
echo 3 >/usr/zk-cluster/zookeeper03/data/myid
- 在每个ZooKeeper的zoo.cfg配置客户端访问端口(clientPort)和集群服务器IP列表
vim /usr/zk-cluster/zookeeper01/apache-zookeeper-3.8.0-bin/conf/zoo.cfg
vim /usr/zk-cluster/zookeeper02/apache-zookeeper-3.8.0-bin/conf/zoo.cfg
vim /usr/zk-cluster/zookeeper03/apache-zookeeper-3.8.0-bin/conf/zoo.cfg
server.1=your_ip:2881:3881
server.2=your_ip:2882:3882
server.3=your_ip:2883:3883
quorumListenOnAllIPs=true
注:server.服务器ID=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口
- 集群,启动!
/usr/zk-cluster/zookeeper01/apache-zookeeper-3.8.0-bin/bin/zkServer.sh start
/usr/zk-cluster/zookeeper02/apache-zookeeper-3.8.0-bin/bin/zkServer.sh start
/usr/zk-cluster/zookeeper03/apache-zookeeper-3.8.0-bin/bin/zkServer.sh start
- 启动后查看每个实例运行状态
/usr/zk-cluster/zookeeper01/apache-zookeeper-3.8.0-bin/bin/zkServer.sh status
/usr/zk-cluster/zookeeper02/apache-zookeeper-3.8.0-bin/bin/zkServer.sh status
/usr/zk-cluster/zookeeper03/apache-zookeeper-3.8.0-bin/bin/zkServer.sh status
集群故障:上面3个节点的集群,如果2个从服务器都挂掉,主服务器也无法运行,因为可运行的机器没有超过集群总数量的半数
集群角色
在ZooKeeper集群服务中有三个角色:
-
Leader领导者
-
处理事务请求
-
集群内部各服务器的调度者
-
-
Follower跟随着
-
处理客户端非事务请求,转发事务请求给Leader服务器
-
参与Leader选举投票
-
-
Observer观察者
- 处理客户端非事务请求,转发事务请求给Leader服务器
r.sh start
/usr/zk-cluster/zookeeper03/apache-zookeeper-3.8.0-bin/bin/zkServer.sh start
- 启动后查看每个实例运行状态
/usr/zk-cluster/zookeeper01/apache-zookeeper-3.8.0-bin/bin/zkServer.sh status
/usr/zk-cluster/zookeeper02/apache-zookeeper-3.8.0-bin/bin/zkServer.sh status
/usr/zk-cluster/zookeeper03/apache-zookeeper-3.8.0-bin/bin/zkServer.sh status
[外链图片转存中...(img-KW4tYm4o-1693481564194)]
**集群故障:**上面3个节点的集群,如果2个从服务器都挂掉,主服务器也**无法运行**,因为可运行的机器没有超过集群总数量的半数
### 集群角色
在ZooKeeper集群服务中有**三个**角色:
- **Leader领导者**
- 处理事务请求
- 集群内部各服务器的调度者
- **Follower跟随着**
- 处理客户端非事务请求,转发事务请求给Leader服务器
- 参与Leader选举投票
- **Observer观察者**
- 处理客户端非事务请求,转发事务请求给Leader服务器
[外链图片转存中...(img-AW27ILqH-1693481564194)]
-------------------完