1.zookeeper单机版搭建在上一节中有步骤(springboot整合dubbo入门)
2.zookeeper得watcher机制
watcher顾名思义就是监听的意思,ZooKeeper 中,引入了 Watcher 机制来实现这种分布式的通知功能。ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务器的一些特定事件触发了这个 Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。Zookeeper得watcher种类大致有如下几种:EventType.NodeCreated,EventType.NodeDataChanged,EventType.NodeChildrenChanged,EventType.NodeDeleted,EventType.NONE ;下面我们结合zkCli的命令来了解下
2.1 watcher事件的设置和触发
EventType.NodeCreated:主要针对于当前节点节点被创建即触发时间
连接zk客户端后输入help指令可以看到通过那些命令来设置watch
我们使用stat path [watch]来设置节点创建时的watch
注意:如上他会提示你设置的path节点不存在,但是虽然path不存在依然会给他path设置一个watch,当你创建此path的节点时就会触发结果如下,客户端可以看到触发的watcher类型
但是我们看下面的现象:
上面的操作为我先删除上面/watchdemo界面,然后我再去创建此阶段,你会发现NodeCreated类型watcher并没有被触发,这是因为Zookeeper的watcher事件都是一次性的,一旦触发后即被销毁。
EventType.NodeDataChanged:节点数据改触发此事件
我们通过get 命令来设置watch
接着我们改变此节点的值,触发的事件类型及信息如下:
EventType.NodeChildrenChanged:当前节点的子节点发生变化的事件
与之前的有所不同,子节点我们需要使用 ls/ls2 [/path] [watch] 来设置
EventType.NodeDeleted: 节点删除事件
我们一样可是使用get /path watch的方式来设置,具体操作不做展示了;
EventType.NONE:此事件再客户端连接时触发;
由上面的命令演示我们可以总结一下Zookeeper的watcher的一些特性:
1)一次性:
watcher事件被触发后即销毁
2)轻量:
watcher被触发时只会告诉你通知状态、事件类型和节点路径。也就是说,Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容,这样根据不同的类型我们可以进行不同的操作达到不同的目的;
2.1.1 watcher应用场景
它可以应用再如下场景:统一配置管理、分布式锁服务、集群管理等功能
举个列子,我们简单的想下统一配置管理的原理:
通过上面的图我们可以大值看到整个流程:
1)首先我们可以我们的配置文件发生改变的时候我们可以通过一定的触发方式去通知zkServer(例如我们如果把配置文件放在github上当我们的文件发生变化时可以通过webhook的回调通知zkserver不知能否行得通纯猜想。。。。)
2)当我们接受到配置文件改变的通知后我们进行应当的处理来触发设定好的watcher事件,(有人可能会问watcher不是一次性的吗那么这个watcher事件改怎么设置然后达到每次zkserver都能触发呢,这个虽然我们zk提供的watcher时一次性的,但是有很多zk的客户端事件(curatoe)他对watcher进行了封装,可以达到watcher是永久有效)
3)由于多个客户端假设都对 /config这个节点设置了watcher,那么zkserver触发watcher后,所有的客户端都会监听到然后我们可以通过相应的代码对新的配置文件进行处理,比如说将文件下载到对应的位置
上面的内容不一定是正确的,仅作为一种思路;
3.本地搭建zookeeper集群
我们使用不同的虚拟机来模拟不同的服务器,首先我们需要提供三台安装了zookeeper的虚拟机,然后需改/conf/zoo.cfg文件
server.0表示第一台主机 server.1表示第二台依次类推,当然server.x 中的x要和你的data目录下的myid文件内容一致用来确定当前的是那台机器:
三台虚拟机关于zookeeper的配置按照上面的方式依次类推;配置完后一次启动zk服务
一般默认的myid为0的这台机器为主机,其他的为从机通过zkServer.sh status可以进行查看
在集群模式下我们修改一台服务器上的节点信息他会自动同步到其他的服务器上以达到数据的一致性;当集群中的leader不存在时,他会自动的去进行leader的选举,选举的细节如下
3.1 leader的选举
(1) 每个Server发出一个投票。由于是初始情况,Server1和Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。
(2) 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器。
(3) 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下
· 优先检查ZXID。ZXID比较大的服务器优先作为Leader。
· 如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器。
对于Server1而言,它的投票是(1, 0),接收Server2的投票为(2, 0),首先会比较两者的ZXID,均为0,再比较myid,此时Server2的myid最大,于是更新自己的投票为(2, 0),然后重新投票,对于Server2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。
(4) 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于Server1、Server2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了Leader。
(5) 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。
zxid:znode节点的状态信息中包含czxid和mzxid, ZooKeeper状态的每一次改变, 都对应着一个递增的Transaction id, 该id称为zxid. 由于zxid的递增性质, 如果zxid1小于zxid2, 那么zxid1肯定先于zxid2发生. 创建任意节点, 或者更新任意节点的数据, 或者删除任意节点, 都会导致Zookeeper状态发生改变, 从而导致zxid的值增加.
注意:假设服务1之前为leader,然后由于意外原因导致服务宕机,服务3变成了leader,过了一段时间服务1正常的连接上来,那么他也只能作为follower存在不会直接赋予它leader的角色;
4.在springboot 中整合curator进行开发
首先我们加入curator的依赖:
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
<type>pom</type>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
对于集群环境下zk服务器地址可以使用逗号分隔,
CuratorFramework client1 = null; private String zkServerPath = "192.168.139.145:2181,192.168.139.146:2181,192.168.139.148:2181"; private static CountDownLatch countDownLatch = new CountDownLatch(1); public void init() throws Exception{ RetryPolicy retryPolicy = new RetryNTimes(3, 5000); client1 = CuratorFrameworkFactory.builder() .connectString(zkServerPath) .sessionTimeoutMs(10000).retryPolicy(retryPolicy) .namespace("workspace").build(); client1.start(); Thread.currentThread().sleep(1000); }
curator客户端和zookeeper原生的客户端相比较,编写的风格好的多,不想原生的那么繁杂;
RetryPolicy:这个时定义重试机制,上面设置可以解析为重试3次,重试的间隔为5s
namespace:定了这个值后他会在当节点下创建一个workspace的节点之后所有的操作都将时在这个节点之下进行;例如:create /demo 实际上它执行的时create /workspace/demo
默认情况客户端会连接作为leader的服务器,当此服务器挂了后会自动切换到新的leader服务器上去:
此时我们让192.168.139.148这台服务器zookeeper重启在查看日志:
当leader重启后,zookeeper经过重新选举出leader之后客户端自动连接;
4.1 curator的监听监听对原生的zookeeper进行封装,实现了watch永久有效,注册watcher方式时向对应的cache中添加一个监听器;
NodeCache:监听节点的新增、修改操作//最后一个参数表示是否进行压缩 NodeCache cache = new NodeCache(curator, "/super", false); cache.start(true); //只会监听节点的创建和修改,删除不会监听 cache.getListenable().addListener(new NodeCacheListener() { System.out.println("路径:" + cache.getCurrentData().getPath()); System.out.println("数据:" + new String(cache.getCurrentData().getData())); System.out.println("状态:" + cache.getCurrentData().getStat()); });
PathChildrenCache:监听子节点的新增、修改、删除操作。
//第三个参数表示是否接收节点数据内容 PathChildrenCache childrenCache = new PathChildrenCache(curator, "/super", true); /** * start()如果不填写这个参数,则无法监听到子节点的数据更新 如果参数为PathChildrenCache.StartMode.BUILD_INITIAL_CACHE,则会预先创建之前指定的/super节点 如果参数为PathChildrenCache.StartMode.POST_INITIALIZED_EVENT,效果与BUILD_INITIAL_CACHE相同,只是不会预先创建/super节点 参数为PathChildrenCache.StartMode.NORMAL时,与不填写参数是同样的效果,不会监听子节点的数据更新操作 */ childrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT); childrenCache.getListenable().addListener((framework, event) -> { switch (event.getType()) { case CHILD_ADDED: System.out.println("CHILD_ADDED,类型:" + event.getType() + ",路径:" + event.getData().getPath() + ",数据:" + new String(event.getData().getData()) + ",状态:" + event.getData().getStat()); break; case CHILD_UPDATED: System.out.println("CHILD_UPDATED,类型:" + event.getType() + ",路径:" + event.getData().getPath() + ",数据:" + new String(event.getData().getData()) + ",状态:" + event.getData().getStat()); break; case CHILD_REMOVED: System.out.println("CHILD_REMOVED,类型:" + event.getType() + ",路径:" + event.getData().getPath() + ",数据:" + new String(event.getData().getData()) + ",状态:" + event.getData().getStat()); break; default: break; } });
TreeCache:既可以监听节点的状态,又可以监听子节点的状态。类似于上面两种Cache的组合。
final TreeCache nodeCache = new TreeCache(watcher.client1,CONFIG_URL); nodeCache.getListenable().addListener(new TreeCacheListener() { @Override public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception { if("NODE_UPDATED".equals(treeCacheEvent.getType().name())){ System.out.println("-------ClientConfig1---------the resource is update-------------"); System.out.println("--------ClientConfig1--------ready to download file-------------"); countDownLatch.countDown(); } } }); nodeCache.start();
4.2 使用aurtor简单实现资源的统一配置:
首先我们要定义三个客户端,由于客户端的代码基本一直这里只展示一份:
CountDownLatch:用来挂起当前的线程,由于我们测试时使用main方式启动客户端去连接zookeeper服务,通过watcher来回调需要一定的时间我们不挂起当前线程那么可能在回调之前我们的客户端已经断开连接了,而CountDownLatch类可以解决这个问题,首先我们先初始化他的数量为1,然后countDownLatch.await()来挂起当前线程,当我们watcher回调完之后package com.lsm.clients; import org.apache.curator.RetryPolicy; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.framework.recipes.cache.*; import org.apache.curator.retry.RetryNTimes; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import java.util.concurrent.CountDownLatch; /** * Created by lsm on 2018/6/28. */ public class ClientConfig1 implements Watcher{ CuratorFramework client1 = null; private String zkServerPath = "192.168.139.145:2181,192.168.139.146:2181,192.168.139.148:2181"; private static CountDownLatch countDownLatch = new CountDownLatch(1); public void init() throws Exception{ RetryPolicy retryPolicy = new RetryNTimes(3, 5000); client1 = CuratorFrameworkFactory.builder() .connectString(zkServerPath) .sessionTimeoutMs(10000).retryPolicy(retryPolicy) .namespace("workspace").build(); client1.start(); Thread.currentThread().sleep(1000); } /** * 关闭客户端 */ public void closeZKClient() { if (client1 != null) { this.client1.close(); } } @Override public void process(WatchedEvent watchedEvent) { } private final static String CONFIG_URL = "/source"; public static void main(String[] args) throws Exception{ ClientConfig1 clientConfig1 = new ClientConfig1(); clientConfig1.init(); if(clientConfig1.client1.checkExists().forPath(CONFIG_URL) == null) { clientConfig1.client1.create().forPath(CONFIG_URL, "你好".getBytes("UTF-8")); } registerTreeNodeCache(clientConfig1); countDownLatch.await();//挂起当前线程 System.out.println("ClientConfig1 客户端:资源操作完毕。。。。。"); NodeCache nodeCache = new NodeCache(clientConfig1.client1,CONFIG_URL); nodeCache.getListenable().addListener(new NodeCacheListener() { @Override public void nodeChanged() throws Exception { } }); } public static void registerTreeNodeCache(ClientConfig1 watcher) throws Exception{ final TreeCache nodeCache = new TreeCache(watcher.client1,CONFIG_URL); nodeCache.getListenable().addListener(new TreeCacheListener() { @Override public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception { if("NODE_UPDATED".equals(treeCacheEvent.getType().name())){ System.out.println("-------ClientConfig1---------the resource is update-------------"); System.out.println("--------ClientConfig1--------ready to download file-------------"); countDownLatch.countDown(); } } }); nodeCache.start(); } }
countDownLatch.countDown()将countDownLatch数量递减当他的数量为0时将自动执行被挂起的线程;
当我们将三个客户端都启动后我们手动的修改资源路径的节点值,触发watcher
通过结果可以看到当我们修改节点值时三个客户端都进行了资源的下载操作,也就达到了资源的统一配置的效果;