声明:此博客为学习笔记,学习自极客学院ZooKeeper相关视频;本文内容是本人照着视频里的前辈所讲知识敲了
一遍的记录,个别地方按照本人理解稍作修改。非常感谢众多大牛们的知识分享。
相关概念:
发布订阅(相关节点)架构图:
注:确切的说,servers节点下的每一个子节点对应的都是一个zookeeper客户端,在条件允许的情况下,一般是一
个服务器对应一个客户端,所以一定程度上可以认为servers节点下的每一个子节点对应一个服务器。
manager server、command server也是一样的。
节点功能说明:如上图所示,zookeeper有三个子节点,从上往下第一个节点为config配置节点;第二个为servers
服务器集群节点;第三个节点为command命令节点;servers节点下又有若干服务器节点(每当往集群中
增加一台用于工作的服务器时,就会在servers节点下创建一个对应的子节点)。特别的,有一个
command server指令客户端(一定程度上也可以认为是一个服务器)、manager server管理客户
端(一定程度上也可以认为是一个服务器)。
监听关系说明:worker server系列的服务器会监控(订阅)config节点,当config节点发生变化时,worker server系列
的服务器会更新自己的配置。manager serer通过监听servers节点的子节点改变事件来更新自己内存
中工作服务器列表信息。command server对应的客户端通过向command节点写入(或修改)信息,进
而触发manager server对应的客户端(manager server监听了command节点),该客户端再向config
节点写入(或修改)信息,进而触发worker server发生相应变化。
manager server程序主流程图:
worker server程序主流程图:
软硬件环境:Windows10、IntelliJ IDEA、SpringBoot、ZkClient客户端、fastjson
准备工作:在pom.xml中引入相关依赖
<!--
https://mvnrepository.com/artifact/com.101tec/zkclient
ZkClient客户端所需依赖
-->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
发布订阅代码实现示例
各个类总体说明:
CommandPOJO:对/command节点的数据要求。该节点的数据应符合此模型。
ConfigPOJO:各个WorkerServer的配置信息模型,且/config节点的数据要符合此模型要求。
ManageServer:即manage server服务器(客户端)。
WorkerServer:即worker server服务器(客户端)。
WorkerServerParamModel:worker server服务器(客户端)的基本信息参数模型。
(注:也可以将WorkerServerParamModel类与WorkerServer类融合起来。)
SubscribeReleaseTest:测试发布订阅的测试类。
各个类细节实现:
CommandPOJO:
/**
* command节点数据 模型
*
* @author JustryDeng
* @date 2018/11/29 9:55
*/
public class CommandPOJO {
/** 指令信息 */
private String cmd;
/** 此指令涉及到的configPOJO */
private ConfigPOJO configPOJO;
public String getCmd() {
return cmd;
}
public void setCmd(String cmd) {
this.cmd = cmd;
}
public ConfigPOJO getConfigPOJO() {
return configPOJO;
}
public void setConfigPOJO(ConfigPOJO configPOJO) {
this.configPOJO = configPOJO;
}
@Override
public String toString() {
return "CommandPOJO{cmd='" + cmd + "', configPOJO=" + configPOJO + '}';
}
/**
* 造点符合逻辑的command节点数据要求的数据,以便 拿此数据直接使用ZkClient命令行客户端测试
* // TODO 真正使用时,记得去掉此无用主函数
*
* @author JustryDeng
* @date 2018/11/29 11:20
*/
public static void main(String[] args) {
CommandPOJO c1= new CommandPOJO();
ConfigPOJO f1 = new ConfigPOJO();
f1.setNameConfig("测试list");
c1.setCmd("list");
c1.setConfigPOJO(f1);
CommandPOJO c2= new CommandPOJO();
ConfigPOJO f2 = new ConfigPOJO();
f2.setNameConfig("modify");
c2.setCmd("modify");
c2.setConfigPOJO(f2);
CommandPOJO c3= new CommandPOJO();
ConfigPOJO f3 = new ConfigPOJO();
f3.setNameConfig("测试create");
c3.setCmd("create");
c3.setConfigPOJO(f3);
System.out.println(com.alibaba.fastjson.JSON.toJSONString(c1));
// 输出: {"cmd":"list","configPOJO":{"ageConfig":0,"nameConfig":"测试list"}}
System.out.println(com.alibaba.fastjson.JSON.toJSONString(c2));
// 输出: {"cmd":"modify","configPOJO":{"ageConfig":0,"nameConfig":"modify"}}
System.out.println(com.alibaba.fastjson.JSON.toJSONString(c3));
// 输出: {"cmd":"create","configPOJO":{"ageConfig":0,"nameConfig":"测试create"}}
}
}
注:需要造几条符合此模型的数据,以便本人后续在运行主函数测试时。直接使用zkCli.sh命令行客户端修改/command节
点数据,触发相应事件进行测试。
ConfigPOJO:
import java.io.Serializable;
/**
* config节点数据 模型
*
* 注:由于涉及到这个模型的ZkClient不需要使用SerializableSerializer,而是使用BytesPushThroughSerializer,
* 所以此模型也可以不实现Serializable接口
*
* @author JustryDeng
* @date 2018/11/28 17:52
*/
public class ConfigPOJO implements Serializable {
/** 配置姓名 */
private String nameConfig;
/** 配置年龄 */
private int ageConfig;
/** 配置性别 */
private String genderConfig;
/** 配置座右铭 */
private String mottoConfig;
public String getNameConfig() {
return nameConfig;
}
public void setNameConfig(String nameConfig) {
this.nameConfig = nameConfig;
}
public int getAgeConfig() {
return ageConfig;
}
public void setAgeConfig(int ageConfig) {
this.ageConfig = ageConfig;
}
public String getGenderConfig() {
return genderConfig;
}
public void setGenderConfig(String genderConfig) {
this.genderConfig = genderConfig;
}
public String getMottoConfig() {
return mottoConfig;
}
public void setMottoConfig(String mottoConfig) {
this.mottoConfig = mottoConfig;
}
@Override
public String toString() {
return "ConfigPOJO{nameConfig='" + nameConfig + "', ageConfig=" + ageConfig
+ "', genderConfig='" + genderConfig + ", mottoConfig='" + mottoConfig + "'}";
}
}
ManageServer:
import com.alibaba.fastjson.JSON;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNoNodeException;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;
import java.util.List;
/**
* 集群中managerServer服务器 相关逻辑实现
* <p>
* 此WorkerServer实例主要做的事有:
* 1.监听/servers节点的子节点改变事件,并作出相应处理
* 2.监听/command节点的数据改变事件,并作出相应处理
*
* @author JustryDeng
* @date 2018/11/28 23:31
*/
public class ManageServer {
/** 节点分割符 */
private final String NODE_SPLITTER = "/";
/** ZkClient实例 */
private ZkClient zkClient;
/** servers节点路径 */
private String serversNodePath;
/** command节点路径 */
private String commandNodePath;
/** config节点路径 */
private String configNodePath;
/** command节点数据 */
private CommandPOJO commandNodeData;
/** 监听servers节点的子节点变化的监听器 */
private IZkChildListener serversNodeChildListener;
/** 监听config节点数据变化的监听器 */
private IZkDataListener commandNodeDataListener;
/** servers节点下的所有节点(即:WorkerServer集合) */
private List<String> workerServerList;
/**
* 构造器
*
* @param zkClient
* ZkClient实例
* @param serversNodePath
* servers节点路径
* @param commandNodePath
* command节点路径
* @param configNodePath
* config节点路径
* @return
* @throws
* @date 2018/11/28 23:43
*/
public ManageServer(ZkClient zkClient, String serversNodePath, String commandNodePath, String configNodePath) {
this.zkClient = zkClient;
this.serversNodePath = serversNodePath;
this.commandNodePath = commandNodePath;
this.configNodePath = configNodePath;
// 实例化 serversNodeChildListener监听器
this.serversNodeChildListener = new IZkChildListener() {
public void handleChildChange(String parentPath,
List<String> currentWorkerServerList) {
workerServerList = currentWorkerServerList;
System.out.println("servers节点的子节点列表发生了变化!");
execList();
}
};
// 实例化 commandNodeDataListener
this.commandNodeDataListener = new IZkDataListener() {
public void handleDataDeleted(String dataPath) {
// do something
}
public void handleDataChange(String dataPath, Object data) {
String commandNodeDataJsonString = new String((byte[]) data);
commandNodeData = JSON.parseObject(commandNodeDataJsonString, CommandPOJO.class);
exeCmd();
}
};
}
/**
* 启动ManagerServer
*
* @author JustryDeng
* @date 2018/11/28 23:51
*/
public void start() {
// 判断command节点是否存在,不存在则创建
createCommandNode();
// 监听(订阅)command节点的数据变化
zkClient.subscribeDataChanges(commandNodePath, commandNodeDataListener);
// 监听(订阅)servers节点的子节点变化
zkClient.subscribeChildChanges(serversNodePath, serversNodeChildListener);
}
/**
* 停止ManagerServer
*
* @author JustryDeng
* @date 2018/11/28 23:51
*/
public void stop() {
// 取消 监听(订阅)command节点的数据变化
zkClient.unsubscribeDataChanges(commandNodePath, commandNodeDataListener);
// 取消 监听(订阅)servers节点的子节点变化
zkClient.unsubscribeChildChanges(serversNodePath, serversNodeChildListener);
}
/**
* 创建command节点
*/
private void createCommandNode() {
if (!zkClient.exists(commandNodePath)) {
try {
// command是持久型节点
zkClient.createPersistent(commandNodePath);
} catch (ZkNodeExistsException e) {
// 如果此时,别的线程恰好刚刚创建了该节点,那么return
return;
} catch (ZkNoNodeException e) {
// 如果父节点不存在,那么同时创建父节点
String parentNodePath = configNodePath.substring(0, commandNodePath.lastIndexOf(NODE_SPLITTER));
zkClient.createPersistent(parentNodePath, true);
createCommandNode();
} catch (Exception e) {
System.out.println("发生其他异常了!" + e.getMessage());
}
}
}
/**
* 执行command指令
* <p>
* 自定义的命令有:
* list 列出servers节点下的所有子节点
* create 创建config节点
* modify 修改config节点数据
*/
private void exeCmd() {
String cmd = commandNodeData.getCmd();
if ("list".equals(cmd)) {
System.out.println("检测到command中的数据发生了变化,处理相应的list指令");
execList();
} else if ("create".equals(cmd)) {
System.out.println("检测到command中的数据发生了变化,处理相应的create指令!");
execCreate();
} else if ("modify".equals(cmd)) {
System.out.println("检测到command中的数据发生了变化,处理相应的modify指令!");
execModify();
} else {
System.out.println("不存在此指令:" + cmd);
}
}
/**
* list
*/
private void execList() {
System.out.println("此时servers节点下的所有子节点有:" + workerServerList.toString());
}
/**
* create
*/
private void execCreate() {
if (!zkClient.exists(configNodePath)) {
try {
// config是持久型节点
zkClient.createPersistent(configNodePath, JSON.toJSONString(commandNodeData.getConfigPOJO()).getBytes());
} catch (ZkNodeExistsException e) {
// 如果此时,别的线程恰好刚刚创建了该节点,那么修改该节点的数据
zkClient.writeData(configNodePath, JSON.toJSONString(commandNodeData.getConfigPOJO()).getBytes());
} catch (ZkNoNodeException e) {
// 如果父节点不存在,那么同时创建父节点
String parentNodePath = configNodePath.substring(0, configNodePath.lastIndexOf(NODE_SPLITTER));
zkClient.createPersistent(parentNodePath, true);
execCreate();
} catch (Exception e) {
System.out.println("发生其他异常了!" + e.getMessage());
}
}
}
/**
* modify
*/
private void execModify() {
try {
zkClient.writeData(configNodePath, JSON.toJSONString(commandNodeData.getConfigPOJO()).getBytes());
} catch (ZkNoNodeException e) {
// 节点不存在则创建节点
execCreate();
} catch (Exception e) {
System.out.println("发生其他异常了!" + e.getMessage());
}
}
}
WorkerServer:
import com.alibaba.fastjson.JSON;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNoNodeException;
/**
* 集群中workerServer服务器 相关逻辑实现
*
* 此WorkerServer实例主要做的事有:
* 1.在调用了start方法后,会在/servers节点下创建对应的临时节点
* 2.在监听到/config节点数据变化时,会做相应的处理
*
* @author JustryDeng
* @date 2018/11/23 16:19
*/
public class WorkerServer {
/** ZkClient客户端 */
private ZkClient zkClient;
/** 当前WorkerServer的数据内容信息 */
private WorkerServerParamModel currentWorkerServer;
/** servers节点路径 */
private String serversNodePath;
/** config节点路径 */
private String configNodePath;
/** config节点初始化数据 */
private ConfigPOJO configPOJOData;
/** 当前workerServer服务器对config节点的数据内容监听器 */
private IZkDataListener configNodeDataListener;
/**
* 构造器
*
* @param zkClient
* ZkClient客户端
* @param wsParamModel
* 当前WorkerServer的数据内容信息
* @param serversNodePath
* servers节点路径
* @param configNodePath
* config节点路径
* @param initConfig
* config节点初始化数据
* @date 2018/11/28 18:32
*/
public WorkerServer(ZkClient zkClient, WorkerServerParamModel wsParamModel,
String serversNodePath, String configNodePath, ConfigPOJO initConfig) {
// 初始化赋值
this.zkClient = zkClient;
this.currentWorkerServer = wsParamModel;
this.serversNodePath = serversNodePath;
this.configNodePath = configNodePath;
this.configPOJOData = initConfig;
// 实例化数据监听器
this.configNodeDataListener = new IZkDataListener() {
public void handleDataDeleted(String dataPath) {
// TODO do something
}
public void handleDataChange(String dataPath, Object data) {
// 获取到config节点的最新数据,并将其转换为json字符串
String jsonString = new String((byte[])data);
ConfigPOJO newestConfigNodeData = JSON.parseObject(jsonString, ConfigPOJO.class);
updateConfigData(newestConfigNodeData);
System.out.println(currentWorkerServer.getServerName() + "监听到config节点数据发生了变化!"
+ "此时,config节点的数据为:" + newestConfigNodeData.toString());
}
};
}
/**
* 启动WorkerServer
*
* @author JustryDeng
* @date 2018/11/28 18:46
*/
public void start() {
System.out.println(currentWorkerServer.getServerName() + "开始工作了!");
// 给当前服务器(严格的说,是给当前客户端)注册对应的临时节点到 /servers下
registCurrentWorkerServerNodeUnderServersNode();
// 订阅
zkClient.subscribeDataChanges(configNodePath, configNodeDataListener);
}
/**
* 停止WorkerServer
*
* @author JustryDeng
* @date 2018/11/28 19:14
*/
public void stop() {
System.out.println(currentWorkerServer.getServerName() + "取消了/config节点的数据订阅!");
zkClient.unsubscribeDataChanges(configNodePath, configNodeDataListener);
}
/**
* 更新此实例的configPOJOData属性,使其为最新的值
*
* @author JustryDeng
* @date 2018/11/28 18:43
*/
private void updateConfigData(ConfigPOJO newestConfigData){
this.configPOJOData = newestConfigData;
}
/**
* 在/server节点下 注册 与当前WorkerServer服务器(严格的说,是客户端)对应的 临时节点
*
* @author JustryDeng
* @date 2018/11/28 19:26
*/
private void registCurrentWorkerServerNodeUnderServersNode() {
// 节点分割符
final String NODE_SPLITTER = "/";
String currWorkerServerNodePath = serversNodePath + NODE_SPLITTER + currentWorkerServer.getServerName();
try {
// 创建临时节点,并将数据放入对应节点
zkClient.createEphemeral(currWorkerServerNodePath, JSON.toJSONString(currentWorkerServer).getBytes());
} catch (ZkNoNodeException e) {
// 如果我们要创建的临时节点的某个父节点不存在,那么创建
// 父节点是持久节点
// 第二个参数为true,表示如果当前要创建的serversNodePath节点不存在父节点的话,那么直接回顺带创建其父节点
zkClient.createPersistent(serversNodePath, true);
registCurrentWorkerServerNodeUnderServersNode();
} catch (Exception e) {
System.err.println("registCurrentWorkerServerNodeUnderServersNode发生异常了!" + e.getMessage());
}
}
}
WorkerServerParamModel:
import java.io.Serializable;
/**
* 服务器模型
* 注:此模型主要用于记录 服务器的基本信息
* 注:由于涉及到这个模型的ZkClient需要使用SerializableSerializer所以此模型需要实现Serializable接口;
* 如果不想实现Serializable,那么可以使用BytesPushThroughSerializer,这样一
* 来设置数据时就需要手动将数据转换为字节数组了
*
* @author JustryDeng
* @date 2018/11/23 15:42
*/
public class WorkerServerParamModel implements Serializable {
private static final long serialVersionUID = 8895156298220482681L;
/** 服务器id */
private int serverId;
/** 服务器名字 */
private String serverName;
public int getServerId() {
return serverId;
}
public void setServerId(int serverId) {
this.serverId = serverId;
}
public String getServerName() {
return serverName;
}
public void setServerName(String serverName) {
this.serverName = serverName;
}
@Override
public String toString() {
return "WorkerServerParamModel{serverId=" + serverId + ", serverName='" + serverName + "'}";
}
}
SubscribeReleaseTest:
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkInterruptedException;
import org.I0Itec.zkclient.serialize.BytesPushThroughSerializer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 发布与订阅 --- 测试
*
* @author JustryDeng
* @date 2018/11/27 15:30
*/
public class SubscribeReleaseTest {
private static final int SIZE = 5;
private static final String IP_PORT = "10.8.109.60:2181";
private static final int SESSION_TIMEOUT = 10000;
private static final int CONNECT_TIMEOUT = 10000;
private static final String CONFIG_NODE_PATH = "/config";
private static final String SERVERS_NODE_PATH = "/servers";
private static final String COMMAND_NODE_PATH = "/command";
/** manageServer相关ZkClient */
private static ZkClient managerServerClient;
private static ManageServer manageServer;
/** workerServer相关ZkClient */
private static List<ZkClient> zkClients = new ArrayList<>(16);
private static List<WorkerServer> workerServers = new ArrayList<>(16);
/**
* 程序入口
*/
public static void main(String[] args) throws InterruptedException {
try {
zkClients.clear();
workerServers.clear();
// 在WorkerServer开始工作前,先开启manager监听管理
managerServerClient = new ZkClient(IP_PORT, SESSION_TIMEOUT, CONNECT_TIMEOUT, new BytesPushThroughSerializer());
manageServer = new ManageServer(managerServerClient, SERVERS_NODE_PATH, COMMAND_NODE_PATH, CONFIG_NODE_PATH);
manageServer.start();
// 循环创建WorkerServer
for (int i = 0; i < SIZE; i++){
// 创建WorkerServer
WorkerServerParamModel wsParamModel = new WorkerServerParamModel();
wsParamModel.setServerId(i);
wsParamModel.setServerName("worker" + i);
// 创建初始化配置
ConfigPOJO initConfig = new ConfigPOJO();
initConfig.setAgeConfig(100 + i);
initConfig.setGenderConfig("我是性别" + i);
initConfig.setMottoConfig("我是座右铭" + i);
initConfig.setNameConfig("我是姓名" + i);
ZkClient zkClient = new ZkClient(IP_PORT, SESSION_TIMEOUT, CONNECT_TIMEOUT, new BytesPushThroughSerializer());
zkClients.add(zkClient);
WorkerServer workerServer = new WorkerServer(zkClient, wsParamModel, SERVERS_NODE_PATH, CONFIG_NODE_PATH, initConfig);
workerServers.add(workerServer);
// 启动zookeeper的某个客户端
workerServer.start();
}
// 为快速测试,直接使用zookeeper命令行客户端,来修改command节点的数据(数据已按要求提前准备好),
// 触发manageServer对command节点的监听事件
// 上述操作需要时间,为了能在控制台观察输出,主线程这里需要阻塞一段时间
System.out.println("主线程开始sleep,等待JustryDeng大帅哥使用zookeeper命令行客户端[set /command data]!");
TimeUnit.SECONDS.sleep(60);
} finally {
System.out.println("开始退出!");
try {
WorkerServer workerServer;
for (WorkerServer ws : workerServers) {
ws.stop();
}
for (ZkClient zkClient :zkClients) {
zkClient.close();
}
manageServer.stop();
managerServerClient.close();
} catch (ZkInterruptedException e) {
e.printStackTrace();
}
}
}
}
启动对应的ZooKeeper服务器,开放端口(或关闭防火墙),然后运行上述主函数(同时在主函数sleep时,及时使用zookeeper命令行客户端修改/command节点数据,触发事件),控制台输出:
所有zookeeper示例内容有(代码链接见本人末):