一、Zookeeper 简介

1、Zookeeper 概念

Zookeeper 是一个分布式协调服务的开源框架。 主要用来解决分布式集群中应用系统的一致性问题,例如怎样避免同时操作同一数据造成脏读的问题。分布式系统中数据存在一致性的问题!!!

  • ZooKeeper 本质上是一个分布式的****文件存储系统。 提供基于类似于文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。
  • ZooKeeper 提供给客户端监控存储在zk内部数据的功能,从而可以达到基于数据的集群管理。 诸如: 统一命名服务(dubbo)、分布式配置管理(solr的配置集中管理)、分布式消息队列(sub/pub)、分布式锁、分布式协调等功能。

    2、zookeeper 架构组成

    image.png

  • Leader

    • Zookeeper 集群工作的核心角色
    • 集群内部各个服务器的调度者。
    • 事务请求(写操作)的**唯一**调度和处理者,保证集群事务处理的顺序性;对于 create,update, delete 等有写操作的请求,则需要统一转发给leader 处理, leader 需要决定编号、执行操作,这个过程称为一个事务。
  • Follower
    • 处理客户端非事务(读操作) 请求,
    • 转发事务请求给 Leader;
    • 参与集群 Leader 选举投票 2n-1台可以做集群投票(后面介绍)

此外,针对访问量比较大的 zookeeper 集群, 还可新增观察者角色 Observer

  • Observer

    • 观察者角色,观察 Zookeeper 集群的最新状态变化并将这些状态同步过来,其对于非事务请求可以进行独立处理,对于事务请求,则会转发给 Leader服务器进行处理。
    • 不会参与任何形式的投票,只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。增加了集群增加并发的读请求
  • 注:ZK也是Master/slave架构,但是与之前不同的是:zk集群中的Leader不是指定而来,而是通过选举产生

    3、Zookeeper 特点

  1. Zookeeper:一个leader,多个 follower 组成的集群
  2. Leader 负责进行投票的发起和决议,更新系统状态(内部原理后面介绍)
  3. Follower 用于接收客户请求并向客户端返回结果,在选举Leader过程中参与投票
  4. 集群中只要有**半数**以上节点存活,Zookeeper集群就能正常服务
  5. 全局数据一致:每个server保存一份相同的数据副本,Client无论连接到哪个server,数据都是一致的
  6. 更新请求顺序进行(内部原理后面介绍)
  7. 数据更新原子性,一次数据更新要么成功,要么失败。

二、环境搭建

1、Zookeeper 搭建方式介绍

Zookeeper安装方式有三种,单机模式和集群模式以及伪集群模式。
■ 单机模式:Zookeeper只运行在一台服务器上,适合测试环境
■ 伪集群模式:就是在一台服务器上运行多个Zookeeper 实例
集群模式:Zookeeper运行于一个集群上,适合生产环境,这个计算机集群被称为一个“集合体”

2、Zookeeper 集群搭建

上传、解压:

  • 任意选择一个节点先进行安装,如 linux121
  • 下载完成后,将 zookeeper压缩包 zookeeper-3.4.14.tar.gz 上传到linux系统/opt/lagou/software
  • tar -zxvf zookeeper-3.4.14.tar.gz -C /opt/lagou/servers/

修改配置文件创建data和logs目录【重要!】

  • data目录:存储数据
  • log目录:存储日志信息
  • 注:以下的配置对所有节点来说都是相同的 ```shell

    创建zk存储数据目录

    mkdir -p /opt/lagou/servers/zookeeper-3.4.14/data

创建zk日志文件目录

mkdir -p /opt/lagou/servers/zookeeper-3.4.14/data/logs

修改zk配置文件

cd /opt/lagou/servers/zookeeper-3.4.14/conf

文件改名

mv zoo_sample.cfg zoo.cfg vim zoo.cfg

更新datadir

dataDir=/opt/lagou/servers/zookeeper-3.4.14/data

增加logdir

dataLogDir=/opt/lagou/servers/zookeeper-3.4.14/data/logs

增加集群配置

server.服务器ID=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口

server.1=linux121:2888:3888 server.2=linux122:2888:3888 server.3=linux123:2888:3888

打开以下的注释

ZK提供了自动清理事务日志和快照文件的功能,这个参数指定了清理频率,单位是小时

autopurge.purgeInterval=1

  1. - 之后,将包含配置文件的ZooKeeper安装包**分发**给其他节点
  2. - `rsync-script /opt/lagou/servers/zookeeper-3.4.14`
  3. **添加 myid 配置**
  4. - **注:每个节点的myid配置是唯一的**
  5. - zookeepe r data 目录下创建一个 `myid` 文件,这个文件就是记录每个服务器的ID
  6. - linux121linux122linux123 myid 文件中记录的值可分别设置为 123
  7. - linux121`echo 1 > /opt/lagou/servers/zookeeper-3.4.14/data/myid`
  8. - linux121`echo 2 > /opt/lagou/servers/zookeeper-3.4.14/data/myid`
  9. - linux121`echo 3 > /opt/lagou/servers/zookeeper-3.4.14/data/myid`
  10. **依次****启动这三个节点**
  11. - 启动命令【所有节点都要执行此命令】
  12. - `/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh start`
  13. - 查看启动情况
  14. - `/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh status`
  15. - 停止命令【所有节点都要执行此命令】
  16. - `/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh stop`
  17. - 为了方便,创建集群启动和停止的**脚本**
  18. - 可将脚本路径设置在目录:`/opt/lagou/servers/zookeeper-3.4.14/bin/`
  19. - `vim zk.sh`
  20. - 启动集群:`sh zk.sh start`
  21. - 关闭集群:`sh zk.sh stop`
  22. - 查看集群情况:`sh zk.sh status`
  23. - 通过命令 jps 查看可知 zk 的进程名是:**QuorumPeerMain**
  24. ```shell
  25. #!/bin/sh
  26. echo "start zookeeper server..."
  27. if(($#==0));then
  28. echo "no params";
  29. exit;
  30. fi
  31. hosts="linux121 linux122 linux123"
  32. for host in $hosts
  33. do
  34. ssh $host "source /etc/profile; /opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh $1"
  35. done

四、ZooKeeper 基本使用

1、ZK 命令行操作

  • 首先,进入到zookeeper的bin目录之后,通过zkClient进入zookeeper客户端命令行
    • ./zkCli.sh 连接本地的zookeeper服务器
    • ./zkCli.sh -server ip:port(2181) 连接指定的服务器
      • 如:./zkCli.sh -server linux122:2181
  • 连接成功之后,系统会输出Zookeeper的相关环境及配置信息等信息。
  • 输入help之后,屏幕会输出可用的Zookeeper命令,如下所示:
    [zk: linux122:2181(CONNECTED) 0] help
    ZooKeeper -server host:port cmd args
      stat path [watch]
      set path data [version]
      ls path [watch]
      delquota [-n|-b] path
      ls2 path [watch]
      setAcl path acl
      setquota -n|-b val path
      history 
      redo cmdno
      printwatches on|off
      delete path [version]
      sync path
      listquota path
      rmr path
      get path [watch]
      create [-s] [-e] path data acl
      addauth scheme auth
      quit 
      getAcl path
      close 
      connect host:port
    

创建节点

  • create [-s][-e] path data

    • 其中,-s-e 分别指定节点特性,顺序或临时节点,若不指定,则创建持久节点
    • 注:用命令行创建节点时,一定要有data,否则创建不成功

      ① 创建顺序节点

  • 使用 create -s /zk-test 123 命令创建zk-test顺序节点,其中内容为 123

    • [zk: localhost:2181(CONNECTED) 4] create -s /zk-test 123
    • 执行完后,就在根节点下创建了一个叫做 /zk-test 的节点,该节点内容就是 123,同时可以看到创建的zk-test节点后面添加了一串数字以示区别
      • Created /zk-test0000000000
    • 此时从根目录查看节点 ls / ,有以下内容
      • [k-test0000000000, zookeeper]

② 创建临时节点

  • 使用 create -e /zk-temp 123 命令创建zk-temp临时节点
  • 临时节点在客户端会话结束后,就会自动删除,使用 quit 命令退出客户端后,通过 ls / 查看根目录下的节点,发现临时节点不存在(已被删除)

③ 创建永久节点

  • 使用 create /zk-permanent 123 命令创建zk-permanent永久节点 ,可以看到永久节点不同于顺序节点,不会自动在后面添加一串数字

读取节点

  • 与读取相关的命令有 ls 命令和 get 命令
    • ls 命令可以列出Zookeeper指定节点下的所有子节点,但只能查看指定节点下的第一级的所有子节点
      • ls path 其中,path表示的是指定数据节点的节点路径
    • get 命令可以获取Zookeeper指定节点的数据内容和属性信息
      • get path
  • 若获取根节点下面的所有子节点,使用 ls / 命令即可
  • 若想获取某节点如 /zk-permanent 的数据内容和属性,可使用如下命令:get /zk-permanent
    • 从输出信息中可以看到,第一行是节点 /zk-permanent 的数据内容,其他几行则是创建该节点的事务ID(cZxid)、最后一次更新该节点的事务ID(mZxid)和最后一次更新该节点的时间(mtime)等属性信息

更新节点

  • 使用 set 命令,可以更新指定节点的数据内容,用法如下
    • set path data
    • 其中,data就是要更新的新内容,version表示数据版本,在zookeeper中,节点的数据是有版本概念的,这个参数用于指定本次更新操作是基于Znode的哪一个数据版本进行的,如将/zk-permanent节点的数据更新为456,可以使用如下命令:set /zk-permanent 456
      • 如下,现在 dataVersion已经变为1了,表示进行了更新
        [zk: localhost:2181(CONNECTED) 4] set /zk-permanent 456
        cZxid = 0x300000008
        ctime = Thu Jul 16 04:33:41 EDT 2020
        mZxid = 0x300000009
        mtime = Thu Jul 16 05:07:00 EDT 2020
        pZxid = 0x300000008
        cversion = 0
        dataVersion = 1
        aclVersion = 0
        ephemeralOwner = 0x0
        dataLength = 3
        numChildren = 0
        

删除节点

  • 使用 delete 命令可以删除Zookeeper上的指定节点,用法如下
    • delete path
    • 其中 version 也是表示数据版本,使用 delete /zk-permanent 命令即可删除/zk-permanent节点
  • 注:若删除节点存在子节点,那么无法删除该节点,必须先删除子节点,再删除父节点

    2、ZK-开源客户端

    ZkClient

  • ZkClient是Github上一个开源的zookeeper客户端,在Zookeeper原生API接口之上进行了包装,是一个更易用的Zookeeper客户端,同时,zkClient在内部还实现了诸如Session超时重连、Watcher反复注册等功能

  • 接下来,还是从创建会话、创建节点、读取数据、更新数据、删除节点等方面来介绍如何使用zkClient这个zookeeper客户端

添加依赖

  • 注:Maven中导入依赖后,如果还是找不到该类,需要进行刷新操作
    • image.png
      <dependency>
         <groupId>org.apache.zookeeper</groupId>
         <artifactId>zookeeper</artifactId>
         <version>3.4.14</version>
      </dependency>
      <dependency>
         <groupId>com.101tec</groupId>
         <artifactId>zkclient</artifactId>
         <version>0.2</version>
      </dependency>
      

① 创建会话

  • 使用 ZkClient 可以轻松的创建会话,连接到服务端
    • 运行结果:zkClient is ready
    • 结果表明已经成功创建会话。
      public class ZkDemo {
      public static void main(String[] args) {
         // 先获取zkClient对象,client与zk集群通信端口为2181
         ZkClient zkClient = new ZkClient("linux121:2181");
         System.out.println("zkClient is ready");
      }
      }
      

② 创建节点

  • ZkClient 提供了递归创建节点的接口,即其帮助开发者先完成父节点的创建,再创建子节点
  • 运行结果:success create znode

    • 结果表明已经成功创建了节点,值得注意的是,ZkClient通过设置createParents参数为true可以递归的先创建父节点,再创建子节点

      public class ZkDemo {
      public static void main(String[] args) {
         // 先获取zkClient对象,client与zk集群通信端口为2181
         ZkClient zkClient = new ZkClient("linux121:2181");
         System.out.println("zkClient is ready");
      
         //1、创建节点
         // 第二个参数设置为true,可以递归创建节点
         zkClient.createPersistent("/lg-zkClient/lg-c1", true);
         System.out.println("success create ZNode");
      }
      }
      

      ③ 删除节点

  • ZkClient提供了 递归删除 节点的接口,即其帮助开发者先删除所有子节点(存在),再删除父节点

    public class ZkDemo {
      public static void main(String[] args) {
          // 先获取zkClient对象,client与zk集群通信端口为2181
          ZkClient zkClient = new ZkClient("linux121:2181");
          System.out.println("zkClient is ready");
    
          //1、创建节点
          // createParents的值设置为true,可以递归创建节点
          zkClient.createPersistent("/lg-zkClient/lg-c1", true);
          System.out.println("success create ZNode.");
    
          //2、删除节点
          // 递归删除:可以删除非空节点,先删除子节点再删除父节点
          zkClient.deleteRecursive("/lg-zkClient");
          System.out.println("delete path successfully.");
    
      }
    }
    

    ④ 监听节点变化

  • 监听器可以对不存在的目录进行监听

  • 监听目录下子节点发生改变,可以接收到通知,携带数据有子节点列表
  • 监听目录创建和删除本身也会被监听到

    public class GetChildChange {
      public static void main(String[] args) throws InterruptedException {
    
          //获取到zkClient
          ZkClient zkClient = new ZkClient("linux121:2181");
    
          //zkClient对指定目录进行监听(不存在的目录:/lg-client),指定收到通知后的逻辑
          zkClient.subscribeChildChanges("/lg-client", new IZkChildListener() {
              //以下方法是接收到通知后的执行逻辑定义
              public void handleChildChange(String path, List<String> childs) throws Exception {
                  //打印节点信息
                  System.out.println(path + " childs changes, current childs " + childs);
              }
          });
    
          //使用zkClient创建节点后,删除节点,验证监听器是否运行
          zkClient.createPersistent("/lg-client");
          Thread.sleep(1000);//此处只是为了方便观察结果数据
          zkClient.createPersistent("/lg-client/c1");
          Thread.sleep(1000);
          zkClient.delete("/lg-client/c1");
          Thread.sleep(1000);
    
          zkClient.delete("/lg-client");
          Thread.sleep(Integer.MAX_VALUE);
      }
    }
    

    ⑤ 获取数据

  • 监听节点是否存在、更新、删除

  • 注:需要设置自定义的序列化类型,否则会报错!!

    public class GetDataChange {
      public static void main(String[] args) throws InterruptedException {
    
          //获取到zkClient
          ZkClient zkClient = new ZkClient("linux121:2181");
    
          //设置自定义的序列化类型,否则会报错!!
          zkClient.setZkSerializer(new ZkStrSerializer());
    
          //判断节点是否存在,不存在则创建节点并赋值
          boolean exists = zkClient.exists("/lg-client");
          if (!exists) {
              zkClient.createEphemeral("/lg-client", "2020");
          }
    
          //注册监听器,节点数据改变的类型,接收通知后的处理逻辑定义
          zkClient.subscribeDataChanges("/lg-client", new IZkDataListener() {
              @Override
              public void handleDataChange(String path, Object data) throws Exception {
                  // 定义接收通知后的处理逻辑
                  System.out.println(path + " data is changed ,new data " + data);
              }
    
              @Override
              public void handleDataDeleted(String path) throws Exception {
                  //数据删除 --> 节点删除
                  System.out.println(path + " is deleted!!");
              }
          });
    
          //更新节点的数据,删除节点,验证监听器是否正常运行
          Object o = zkClient.readData("/lg-client");
          System.out.println(o);
    
          zkClient.writeData("/lg-client", "I am new data");
          Thread.sleep(1000);
    
          //删除节点
          zkClient.delete("/lg-client");
          Thread.sleep(Integer.MAX_VALUE);
      }
    }
    
  • 自定义的序列化方法

    public class ZkStrSerializer implements ZkSerializer {
    
      // 序列化方法 string --> byte
      @Override
      public byte[] serialize(Object o) throws ZkMarshallingError {
          return String.valueOf(o).getBytes();
      }
    
      //反序列化方法 byte --> string
      @Override
      public Object deserialize(byte[] bytes) throws ZkMarshallingError {
          return new String(bytes);
      }
    }