ZooKeeper实战之ZkClient客户端实现数据的发布订阅

声明:此博客为学习笔记,学习自极客学院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示例内容有(代码链接见本人末)

微笑如有不当之处,欢迎指正

微笑zookeeper示例代码托管链接
             
 https://github.com/JustryDeng/CommonRepository

微笑参考书籍
             《ZooKeeper-分布式过程协同技术详解》
                        Flavio Junqueira Benjamin Reed 著, 谢超 周贵卿 译

微笑学习视频(推荐观看学习)
             极客学院ZooKeeper相关视频

微笑本文已经被收录进《程序员成长笔记(三)》,笔者JustryDeng

猜你喜欢

转载自blog.csdn.net/justry_deng/article/details/84626201
今日推荐