Zookeeper简介
分布式系统定义面临的问题
我们将分布式系统定义为:分布式系统是同时跨越多个物理主机,独立运行的多个软件组成的系统。分布式系统优点显而易见,人多干活块,并且互为备份。但是缺点也随之而来,以一个小研发团队为例,假设我们有个5人研发团队,要开始项目的研发,会面临以下几种问题:
解决上诉问题的方法就是让信息同步。分布式系统的协调工作就是通过某种方式让各个节点的信息能够同步和共享。这依赖于服务进程之间通信,通信方式有两种:
-
通过网络进行信息共享
犹如现实生活一样,leader会在会议上将任务传达下去,组员通过监听leader的命令或者查看leader的邮件知道自己要干什么。当任务有变化时,leader会单独告诉组员或通过邮件的方式传递给组员。
-
通过共享存储
这就好比leader按照时间和路径将任务表放到git上,组员每天去git上拉去任务表。其中git就是共享存储。更好的做法是,当git有更新时,出发邮件通知各个组员,每个组员接收到邮件后去拉去最新任务表。
zookeeper如何解决分布式面临的问题
zookeeper的分布式系统的协调采用的共享存储,其实共享存储也需要用到网络通信。
实际上zookeeper就像上述例子说的git一样,zookeeper就是git中央存储器,存储了任务的分配和完成情况等共享信息。每个分布式的节点就是组员,订阅这些共享信息。当主节点leader对某个从节点发出任务改变通知时,从节点订阅最新任务。完成工作后,把完成结果通知给leader。
zookeeper基本概念
Zookeeper是一个开源的分布式协调服务,其设计目的就是将一些复杂的容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一些简单的接口提供给客户。zookeeper是一个典型的分布式数据一致性的解决方案,分布式应用可基于zookeeper实现数据的发布订阅、负载均衡、命名服务、集群管理、分布式锁、分布式队列等。
基本概念
- 集群角色
通常集群中每一台机器都有自己的角色,最典型的就是Master/slave主从模式。此情况下把所有能够处理写的机器成为Master,把所有通过异步复制过来获取最新数据,并提供读服务的机器叫slave。
但是在zookeeper中,它没有延续Master/slave主从模式,而是引入了Leader、Follower、ObServer三种角色。
zookeeper集群中的所有机器通过leader选举机制来选举一台leader机器,Leader服务器为客户端提供读写服务。除leader外,其余机器都只提供读服务。ObServer和Follower不同点在于,Observer不参与选举过程。
- 会话(Session)
Session指客户端会话,**一个客户端连接指客户端与服务端之间的一个长TCP连接,**zookeeper对外的端口是2181,客户端启动的时候,首先会与服务器建立一个TCP连接,从第一次客户端连接开始,客户端的会话生命周期也就开始了,通过这个连接,客户端能够心跳检测与服务端的有效会话,也能够向zookeeper服务器发送请求并接收响应。
- 数据节点(Znode)
在分布式中,我们通常说到的“节点”是指每一台机器。然而在zookeeper中,“节点”分为两类,第一类同样是构成集群的机器,我们称之为机器节点。第二类则是数据模型中的数据单元,我们称之为数据节点(Znode)。Zookeeper将所有数据都保存在内存中,数据模型就是一颗树(Znode Tree),由(/)进行分割,例如:/app/znode1。每个ZNode上都会保存自己的数据内容,同时还会保存一些属性内容。
- 版本
对于每个Znode,zookeeper都会为其维护一个叫做Stat的数据结构,Stat记录了这个Znode的三个版本数据,包括Version(当前Znode的版本)、cversion(当前znode子节点的版本)、aversion(当前Znode的ACL版本)。
- Watcher(事件监听器)
事件监听器是zookeeper的一个重要的特性,zookeeper允许用户在指定节点上注册一些watcher,并在一些特定的情况下触发的时候,zookeeper会将服务端的事件通知给感兴趣的客户端,该机制是zookeeper实现分布式协调服务的重要特性。
- ACL
Zookeeper采用了ACL(Access Control List)策略来进行权限控制,其定义了以下五种权限:
1、CREATE:创建子节点的权限;
2、READ:获取子节点数据和子节点列表的权限;
3、WRITE:更新节点数据的权限;
4、DELETE:删除子节点的权限;
5、ADMIN:设置节点ACL的权限。
注意:CREATE和DELETE都是针对子节点的权限。
Zookeeper环境搭建
Zookeeper的搭建方式
- 单机模式:zookeeper只运行在一台机器上,适合测试环境。
- 集群模式:Zookeeper运行在一个集群上,适合生成环境。
- 伪集群模式:zookeeper运行在一台机器上,端口不一样。
单机模式搭建
1、下载Zookeeper实例
http://zookeeper.apache.org/releases.html
2、上传zookeeper
下载后将zookeeper-3.4.14.tar.gz上传到linux服务器上。
3、解压压缩包
tar -zxvf zookeeper-3.4.14.tar.gz -C 目标文件夹
4、进入zookeeper-3.4.14文件夹创建data目录
cd zookeeper-3.4.14
mkdir data
5、修改配置文件名称
cd /conf
mv zoo_sample.cfg zoo.cfg
6、修改zoo.cfg中的dataDir属性
vim zoo.cfg
dataDir=/root/zookeeper-3.4.14/data
7、进入zookeeper bin目录下启动服务
cd /bin
./ zkServer.sh start ## 启动zookeeper
./ zkServer.sh status ## 查看zookeeper状态
./ zkServer.sh stop ## 关闭zookeeper
伪集群模式搭建
- ClientPort
如果在一台机器上部署多个zookeeper实例,那么每台机器都需要不同的clientport,比如server1是2181,server2是2182,server3是2183。
- dataDir和dataLogDir
dataDir和dataLogDir也要区分一下,将数据文件和日志文件分开存放,同时每个server的这两个变量都不一样。
- server.x和myid
server.x这个数字对应data/myid中的数字,在三个server的myid文件分别中写入1、2、3,那么在每个server的zoo.cfg文件中都配置server1、server2、server3。
1、下载zookeeper
http://zookeeper.apache.org/releases.html
2、上传文件
下载后将zookeeper-3.4.14.tar.gz上传到linux服务器上。
3、创建cluster文件夹
mkdir cluster
4、解压压缩包
tar -zxvf zookeeper-3.4.14.tar.gz -C /zkcluster
5、改变文件夹名称
mv zookeeper-3.4.14 zookeeper01
5、复制改名
cp -r zookeeper01/ zookeeper02 cp -r zookeeper01/ zookeeper03
6、分别在 zookeeper01、zookeeper02、zookeeper03 目录下创建data文件夹及logs目录
mkdir data
cd data
mkdir logs
7、修改配置文件名称
cd conf
mv zoo_sample.cfg zoo.cfg
8、配置每个zookeeper的dataDir和dataLogDir
clientPort=2181
dataDir=/zkcluster/zookeeper01/data
dataLogDir=/zkcluster/zookeeper01/data/logs
clientPort=2182
dataDir=/zkcluster/zookeeper02/data
dataLogDir=/zkcluster/zookeeper02/data/logs
clientPort=2183
dataDir=/zkcluster/zookeeper03/data
dataLogDir=/zkcluster/zookeeper03/data/logs
9、配置集群
(1)在每个zookeeper的data目录下创建myid文件,内容分别是1、2、3。这个文件记录每个服务器的ID
touch myid
(2)在每个zookeeper的zoo.cfg配置文件中配置客户端访问端口(clientPort)和集群服务器列表
server.1=10.211.55.4:2881:3881
server.2=10.211.55.4:2882:3882
server.3=10.211.55.4:2883:3883
#server.服务器ID=服务器IP地址:服务器之间通信端⼝:服务器之间投票选举端⼝
10、集群启动
一次启动三个服务,并查看状态。可以看到启动第一个server时,状态报错;当启动第二个server时,再次查看状态,两个server中出现一个leader和follower。
zookeeper的基本使用
zookeeper的系统模型
1、zookeeper数据模型Znode
在zookeeper中,数据信息被保存在一个个数据节点上,这些节点被称为Znode。Znode是zookeeper中最小的数据单元,在Znode下面可以再挂Znode,这样就形成了一个树。
Znode节点路径以(/)进行分割,开发人员可以向该节点添加数据和增加子节点。
2、Znode的类型
Znode的节点类型可以分为三大类:
- 持久性节点(Persistent)
- 临时性节点(Ephemeral)
- 顺序性节点(Sequential)
在开发过程中根据组合不同可以创建以下四种节点:持久性节点、持久顺序性节点、临时性节点、临时顺序性节点。不同的节点有不同的生命周期。
持久性节点: 是zookeeper中最常见的一种节点类型,所谓持久性节点,就是节点被创建之后会一直存在服务器上,直到删除操作主动删除。
持久顺序性节点: 就是有顺序的持久性节点,节点特性和持久性节点一样。只是额外特性表现在顺序上。顺序特性表现在节点被创建的时候回增加一个数字后缀来表示其添加顺序。
临时性节点: 就是会被自动清理的节点。临时性节点的生命周期和客户端绑定在一起,当客户端与服务端会话结束时,临时性节点被删除。临时节点不能创建子节点。
临时顺序性节点: 就是有顺序的临时性节点。
3、事务ID
在zookeeper中事务是指能够改变Zookeeper服务器状态的操作,我们称之为事务操作或者更新操作,一般包括数据节点的创建、删除、数据节点内容的更新等操作。对于每一个事务请求,zookeeper都会为其分配一个去全局唯一的事务ID,用ZXID来表示,通常是一个64为的数字。每一个ZXID对应一次更新操作。
4、Znode的状态信息
整个Znode的节点内容包括两部分:节点内容和节点状态信息。图中的quota就是该节点的数据内容。其他内容属于节点状态信息内容。
- cZxid 就是 Create ZXID,表示节点被创建时的事务ID。
- ctime 就是 Create Time,表示节点创建时间。
- mZxid 就是 Modified ZXID,表示节点最后⼀次被修改时的事务ID。
- mtime 就是 Modified Time,表示节点最后⼀次被修改的时间。
- pZxid 表示该节点的⼦节点列表最后⼀次被修改时的事务 ID。只有⼦节点列表变更才会更新 pZxid, ⼦节点内容变更不会更新。
- cversion 表示⼦节点的版本号。
- dataVersion 表示内容版本号。
- aclVersion 标识acl版本 ephemeralOwner 表示创建该临时节点时的会话 sessionID,如果是持久性节点那么值为 0 dataLength 表示数据⻓度。
- numChildren 表示直系⼦节点数。
5、watcher–数据变更通知
Zookeeper使用Watcher机制来实现分布式数据的发布与订阅。
一个典型的分布式发布与订阅系统定义了一种一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象发生变化时,会通知所有订阅者,使它们做出相应的处理。
在zookeeper中。引入了watcher机制来实现分布式的通知功能,zookeeper允许客户端向服务端注册了一个watcher监听,当服务端的一些特定事件触发率额这个监听器,就会向客户端发出通知。
5、ACL–保障数据的安全
zookeeper提供了一套完整的ACL(access control List)权限控制机制来保障数据的安全。
我们可以从三个方面来理解ACL机制:权限模式(Scheme)、授权对象(ID)、权限(Permission),通常使用scheme:ID:permission来标识一个有效的ACL信息。
-
权限模式(Scheme)
权限模式用于确定权限验证过程使用的策略,主要有以下四种方式
- IP IP模式就是用过IP地址粒度来进行权限控制,如 “ip:192.168.0.110” 表示权限控制针对该IP地址,也支持网段控制,如: “ip:192.168.0.1/24” 。
- Digest 此模式是最常用的模式,更符合我们对权限控制的认识,其使用“username:password”形式来进行权限控制。
- World world是一种最开放的权限模式,此方式基本没什么用,数据节点的访问权限对所有用户开放。world模式是一种特殊的Digest模式,它仅有一个标识符,即“world:anyone”。
- Supper 就是超级用户的意思,也是一种特殊的Digest模式,在supper模式下,超级用户可以对任意zookeeper上的数据节点进行操作。
-
授权对象ID
授权对象ID指权限赋予的用户和一个指定实体,例如IP地址或是机器等。不同的授权模式下,授权对象ID 是不同的。
授权模式 授权对象 IP 通常是一个IP地址或一个网段192.168.10.110 或192.168.10.1/24 Digest 自定义,通常是username:BASE64(SHA-1(username:password)) World 只有一个ID:anyone Supper 超级用户 -
权限
1、CREATE:创建子节点的权限;
2、READ:获取子节点数据和子节点列表的权限;
3、WRITE:更新节点数据的权限;
4、DELETE:删除子节点的权限;
5、ADMIN:设置节点ACL的权限。
注意:CREATE和DELETE都是针对子节
zookeeper命令行操作
- 连接zookeeper
./zkcli.sh 连接本地的zookeeper服务器
./zkCli.sh -server ip:port 连接指定的服务器
- 创建节点
create [-s][-e] path data acl
其中,-s或-e分别指定节点特性,顺序或临时节点,若不指定,则创建持久节点;acl⽤来进⾏权限控制。
① 创建顺序节点
create -s /zk-test 123 创建顺序节点zk-test,节点内容123
② 创建临时节点
create -e /zk-temp 123 创建临时节点 zk-temp 节点内容为123
③ 创建永久节点
create /zk-permanent 123 创建永久节点 zk-permanent 节点内容123
-
读取节点信息
ls命令可以列出zookeeper指定节点下的所有子节点,但只能查看指定节点的下一级的所有子节点;
ls path 其中,path表示的是指定数据节点的节点路径
get命令获取zookeeper指定节点的数据内容和属性信息
get path
-
更新节点
使用set命令可以对指定节点的数据内容进行更新。
set path data [version]
其中data就是要更新的数据内容,version表示数据版本信息
- 删除节点
使用delete命令,可以删除zookeeper上的指定节点,用法如下:
delete path [version]
zookeeper的API的使用
zookeeper作为一个分布式框架,提供了多语言的API,下面列出zookeeper对java语言提供的API接口。
1、导入jar
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
</dependency>
2、创建会话
package com.liangke.zookeeper.client;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
/**
* @author 梁科
* date2020/8/21 15:10
* description
*/
public class CreateSession implements Watcher {
//countDownLatch这个类使⼀个线程等待,主要不让main⽅法结束
private static CountDownLatch countDownLatch=new CountDownLatch(1);
public static void main(String[] args) throws IOException, InterruptedException {
/*
客户端可以通过创建⼀个zk实例来连接zk服务器
new Zookeeper(connectString,sesssionTimeOut,Wather)
connectString: 连接地址:IP:端⼝
sesssionTimeOut:会话超时时间:单位毫秒
Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端)
*/
ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new CreateSession());
countDownLatch.await();
//打印zookeeper状态信息
System.out.println(zooKeeper.getState());
System.out.println("=========Client Connected to zookeeper==========");
}
/**
* 当前类实现了Watcher接⼝,重写了process⽅法,该⽅法负责处理来⾃Zookeeper服务端的
* watcher通知,在收到服务端发送过来的SyncConnected事件之后,解除主程序在CountDownLatch上
* 的等待阻塞,⾄此,会话创建完毕
* @param watchedEvent
*/
@Override
public void process(WatchedEvent watchedEvent) {
//当连接创建了,服务端发送给客户端SyncConnected事件
if(watchedEvent.getState()==Event.KeeperState.SyncConnected){
countDownLatch.countDown();
}
}
}
3、创建节点
package com.liangke.zookeeper.client;
import org.apache.zookeeper.*;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
/**
* @author 梁科
* date2020/8/21 15:10
* description
*/
public class CreateNode implements Watcher {
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws IOException, InterruptedException {
zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new CreateNode());
// countDownLatch.await();
Thread.sleep(Integer.MAX_VALUE);
}
@Override
public void process(WatchedEvent watchedEvent) {
//当连接创建了,服务端发送给客户端SyncConnected事件
if(watchedEvent.getState()==Event.KeeperState.SyncConnected){
try {
createNodeSync();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void createNodeSync() throws KeeperException, InterruptedException {
/**
* path :节点创建的路径
* data[] :节点创建要保存的数据,是个byte类型的
* acl :节点创建的权限信息(4种类型)
* ANYONE_ID_UNSAFE : 表示任何⼈
* AUTH_IDS :此ID仅可⽤于设置ACL。它将被客户机验证的ID替
换。
* OPEN_ACL_UNSAFE :这是⼀个完全开放的ACL(常⽤)-->
world:anyone
* CREATOR_ALL_ACL :此ACL授予创建者身份验证ID的所有权限
* createMode :创建节点的类型(4种类型)
* PERSISTENT:持久节点
* PERSISTENT_SEQUENTIAL:持久顺序节点
* EPHEMERAL:临时节点
* EPHEMERAL_SEQUENTIAL:临时顺序节点
String node = zookeeper.create(path,data,acl,createMode);
*/
//创建持久节点
String liangke_persistent= zooKeeper.create("/liangke_persistent", "创建持久节点".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//创建持久顺序节点
String liangke_persistent_sequential = zooKeeper.create("/liangke_persistent_sequential", "创建持久顺序节点".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
//创建临时节点
String liangke_ephemeral = zooKeeper.create("/liangke_ephemeral", "创建临时节点".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
System.out.println("创建持久节点:"+liangke_persistent);
System.out.println("创建临时节点:"+liangke_ephemeral);
System.out.println("创建持久顺序节点:"+liangke_persistent_sequential);
}
}
4、获取节点数据
package com.liangke.zookeeper.client;
import org.apache.zookeeper.*;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* @author 梁科
* date2020/8/21 15:10
* description
*/
public class GetNode implements Watcher {
//countDownLatch这个类使⼀个线程等待,主要不让main⽅法结束
private static CountDownLatch countDownLatch=new CountDownLatch(1);
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws IOException, InterruptedException {
zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new GetNode());
Thread.sleep(Integer.MAX_VALUE);
}
@Override
public void process(WatchedEvent watchedEvent) {
//当连接创建了,服务端发送给客户端SyncConnected事件
if(watchedEvent.getState()==Event.KeeperState.SyncConnected){
// countDownLatch.countDown();
try {
getNodeData();
getChildrenList();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void getChildrenList() throws KeeperException, InterruptedException {
List<String> lg_persistent = zooKeeper.getChildren("lg_persistent", true);
System.out.println(lg_persistent);
}
private void getNodeData() throws KeeperException, InterruptedException {
/**
* path : 获取数据的路径
* watch : 是否开启监听
* stat : 节点状态信息
* null: 表示获取最新版本的数据
* zk.getData(path, watch, stat);
*/
byte[] data = zooKeeper.getData("/liangke_persistent", true, null);
System.out.println(new String(data));
}
}
5、修改节点数据
package com.lagou.api;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class UpdateNoteData implements Watcher {
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static ZooKeeper zooKeeper;
/*
建立会话
*/
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
/*
客户端可以通过创建一个zk实例来连接zk服务器
new Zookeeper(connectString,sesssionTimeOut,Wather)
connectString: 连接地址:IP:端口
sesssionTimeOut:会话超时时间:单位毫秒
Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端)
*/
zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new UpdateNoteData());
System.out.println(zooKeeper.getState());
// 计数工具类:CountDownLatch:不让main方法结束,让线程处于等待阻塞
//countDownLatch.await();\
Thread.sleep(Integer.MAX_VALUE);
}
/*
回调方法:处理来自服务器端的watcher通知
*/
public void process(WatchedEvent watchedEvent) {
// SyncConnected
if(watchedEvent.getState() == Event.KeeperState.SyncConnected){
//解除主程序在CountDownLatch上的等待阻塞
System.out.println("process方法执行了...");
// 更新数据节点内容的方法
try {
updateNoteSync();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*
更新数据节点内容的方法
*/
private void updateNoteSync() throws KeeperException, InterruptedException {
/*
path:路径
data:要修改的内容 byte[]
version:为-1,表示对最新版本的数据进行修改
zooKeeper.setData(path, data,version);
*/
byte[] data = zooKeeper.getData("/lg-persistent", false, null);
System.out.println("修改前的值:" + new String(data));
//修改/lg-persistent 的数据 stat: 状态信息对象
Stat stat = zooKeeper.setData("/lg-persistent", "客户端修改了节点数据".getBytes(), -1);
byte[] data2 = zooKeeper.getData("/lg-persistent", false, null);
System.out.println("修改后的值:" + new String(data2));
}
}
删除节点
package com.lagou.api;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class DeleteNote implements Watcher {
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static ZooKeeper zooKeeper;
/*
建立会话
*/
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
/*
客户端可以通过创建一个zk实例来连接zk服务器
new Zookeeper(connectString,sesssionTimeOut,Wather)
connectString: 连接地址:IP:端口
sesssionTimeOut:会话超时时间:单位毫秒
Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端)
*/
zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new DeleteNote());
System.out.println(zooKeeper.getState());
// 计数工具类:CountDownLatch:不让main方法结束,让线程处于等待阻塞
//countDownLatch.await();\
Thread.sleep(Integer.MAX_VALUE);
}
/*
回调方法:处理来自服务器端的watcher通知
*/
public void process(WatchedEvent watchedEvent) {
// SyncConnected
if(watchedEvent.getState() == Event.KeeperState.SyncConnected){
//解除主程序在CountDownLatch上的等待阻塞
System.out.println("process方法执行了...");
// 删除节点
try {
deleteNoteSync();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*
删除节点的方法
*/
private void deleteNoteSync() throws KeeperException, InterruptedException {
/*
zooKeeper.exists(path,watch) :判断节点是否存在
zookeeper.delete(path,version) : 删除节点
*/
Stat stat = zooKeeper.exists("/lg-persistent/c1", false);
System.out.println(stat == null ? "该节点不存在":"该节点存在");
if(stat != null){
zooKeeper.delete("/lg-persistent/c1",-1);
}
Stat stat2 = zooKeeper.exists("/lg-persistent/c1", false);
System.out.println(stat2 == null ? "该节点不存在":"该节点存在");
}
}
zookeeper开源客户端
zkClient
ZKClient是github上的一个开源的zookeeper客户端,在zookeeper原生API的接口上进行了封装,是一个更容易用的zookeeper客户端,同时,zkclient在内部还实现了诸如session超时重连、watcher反复注册等功能。
1、添加依赖
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.2</version>
</dependency>
2、创建会话
package com.liangke.zookeeper.zkclient;
import org.I0Itec.zkclient.ZkClient;
/**
* @author 梁科
* date2020/8/25 14:31
* description
*/
public class CreateNode {
/**
* 创建⼀个zkClient实例来进⾏连接
* 注意:zkClient通过对zookeeperAPI内部包装,将这个异步的会话创建过程同步化了
* @param args
*/
public static void main(String[] args) {
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
System.out.println("创建会话session成功");
}
}
3、创建节点
ZkClient提供了递归创建节点的接口,即其可以帮助开发者创建父节点,再创建子节点。
package com.liangke.zookeeper.zkclient;
import org.I0Itec.zkclient.ZkClient;
/**
* @author 梁科
* date2020/8/25 14:31
* description
*/
public class CreateNode {
/**
* 创建⼀个zkClient实例来进⾏连接
* 注意:zkClient通过对zookeeperAPI内部包装,将这个异步的会话创建过程同步化了
* @param args
*/
public static void main(String[] args) {
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
System.out.println("创建会话session成功");
//创建节点
//true 代表递归创建父节点,再创建子节点
zkClient.createPersistent("/liangke/lk-zk",true);
System.out.println("成功创建节点");
}
}
4、删除节点
Zkclient提供递归删除子节点的接口,即帮助开发者删除所有子节点,再删除节点
package com.liangke.zookeeper.zkclient;
import org.I0Itec.zkclient.ZkClient;
/**
* @author 梁科
* date2020/8/25 14:31
* description
*/
public class DeleteNode {
/**
* 创建⼀个zkClient实例来进⾏连接
* 注意:zkClient通过对zookeeperAPI内部包装,将这个异步的会话创建过程同步化了
* @param args
*/
public static void main(String[] args) {
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
System.out.println("创建会话session成功");
String path="/liangke";
zkClient.deleteRecursive(path);
System.out.println("删除节点成功");
}
}
5、获取子节点
package com.lagou.zkclient;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.ZkClient;
import java.util.List;
public class Get_NoteChildren {
/*
借助zkclient完成会话的创建
*/
public static void main(String[] args) throws InterruptedException {
/*
创建一个zkclient实例就可以完成连接,完成会话的创建
serverString : 服务器连接地址
注意:zkClient通过对zookeeperAPI内部封装,将这个异步创建会话的过程同步化了..
*/
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
System.out.println("会话被创建了..");
// 获取子节点列表
List<String> children = zkClient.getChildren("/lg-zkclient");
System.out.println(children);
// 注册监听事件
/*
客户端可以对一个不存在的节点进行子节点变更的监听
只要该节点的子节点列表发生变化,或者该节点本身被创建或者删除,都会触发监听
*/
zkClient.subscribeChildChanges("/lg-zkclient-get", new IZkChildListener() {
/*
s : parentPath
list : 变化后子节点列表
*/
public void handleChildChange(String parentPath, List<String> list) throws Exception {
System.out.println(parentPath + "的子节点列表发生了变化,变化后的子节点列表为"+ list);
}
});
//测试
zkClient.createPersistent("/lg-zkclient-get");
Thread.sleep(1000);
zkClient.createPersistent("/lg-zkclient-get/c1");
Thread.sleep(1000);
}
}
6、获取节点数据
package com.lagou.zkclient;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
public class Note_API {
/*
借助zkclient完成会话的创建
*/
public static void main(String[] args) throws InterruptedException {
/*
创建一个zkclient实例就可以完成连接,完成会话的创建
serverString : 服务器连接地址
注意:zkClient通过对zookeeperAPI内部封装,将这个异步创建会话的过程同步化了..
*/
ZkClient zkClient = new ZkClient("127.0.0.1:2181");
System.out.println("会话被创建了..");
// 判断节点是否存在
String path = "/lg-zkClient-Ep";
boolean exists = zkClient.exists(path);
if(!exists){
// 创建临时节点
zkClient.createEphemeral(path,"123");
}
// 读取节点内容
Object o = zkClient.readData(path);
System.out.println(o);
// 注册监听
zkClient.subscribeDataChanges(path, new IZkDataListener() {
/*
当节点数据内容发生变化时,执行的回调方法
s: path
o: 变化后的节点内容
*/
public void handleDataChange(String s, Object o) throws Exception {
System.out.println(s+"该节点内容被更新,更新的内容"+o);
}
/*
当节点被删除时,会执行的回调方法
s : path
*/
public void handleDataDeleted(String s) throws Exception {
System.out.println(s+"该节点被删除");
}
});
// 更新节点内容
zkClient.writeData(path,"456");
Thread.sleep(1000);
// 删除节点
zkClient.delete(path);
Thread.sleep(1000);
}
}
Curator客户端
Curator客户端是Netflix公司开源的一套zookeeper客户端框架,和zkclient一样,curator解决了很多zookeeper客户端非常底层的细节开发工作,包括连接重连,反复注册watcher和 NodeExistsException 异常等,是最流行的zookeeper客户端之一。从编码上来讲,他提供了Fluent编程风格(类似于连式编程)。
1、添加依赖
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
2、创建会话
Curator的创建会话方式与原生的API和ZkClient的创建方式区别很大。Curator创建客户端是通过CuratorFrameworkFactory工厂类来实现的。具体如下:
1)使用CuratorFrameworkFactory这个工厂类的两个静态方法来创建一个客户端
public static CuratorFramework newClient(String connectString, RetryPolicy retryPolicy)
public static CuratorFramework newClient(String connectString, int sessionTimeoutMs, int connectionTimeoutMs, RetryPolicy retryPolicy)
其中参数RetryPolicy提供重试策略接口,可以让用户实现自定义重试策略,默认提供了以下几种重试策略: ExponentialBackoffRetry (基于backoff的重试策略), ̵RetryNTimes (重连N次重试策略), RetryForever (永远重试策略)。
2)通过CuratorFramework的start()方法启动会话。
// 使用fluent编程风格
RetryPolicy exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(50000)
.connectionTimeoutMs(30000)
.retryPolicy(exponentialBackoffRetry)
.namespace("base") // 独立的命名空间 /base
.build();
//启动会话
client.start();
System.out.println("会话2创建了");
参数说明:
- connectingString:zk的Server地址,多个Server之间用逗号隔开;
- connectionTimeoutMs:连接超时时间,如上是30秒,默认是15秒;
- sessionTimeoutMs:会话超时间,如上是50秒,默认是60秒;
- retryPolicy:失败重试策略
- ExponentialBackoffRetry (基于backoff的重试策略) 构造器有三个参数:
- baseSleepTimeMs :初始的sleep时间,用于计算之后的每次重试的sleep时间,计算公式:当前sleep时间= baseSleepTimeMs*Math.max(1, random.nextInt(1<<(retryCount+1))) 。
- maxRetries:最大重试次数。
- maxSleepMs:最大sleep时间,如果上述的当前时sleep计算出来比这个大,那么sleep用这个时间,默认的最大时间是Integer.MAX_VALUE毫秒。
- 其他,查看 org.apache.curator.RetryPolicy 接口的实现类
- start():完成会话的创建。
- ExponentialBackoffRetry (基于backoff的重试策略) 构造器有三个参数:
3、创建节点
Curator提供了一系列的FLuent风格的接口,通过使用Fluent编程风格的接口,开发人员可以进行自由组合来完成各种类型节点的创建。
(1)创建一个初始内容为空的节点
client.create().forPath(path);
Curator默认创建的是持节点,内容为空。
(2)创建一个包含内容的节点
client.create().forPath(path,"我是内容".getBytes());
(3)递归创建父节点,并选择节点类型
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
package com.lagou.curator;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
public class CreateNote_curator {
// 创建会话
public static void main(String[] args) throws Exception {
//不使用fluent编程风格
RetryPolicy exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3);
// 使用fluent编程风格
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(50000)
.connectionTimeoutMs(30000)
.retryPolicy(exponentialBackoffRetry)
.namespace("base") // 独立的命名空间 /base
.build();
client.start();
System.out.println("会话2创建了");
// 创建节点
String path = "/lg-curator/c1";
String s = client.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT).forPath(path, "init".getBytes());
System.out.println("节点递归创建成功,该节点路径" + s);
}
}
4、删除节点
删除节点的方法也是基于Fluent方式来操作。
(1)删除一个子节点
client.delete().forPath(path);
(2)删除节点并递归删除子节点
client.delete().deletingChildrenIfNeeded().forPath(path);
(3)指定版本进行删除
client.delete().withVersion(1).forPath(path);
如果此版本不存在,则删除异常:
org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode =
BadVersion for
(4)强制保证删除一个节点
client.delete().guaranteed().forPath(path);
只要客户端有效,那么Curator会在后台持续进行删除操作,知道节点删除成功。比如遇到网络异常的情况。
package com.lagou.curator;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class DeleteNote_curator {
// 创建会话
public static void main(String[] args) throws Exception {
//不使用fluent编程风格
RetryPolicy exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3);
CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("127.0.0.1:2181", exponentialBackoffRetry);
curatorFramework.start();
System.out.println( "会话被建立了");
// 使用fluent编程风格
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(50000)
.connectionTimeoutMs(30000)
.retryPolicy(exponentialBackoffRetry)
// .namespace("base") // 独立的命名空间 /base
.build();
client.start();
System.out.println("会话2创建了");
// 删除节点
String path = "/dubbo";
client.delete().deletingChildrenIfNeeded().withVersion(-1).forPath(path);
System.out.println("删除成功,删除的节点" + path);
}
}
5、获取数据
删除节点的方法也是根据Fluent风格方式来操作的。
- 删除一个子节点
client.delete().forPath(path);
- 删除节点并递归删除子节点
client.delete().deletingChildrenIfNeeded().forPath(path);
- 指定版本删除
client.delete().withVersion(1).forPath(path);
如果指定版本不存在,则会抛出异常
org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode =BadVersion for
- 强制保证删除一个节点
client.delete().guaranteed().forPath(path);
只要客户端会话有效,那么Curator会在后台持续进行删除操作,直到节点删除成功为止。
演示实例:
package com.lagou.curator;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class DeleteNote_curator {
// 创建会话
public static void main(String[] args) throws Exception {
//不使用fluent编程风格
RetryPolicy exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3);
CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("127.0.0.1:2181", exponentialBackoffRetry);
curatorFramework.start();
System.out.println( "会话被建立了");
// 使用fluent编程风格
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(50000)
.connectionTimeoutMs(30000)
.retryPolicy(exponentialBackoffRetry)
// .namespace("base") // 独立的命名空间 /base
.build();
client.start();
System.out.println("会话2创建了");
// 删除节点
String path = "/dubbo";
client.delete().deletingChildrenIfNeeded().withVersion(-1).forPath(path);
System.out.println("删除成功,删除的节点" + path);
}
}
6.获取数据
获取节点数据的同时,Curator需要传入一个stat变量来存储返回的最新节点的状态信息。
- 普通查询
client.getData().forPath(path);
- 包含状态的查询
Stat stat = new Stat();
client.getData().storingStatIn(stat).forPath(path);
演示示例:
package com.lagou.curator;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
public class GetNote_curator {
// 创建会话
public static void main(String[] args) throws Exception {
//不使用fluent编程风格
RetryPolicy exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3);
// 使用fluent编程风格
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(50000)
.connectionTimeoutMs(30000)
.retryPolicy(exponentialBackoffRetry)
.namespace("base") // 独立的命名空间 /base
.build();
client.start();
System.out.println("会话2创建了");
// 创建节点
String path = "/lg-curator/c1";
String s = client.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT).forPath(path, "init".getBytes());
System.out.println("节点递归创建成功,该节点路径" + s);
// 获取节点的数据内容及状态信息
// 数据内容
byte[] bytes = client.getData().forPath(path);
System.out.println("获取到的节点数据内容:" + new String(bytes));
// 状态信息
Stat stat = new Stat();
client.getData().storingStatIn(stat).forPath(path);
System.out.println("获取到的节点状态信息:" + stat );
}
}
7.更新数据
更新数据时,如果未传入version参数,那么更新当前最新版本;如果传入了version参数,则更新指定版本的数据,如果version已经变更,则抛出异常。
- 普通更新
client.setData().forPath(path,"新内容".getBytes());
- 指定版本更新
client.setData().withVersion(1).forPath(path);
版本不一致异常信息
org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode =BadVersion for
演示示例:
package com.lagou.curator;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.data.Stat;
public class UpdateNote_curator {
// 创建会话
public static void main(String[] args) throws Exception {
//不使用fluent编程风格
RetryPolicy exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3);
// 使用fluent编程风格
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(50000)
.connectionTimeoutMs(30000)
.retryPolicy(exponentialBackoffRetry)
.namespace("base") // 独立的命名空间 /base
.build();
client.start();
System.out.println("会话2创建了");
// 创建节点
String path = "/lg-curator/c1";
// 获取节点的数据内容及状态信息
// 数据内容
byte[] bytes = client.getData().forPath(path);
System.out.println("获取到的节点数据内容:" + new String(bytes));
// 状态信息
Stat stat = new Stat(); //0
client.getData().storingStatIn(stat).forPath(path);
System.out.println("获取到的节点状态信息:" + stat );
// 更新节点内容 //1
int version = client.setData().withVersion(stat.getVersion()).forPath(path, "修改内容1".getBytes()).getVersion();
System.out.println("当前的最新版本是" + version);
byte[] bytes2 = client.getData().forPath(path);
System.out.println("修改后的节点数据内容:" + new String(bytes2));
// BadVersionException
client.setData().withVersion(stat.getVersion()).forPath(path,"修改内容2".getBytes());
}
}
zookeeper的应用场景
数据发布/订阅
数据发布/订阅(Publish/ Subscribe )系统。发布者将数据发布到zookeeper的节点上,供订阅者进行订阅。
发布/订阅一般有两种模式,分别是推和拉模式,在推模式中,服务端主动将数据更新发送给所有的订阅客户端;拉模式则是由客户端发起拉去数据请求,通常客户端是采用定时进行轮询拉去的方式。
zookeeper是采用推拉相结合的方式:客户端向服务器注册自己关心的节点,一旦该节点数据发送改变,那么服务端会向响应的客户端发送watcher事件通知,客户端接收到这个事件通知后,需要主动到服务器获取最新数据。
如果将配置信息放在zookeeper上进行集中管理,那么在通常情况下,程序在启动的时候会主动到zookeeper中拉配置信息,同时,在相应节点上注册一个watcher监听,这样当该节点下的配置信息发送改变的时候,能及时通知所有的订阅客户端。
在我们的通常开发过程中,会遇到这种情况:系统中需要将一些通用的配置信息,如机器列表信息、运行时的配置开关、数据库配置信息等。这些全局的通用配置信息有以下3个特性:
- 数据量通常很小
- 数据内容在运行时会发生动态变化
- 集群各机器共享,配置一致
接下来我们以一个数据库动态切换的应用场景展开,看zookeeper如何实现配置管理“
1、配置存储
首先需要将配置信息存储在zookeeper中,一般情况下我们需要在zookeeper中选取一个节点来存储配置信息,如: /app1/database_config 。
我们需要将配置信息写到该数据节点中,例如:
#数据库配置信息
#DBCP
dbcp.driverClassName=com.mysql.jdbc.Driver
dbcp.dbJDBCUrl=jdbc:mysql://127.0.0.1:3306/lagou-test
dbcp.username=zm
dbcp.password=1234
dbcp.maxActive=30
dbcp.maxIdle=10
2、配置获取
集群中每台机器在启动初始化的时候,首先会从zookeeper配置节点上读取数据库配置信息,同时,客户端还需在该节点上注册一个watcher监听,一旦数据库配置信息发生改变,所有订阅客户端能够获取到数据变更通知。
- version);
byte[] bytes2 = client.getData().forPath(path);
System.out.println(“修改后的节点数据内容:” + new String(bytes2));
// BadVersionException
client.setData().withVersion(stat.getVersion()).forPath(path,“修改内容2”.getBytes());
}
}
## zookeeper的应用场景
### 数据发布/订阅
数据发布/订阅(Publish/ Subscribe )系统。发布者将数据发布到zookeeper的节点上,供订阅者进行订阅。
发布/订阅一般有两种模式,分别是推和拉模式,在推模式中,服务端主动将数据更新发送给所有的订阅客户端;拉模式则是由客户端发起拉去数据请求,通常客户端是采用定时进行轮询拉去的方式。
zookeeper是采用推拉相结合的方式:客户端向服务器注册自己关心的节点,一旦该节点数据发送改变,那么服务端会向响应的客户端发送watcher事件通知,客户端接收到这个事件通知后,需要主动到服务器获取最新数据。
如果将配置信息放在zookeeper上进行集中管理,那么在通常情况下,程序在启动的时候会主动到zookeeper中拉配置信息,同时,在相应节点上注册一个watcher监听,这样当该节点下的配置信息发送改变的时候,能及时通知所有的订阅客户端。
在我们的通常开发过程中,会遇到这种情况:系统中需要将一些通用的配置信息,如机器列表信息、运行时的配置开关、数据库配置信息等。这些全局的通用配置信息有以下3个特性:
* 数据量通常很小
* 数据内容在运行时会发生动态变化
* 集群各机器共享,配置一致
接下来我们以一个**数据库动态切换**的应用场景展开,看zookeeper如何实现配置管理“
**1、配置存储**
首先需要将配置信息存储在zookeeper中,一般情况下我们需要在zookeeper中选取一个节点来存储配置信息,如: /app1/database_config 。
[外链图片转存中...(img-F9XtgkT9-1600944550268)]
我们需要将配置信息写到该数据节点中,例如:
```properties
#数据库配置信息
#DBCP
dbcp.driverClassName=com.mysql.jdbc.Driver
dbcp.dbJDBCUrl=jdbc:mysql://127.0.0.1:3306/lagou-test
dbcp.username=zm
dbcp.password=1234
dbcp.maxActive=30
dbcp.maxIdle=10
2、配置获取
集群中每台机器在启动初始化的时候,首先会从zookeeper配置节点上读取数据库配置信息,同时,客户端还需在该节点上注册一个watcher监听,一旦数据库配置信息发生改变,所有订阅客户端能够获取到数据变更通知。