1、Zookeeper是什么?用来干什么?
Zookeeper中文动物管理员,Zookeeper是java语言开发的,它主要用在分布式系统架构中。官方文档上这么解释Zookeeper,它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。我们在分布式系统架构中主要用来实现如下功能:
- 配制中心;
- 分布式锁;
- 服务注册;
- 发布订阅(消息中间件);
- Master选举;
- 分布式队列;
以上6功能,后续会讲解,当然Zookeeper,还能实现很多其它功能,当明白其原理之后,可以按需使用它。首先我们一起来看看Zookeeper的架构:
1、文件系统结构:
Zookeeper维护一个类似文件系统的数据结构,如图:
此图,是用Zookeeper客户端工具(ZooInspector)连结Zookeeper服务器显示的效果,图中文件夹和文件称之为节点(znode),可以理解为linux的目录结构,和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。Zookeeper中将znode分为4类:
1、PERSISTENT-持久节点
客户端与Zookeeper服务器断掉连接后,该节点依旧存在,节点被持久化到磁盘。
2、PERSISTENT_SEQUENTIAL-持久顺序编号节点
客户端与Zookeeper断掉连接后,该节点依旧存在,被持久化到磁盘,创建节点时会自动加序值创建,如图:
3、EPHEMERAL-临时节点
客户端与Zookeeper断掉连接后,该节点被删除。
4、EPHEMERAL_SEQUENTIAL-临时顺序编号节点
客户端与Zookeeper断掉连接后,该节点被删除,创建节点会自动加序值创建。
2、事件监听机制:
客户端注册监听它关心的节点,当节点发生变化(数据改变、被删除、子目录节点增加删除)时,Zookeeper服务会通知客户端。
2、安装Zookeeper:
首先我们得安装Zookeeper,这里介绍Linux系统上的安装,当然可以用Docker。
Step1:配置JAVA环境,检验环境:java -version。
Step2:下载并解压zookeeper。
cd /usr/local
wget http://mirror.bit.edu.cn/apache/zookeeper/stable/zookeeper-3.4.12.tar.gz
tar -zxvf zookeeper-3.4.12.tar.gz
cd zookeeper-3.4.12
Step3:重命名配置文件zoo_sample.cfg。
cp conf/zoo_sample.cfg conf/zoo.cfg
Step4:启动zookeeper。
bin/zkServer.sh start
Step5:检测是否成功启动,用zookeeper客户端连接下服务端。
bin/zkCli.sh
zkCli.sh是Zookeeper自带的客户端,有如下常用命令:
1、使用 ls 命令来查看当前 ZooKeeper 中所包含的内容:
2、创建一个新的 znode ,使用 create /test tdata:
3、下面我们运行 get 命令来确认第二步中所创建的 znode 是否包含我们所创建的字符串:
zkCli.sh使用不方便也不直观,可使用ZooInspector这个客户端管理工具,工具使用很简单,网上找找就行。
安装好Zookeeper后我们就可以开始学习。
3、Zookeeper相关知识学习与使用:
其实Zookeeper的知识很简单,无非就是4类结点,和事件通知,所谓事件通知,就是客户端连上Zookeeper服务后,当节点新增、删除、或节点中值发生变化,会事件通知客户端,基于这一特性可以实现发布订阅、配制中心等。Zookeeper中的节点不能重名,类似于文件系统的文件名,基于这一特性可以实现分布式服务器的master选主。基于事件通知和节点不能重名可实现分布式锁。Zookeeper中节点可以有序,基于这一特性可以实现分布式先进先出队列。另外Zookeeper中出于安全考虑可以设置访问节点的权限。下面将用代码带着大家走一遍。
1、配制中心:
服务器配制中心用来解决服务器在线参数配制,不像传统文件参数配制需要修改配制文件中的值,重启服务器加载到内存。配制中心用到了Zookeeper事件通知特性。
1、首先创建一个Springboot的简单工程项目,并引入Zookeeper原生的Maven依赖,后续会使用第三方客户端。
<dependencies>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.12</version>
</dependency>
</dependencies>
2、通过在服务器上的Zookeeper客户端工具创建一个持久节点/configtest,并设置值test1。
3、在项目中编写如下代码并运行:
public class Application implements Watcher {
private static ZooKeeper zk = null;
private static Stat stat = new Stat();
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
public static void main(String[] args) throws Exception {
// zookeeper配置数据存放路径
String path = "/configtest";
// 连接zookeeper并且注册一个默认的监听器,ZooKeeper实现了Watcher监听
zk = new ZooKeeper("201.37.83.45:2181", 50, new Application());
// 等待zk连接成功的通知
connectedSemaphore.await();
// 获取path目录节点的配置数据
System.out.println(new String(zk.getData(path, true, stat)));
Thread.sleep(Integer.MAX_VALUE);
}
public void process(WatchedEvent event) {
// zk连接成功通知事件
if (KeeperState.SyncConnected == event.getState()) {
if (EventType.None == event.getType() && null == event.getPath()) {
connectedSemaphore.countDown();
System.out.println("connect server!");
} else if (event.getType() == EventType.NodeDataChanged) {
// zk目录节点数据变化通知事件
try {
System.out.println("配置已修改,新值为:" + new String(zk.getData(event.getPath(), true, stat)));
} catch (Exception e) {
}
}
}
// zk断掉连结通知事件
if (KeeperState.Disconnected == event.getState()) {
System.out.println("dissconnect server!");
}
}
}
运行程序,连结Zookeeper服务器成功后,会获取节点的值,如图:
然后通过ZooInspector节点的值,如图:
点击“是”,程序收到事件通知,获取新值如图:
2、节点权限:
Zookeeper节点的数据结构非常类似于Linux文件系统的结构。在Linux文件系统中,每个文件或目录针对不同的用户和用户组都具有相应的rwx权限。同样的,在Zookeeper中,每个节点针对不同的用户或主机也具有相应的权限。下面我们来看下Zookeeper中节点权限的基本概念。
在Zookeeper中,权限模式有两种类型,分别是ip和digest。
1、ip模式是基于ip白名单的方式指定某个服务器具有那些权限;
2、digest模式是基于用户名和密码的方式指定谁具有那些权限;
在ip权限模式下,ID就是具体的ip地址字符串,在digest权限模式下,ID是username:Base64(Sha1(username:password))字符串。在Zookeeper中,有create(c)、delete(d)、read(r)、write(w)和admin(a)这五种权限类型。
下面用代码示例digest权限。
1、首先创建一个Springboot的简单工程项目,并引入Zookeeper第三方客户端的Maven依赖。
<dependencies>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
</dependencies>
2、在项目中编写如下代码并运行:
public class Application {
// zk连接地址
private static final String address = "201.37.83.45:2181";
// zk连接客户端
protected static ZkClient zkClient = new ZkClient(address);
public static void main(String[] args) {
try {
// 构造权限信息
List<ACL> acl = new ArrayList<ACL>();
// 指定用户名和密码的权限
acl.add(new ACL(ZooDefs.Perms.ALL, /* 指定所有权限类型 */
new Id("digest", DigestAuthenticationProvider.generateDigest("admin:admin123"))));
// 创建节点
zkClient.create("/idacl", "helloworld!", acl, CreateMode.EPHEMERAL);
// 未添加授权信息,读取失败
try {
System.out.println(zkClient.readData("/idacl"));
} catch (Exception e) {
System.out.println("权限不够");
}
// 添加授权信息
zkClient.addAuthInfo("digest", "admin:admin123".getBytes());
try {
System.out.println(zkClient.readData("/idacl"));
} catch (Exception e) {
System.out.println("权限不够");
return;
}
System.out.println("有权限读取");
} catch (Exception ex) {
ex.printStackTrace();
}
return;
}
}
运行结果:
3、分布式锁:
为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。如集群服务器中跑批处理,某一时刻仅需一个服务器工作。
下面用代码示例Zookeeper实现分布式锁,主要用到了Zookeeper的节点不能同名、事件通知(节点删除的事件通知)和临时节点连结断掉就自动删除的特性。
1、首先创建一个Springboot的简单工程项目,并引入Zookeeper第三方客户端的Maven依赖:
<dependencies>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
</dependencies>
2、在项目中编写如下代码并运行:
第一个Class,锁接口的定义:
/*
* 分布式锁的接口定义
*/
public interface IDistributedLock {
void lock();
void unLock();
}
第二个Class,用Zookeeper实现的分布式锁:
/*
* 用Zookeeper实现分布式锁
*/
public class ZooKeeperDistributedLock implements IDistributedLock {
// zk连接地址
private static final String address = "201.37.83.45:2181";
// zk临时节点
protected static final String path = "/lock";
// zk连接客户端
protected ZkClient zkClient = new ZkClient(address);
protected CountDownLatch countDownLatch = null;
public void lock() {
// 尝试去上锁
if (tryLock()) {
System.out.println("###上锁成功###");
return;
}
// 等待去上锁
waitLock();
// 再次去上锁
lock();
}
public void unLock() {
if (null != zkClient) {
zkClient.close();
}
}
private boolean tryLock() {
try {
zkClient.createEphemeral(path);
} catch (Exception e) {
return false;
}
return true;
}
private void waitLock() {
// 使用事件监听,获取到节点被删除
IZkDataListener iZkDataListener = new IZkDataListener() {
// 当节点被删除的时候
public void handleDataDeleted(String dataPath) throws Exception {
if (null != countDownLatch) {
// 信号量减一,唤醒
countDownLatch.countDown();
}
}
// 当节点发生改变
public void handleDataChange(String dataPath, Object data) throws Exception {
}
};
// 注册节点监听事件
zkClient.subscribeDataChanges(path, iZkDataListener);
// 检测节点是否存在,如果存在则等待
if (zkClient.exists(path)) {
// 创建信号量
countDownLatch = new CountDownLatch(1);
try {
// 进行等待
countDownLatch.await();
} catch (Exception e) {
}
}
// 删除节点事件通知
zkClient.unsubscribeDataChanges(path, iZkDataListener);
}
}
第三个Class,线程共享资源的实现,用以模拟多进程:
public class TestRunnable implements Runnable {
private boolean bIfLock;
private CountDownLatch countDownLatch;
private IDistributedLock distributedLock;
private static int showTimes;
/*
* 该类实现线程接口,测试时多个线程,每次加showTimes+1打印值,然后再++showTimes;
* 模拟多服务器多进程共享资源使用,如果不加锁将会出现大量重复的值(如果是火车票,多人将会拿到同一张票)。
*/
public TestRunnable(boolean bIfLock, CountDownLatch countDownLatch) {
this.bIfLock = bIfLock;
this.countDownLatch = countDownLatch;
if (bIfLock) {
distributedLock = new ZooKeeperDistributedLock();
}
}
public void run() {
String threadName = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
if (bIfLock) {
distributedLock = new ZooKeeperDistributedLock();
distributedLock.lock();
}
/* 不使用分布式事务,很容易出现重复 */
System.out.println(threadName + (TestRunnable.showTimes + 1));
++TestRunnable.showTimes;
if (null != distributedLock) {
distributedLock.unLock();
distributedLock = null;
}
}
countDownLatch.countDown();
}
}
第四个Class,主程序调用:
/*
* 主程序
*/
public class Application {
public static void main(String[] args) {
int iTimes = 3;
CountDownLatch countDownLatch = new CountDownLatch(iTimes);
Thread thread = null;
TestRunnable runnable = null;
for (int i = 0; i < iTimes; i++) {
runnable = new TestRunnable(true/* false不开启分布式事务 */, countDownLatch);
thread = new Thread(runnable, "the " + i + " Tread ");
thread.start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return;
}
}
不使用锁的运结果:
使用锁的运结果:
4、分布式队列:
分布式队列简单理解就是实现跨进程、跨主机、跨网络的数据共享与数据传递,借助Zookeeper可以处理两种类型的分布式队列:
1、同步队列:
当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。
如:一个公司去旅游,看是否所有人都到齐了,到齐了才发车;
如:一个大任务分解为多个子任务,需所有子任务都完成了才能进入到下一流程;
可在Zookeeper中先创建一个根目录/sync_queue,做为队列,队列的消费者监视/sync_queue节点,入队列操作就是在/sync_queue下创建子节点,然后计算子节点的总数,看是否和队列的目标数量相同,由于/sync_queue这个节点有了状态变化,Zookeeper就会通知监视者,监视者得到通知后进行目标数量相等判断。
2、先进先出队列:
按照FIFO方式进行入队和出队,在Zookeeper中先创建一个根目录/fifo_queue,做为队列,入队列操作就是在/fifo_queue下创建自增序的子节点,并把数据放入节点内,出队列操作就是先找到/fifo_queue下序号最小的那个节点,取出数据,然后删除此节点。
下面用代码示例Zookeeper的有序节点。
1、首先创建一个Springboot的简单工程项目,并引入Zookeeper第三方客户端的Maven依赖。
<dependencies>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
</dependencies>
2、在项目中编写如下代码并运行:
public class Application {
// zk连接地址
private static final String address = "201.37.83.45:2181";
// zk连接客户端
protected static ZkClient zkClient = new ZkClient(address);
public static void main(String[] args) {
// 判断持久节点是否存在,存在无需再创建,zookeeper不允许有同名结点。
if (!zkClient.exists("/sroot")) {
zkClient.createPersistent("/sroot"); // 创建持久结点
// zkClient.createEphemeral("/sroot"); // 创建临时结点,临时结点不能做为父结点。
}
/*
* 有序节点就是创建时,会自动在节点后面加一个序号,基于这特性,对于节点创建时,可以使用同名节点名, 因为真正创建时,会自动加上序号。
*/
for (int i = 0; i < 10; i++) {
zkClient.createEphemeralSequential("/sroot/sequential", i + ""); // 创建有序临时结点
}
/*
* 有序节点读值,可以用父子节点读法获取。
*/
List<String> children = zkClient.getChildren("/sroot");
for (int i = 0; i < children.size(); i++) {
System.out.println(zkClient.readData("/sroot/" + children.get(i)));
}
System.out.println("run over");
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
return;
}
}
运行结果如图:
以上所述,不是Zookeeper的所有用法,仅是一些常用的用法,其它用法,请感兴趣的朋友自己去了解,另外以上所写,可能有一些问题,望留言纠正,或请加qq(907128466)群一起学习讨论,谢谢每位观看到朋友!
下一篇文章将会继续述Zookeeper集群相关知识。