4
2
0
2

Zookeeper

Zookeeper

1、本地安装

1.1 安装前准备

  1. 确保Linux中安装了 JDK

  2. 拷贝 apache-zookeeper-x.x.x-x.x.x-bin.tar.gz 安装包到Linux系统下。

  3. 解压到指定的目录

    1. [root@localhost module]# tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module/
      
  4. 修改名称

    1. [root@localhost module]# mv apache-zookeeper-3.5.7-bin zookeeper-3.5.7
      

1.2 配置修改

  1. 将/opt/module/zookeeper-3.5.7/conf 这个路径下的 zoo_sample.cfg 复制并修改为 zoo.cfg;

    1. cp zoo_sample.cfg zoo.cfg
      
  2. 打开 zoo.cfg 文件,修改 dataDir 路径:

    1. vim zoo.cfg
      
    2. 修改内容

      1. dataDir=/opt/module/zookeeper-3.5.7/zkData
        
  3. 在/opt/module/zookeeper-3.5.7/这个目录上创建 zkData 文件夹

    1. mkdir zkData
      

1.3 操作 Zookeeper

  1. 启动 Zookeeper

    1. bin/zkServer.sh start
      
  2. 查看进程是否启动

    1. jps命令作用:用来查看本地运行着几个java程序,并显示他们的进程号。使用jps时,不需要传递进程号做为参数。

    2. 安装jps,参数详解

      1. yum install -y java-1.8.0-openjdk-devel.x86_64
        # 查看jps命令是否存在
        which jps
        
      2. 选项 描述
        -q 仅输出VM标识符,不包括class
        name,jar name,arguments
        in main
        method -m
        输出main method的参数
        -l 输出完全的包名,应用主类名,jar的完全路径名
        -v 输出jvm参数
        -V 输出通过flag文件传递到JVM中的参数(.hotspotrc文件或-XX:Flags=所指定的文件
    3. # 查看进程是否启动 jps [选项] [参数]
      jps
      
  3. 查看状态

    1. bin/zkServer.sh status
      
  4. 启动客户端

    1. bin/zkCli.sh
      
  5. 退出客户端:

    1. quit
      
  6. 停止 Zookeeper

    1. bin/zkServer.sh stop
      

1.4 配置参数

  • tickTime= 2000通信心跳时间,Zookeeper服务器与客户端心跳时间,单位毫秒。

    • # The number of milliseconds of each tick
      tickTime=2000
      
  • initLimit= 10Leader和Follower初识通信时限

    • # The number of ticks that the initial 
      # synchronization phase can take
      initLimit=10
      
    • Leader和Follower初始连接时能容忍的最多心跳数(tickTime的数量)

  • syncLimit = 5Leader 和 Follower同步通信时限

    • # The number of ticks that can pass between 
      # sending a request and getting an acknowledgement
      syncLimit=5
      
    • Leader 和 Follower 之间通信时间如果超过 syncLimit * tickTime , Leader认为Follower死掉,从服务器列表中删除Follower

  • dataDir保存Zookeeper中的数据

    • # the directory where the snapshot is stored.
      # do not use /tmp for storage, /tmp here is just 
      # example sakes.
      dataDir=/opt/module/zookeeper-3.5.7/zkData
      
    • 默认的 tmp目录,容易被Linux系统定期删除,所以一般不用默认的tmp目录。

  • clientPort = 2181客户端连接端口,通常不做修改

    • # the port at which the clients will connect
      clientPort=2181
      
  • 剩余的配置

    • # the maximum number of client connections.
      # increase this if you need to handle more clients
      #maxClientCnxns=60
      #
      # Be sure to read the maintenance section of the 
      # administrator guide before turning on autopurge.
      #
      # http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
      #
      # The number of snapshots to retain in dataDir
      #autopurge.snapRetainCount=3
      # Purge task interval in hours
      # Set to "0" to disable auto purge feature
      #autopurge.purgeInterval=1
      

2、集群

2.1 集群操作

1、集群规划

在linux1,Linux2,Linux3 三个节点部署Zookeeper。

2、解压安装

(1)在 hadoop102 解压 Zookeeper 安装包到/opt/module/目录下

tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module/

(2)修改 apache-zookeeper-3.5.7-bin 名称为 zookeeper-3.5.7

mv apache-zookeeper-3.5.7-bin/ zookeeper-3.5.7

3、配置服务器编号

(1)在/opt/module/zookeeper-3.5.7/这个目录下创建 zkData

 mkdir zkData

(2)在/opt/module/zookeeper-3.5.7/zkData 目录下创建一个 myid 的文件

vi myid

在文件中添加与 server 对应的编号(注意:上下不要有空行,左右不要有空格)

1

注意:添加 myid 文件,一定要在 Linux 里面创建,在 notepad++里面很可能乱码

(3)拷贝配置好的 zookeeper 到其他机器上

xsync zookeeper-3.5.7

并分别在 Linux2、Linux3 上修改 myid 文件中内容为 3、4

xsync 脚本:

#!/bin/bash
#1 获取输入参数个数,如果没有参数,直接退出
pcount=$#
if((pcount==0)); then
echo no args;
exit;
fi

#2 获取文件名称
p1=$1
fname=`basename $p1`
echo fname=$fname

#3 获取上级目录到绝对路径
pdir=`cd -P $(dirname $p1); pwd`
echo pdir=$pdir

#4 获取当前用户名称
user=`whoami`

#5 循环
for((host=2; host<4; host++)); do
        #echo $pdir/$fname $user@hadoop$host:$pdir
        echo --------------- linux$host ----------------
        rsync -rl $pdir/$fname $user@linux$host:$pdir
done

4、配置zoo.cfg

(1)复制/opt/module/zookeeper-3.5.7/conf 这个目录下的 zoo_sample.cfg 为 zoo.cfg

cp zoo_sample.cfg zoo.cfg

(2)打开 zoo.cfg 文件

vim zoo.cfg

修改数据存储路径配置

dataDir=/opt/module/zookeeper-3.5.7/zkData

增加如下配置

前提:在/etc/hosts 下编辑对应的ip地址

#######################cluster##########################
server.1=linux1:2888:3888
server.2=linux2:2888:3888
server.3=linux3:2888:3888

(3)配置参数解读

server.A=B:C:D

  • A 是一个数字,表示这个是第几号服务器;

    集群模式下配置一个文件 myid,这个文件在 dataDir 目录下,这个文件里面有一个数据

    就是 A 的值,Zookeeper 启动时读取此文件,拿到里面的数据与 zoo.cfg 里面的配置信息比

    较从而判断到底是哪个 server。

  • B 是这个服务器的地址;

  • C 是这个服务器 Follower 与集群中的 Leader 服务器交换信息的端口;

  • D 是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的

    Leader,而这个端口就是用来执行选举时服务器相互通信的端口。

(4)同步 zoo.cfg 配置文件

xsync zoo.cfg

5、集群操作

(1)三台节点分别启动 Zookeeper

bin/zkServer.sh start

(2)分别三台节点查看状态

[root@linux1 apache-zookeeper-3.5.7]#  bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/module/apache-zookeeper-3.5.7/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower
[root@linux2 apache-zookeeper-3.5.7]# bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/module/apache-zookeeper-3.5.7/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader
[root@linux3 apache-zookeeper-3.5.7]#  bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/module/apache-zookeeper-3.5.7/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower

(3)查看日志

bin/zkServer.sh start-foreground

3、选举机制

3.1 Zookeeper选举机制——第一次启动

  • SID:服务器ID。用来唯一标识一台

    ZooKeeper集群中的机器,每台机器不能重

    复,和myid一致。

  • ZXID:事务ID。ZXID是一个事务ID,用来

    标识一次服务器状态的变更。在某一时刻,

    集群中的每台机器的ZXID值不一定完全一

    致,这和ZooKeeper服务器对于客户端“更

    新请求”的处理逻辑有关。

  • Epoch:每个Leader任期的代号。没有

    Leader时同一轮投票过程中的逻辑时钟值是

    相同的。每投完一次票这个数据就会增加

(1)服务器1启 动,发起一次选举。服务器1投自己一票。此时服务器1票数一票,不够半数以上(

3票),选举无法完成,服务器1状态保持为LOOKING;

(2)服务器2启动,再发起一次选举。服务器1和2分别投自己一票并交换选票信息:此时服务器1发现服务器2的myid比自己目前投票推举的(服务器1)

大,更改选票为推举服务器2。此时服务器1票数0票,服务器2票数2票,没有半数以上结果,选举无法完成,服务器1,2状态保持LOOKING

(3)服务器3启动,发起一次选举。此时服务器1和2都会更改选票为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数,服务器3当选Leader。服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;

(4)服务器4启动,发起一次选举。此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为 1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING;

(5)服务器5启动,同4一样当小弟。

3.2 Zookeeper选举机制——非第一次启动

(1)当ZooKeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举:

  • 服务器初始化启动。

  • 服务器运行期间无法和Leader保持连接。

(2)而当一台机器进入Leader选举流程时,当前集群也可能会处于以下两种状态:

  • 集群中本来就已经存在一个Leader。

对于第一种已经存在Leader的情况,机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader机器建立连接,并进行状态同步即可。

  • 集群中确实不存在Leader

假设ZooKeeper由5台服务器组成,SID分别为1、2、3、4、5,ZXID分别为8、8、8、7、7,并且此时SID为3的服务器是Leader。某一时刻,3和5服务器出现故障,因此开始进行Leader选举。

(EPOCH,ZXID,SID ) EPOCH,ZXID,SID ) (EPOCH,ZXID,SID )

SID为1、2、4的机器投票情况:

(1,8,1) (1,8,2) (1,7,4)

选举Leader规则:①EPOCH大的直接胜出 ②EPOCH相同,事务id大的胜出 ③事务id相同,服务器id大的胜出

4、ZK 集群启动停止脚本

(1)在 hadoop102 的/usr/local/bin 目录下创建脚本

 vim zk.sh

在脚本中编写如下内容

#!/bin/bash
case $1 in
"start"){
for i in linux1 linux2 linux3
do
 echo ---------- zookeeper $i 启动 ------------
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"
done
};;
"stop"){
for i in linux1 linux2 linux3
do
 echo ---------- zookeeper $i 停止 ------------ 
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh stop"
done
};;
"status"){
for i in linux1 linux2 linux3
do
 echo ---------- zookeeper $i 状态 ------------ 
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status"
done
};;
esac

(2)增加脚本执行权限

chmod 777 zk.sh

(3)Zookeeper 集群启动脚本

zk.sh start

(4)Zookeeper 集群状态查看脚本

zk.sh status

(5)Zookeeper 集群停止脚本

 zk.sh stop

5、客户端命令行操作

5.1 命令行语法

命令基本语法 功能描述
help 显示所有操作命令
ls path 使用 ls 命令来查看当前 znode 的子节点 [可监听] -w 监听子节点变化 -s 附加次级信息
create 普通创建 -s 含有序列 -e 临时(重启或者超时消失)
get path 获得节点的值 [可监听] -w 监听节点内容变化 -s 附加次级信息
set 设置节点的具体值
stat 查看节点状态
delete 删除节点
deleteall 递归删除节点

(1)启动客户端

bin/zkCli.sh -server linux2:2181

(2)显示所有操作命令

help

5.2 znode 节点数据信息

(1)查看当前znode中所包含的内容

ls /

(2)查看当前节点详细信息

[zk: linux2:2181(CONNECTED) 4] ls -s /
[zookeeper]cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1

(1)czxid:创建节点的事务 zxid

每次修改 ZooKeeper 状态都会产生一个 ZooKeeper 事务 ID。事务 ID 是 ZooKeeper 中所

有修改总的次序。每次修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之

前发生。

(2)ctime:znode 被创建的毫秒数(从 1970 年开始)

(3)mzxid:znode 最后更新的事务 zxid

(4)mtime:znode 最后修改的毫秒数(从 1970 年开始)

(5)pZxid:znode 最后更新的子节点 zxid

(6)cversion:znode 子节点变化号,znode 子节点修改次数

(7)dataversion:znode 数据变化号

(8)aclVersion:znode 访问控制列表的变化号

(9)ephemeralOwner:如果是临时节点,这个是 znode 拥有者的 session id。如果不是

临时节点则是 0。

(10)dataLength:znode 的数据长度

(11)numChildren:znode 子节点数量

5.3 节点类型(持久/临时)

有序号

(1)分别创建2个普通节点(永久节点 + 带序号

​ (1)先创建一个普通的根节点/sanguo/weiguo

create /sanguo/weiguo "caocao"

​ (2)创建带序号的节点

[zk: linux2:2181(CONNECTED) 15] create -s /sanguo/weiguo/zhangliao "zhangliao"
Created /sanguo/weiguo/zhangliao0000000000
[zk: linux2:2181(CONNECTED) 17] create -s /sanguo/weiguo/zhangliao "zhangliao"
Created /sanguo/weiguo/zhangliao0000000001

如果原来没有序号节点,序号从 0 开始依次递增。如果原节点下已有 2 个节点,则再排

序时从 2 开始,以此类推。

(2)分别创建2个普通节点(临时节点 + 带序号
 create -e -s /sanguo/wuguo "zhouyu"
 ls /sanguo 
[wuguo, wuguo0000000001, shuguo]

无序号

(1)分别创建2个普通节点(永久节点 + 不带序号
 create /sanguo "diaochan"
 create /sanguo/shuguo "liubei"

(2)获得节点的值

[zk: linux2:2181(CONNECTED) 7] get -s /sanguo
diaochan
cZxid = 0x300000002
ctime = Mon Sep 27 13:04:24 CST 2021
mZxid = 0x300000002
mtime = Mon Sep 27 13:04:24 CST 2021
pZxid = 0x30000000c
cversion = 6
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 2

[zk: linux2:2181(CONNECTED) 12] get -s /sanguo/weiguo
simayi
cZxid = 0x300000004
ctime = Mon Sep 27 13:07:24 CST 2021
mZxid = 0x30000000e
mtime = Mon Sep 27 13:13:33 CST 2021
pZxid = 0x300000006
cversion = 2
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 6
numChildren = 2
(2)分别创建2个普通节点(临时节点 + 不带序号
create -e /sanguo/wuguo "zhouyu"

再次查看根目录下短暂节点已经删除

5.4 修改节点数据值

set /sanguo/weiguo "simayi"

5.5 节点删除与查看

(1)删除节点

 delete /sanguo/jin

(2)递归删除节点

 deleteall /sanguo/shuguo

(3)查看节点状态

[zk: localhost:2181(CONNECTED) 13] stat /zookeeper
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x0
cversion = -2
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 2

6、监听器原理

客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听了该节点的应用程序。

6.1 监听器原理

1)首先要有一个main()线程

2)在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。

3)通过connect线程将注册的监听事件发送给Zookeeper。

4)在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。

5)Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。

6)listener线程内部调用了process()方法。

6.2 常见的监听

1)监听节点数据的变化

get path [watch]

2)监听子节点增减的变化

ls path [watch]

6.3 节点的值变化监听

(1)在 linux2主机上注册监听/sanguo 节点数据变化

 get -w /sanguo

(2)在 linux3主机上修改/sanguo 节点的数据

set /sanguo "xisi"

(3)观察 linux2主机收到数据变化的监听

WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/sanguo

注意:在linux3再多次修改/sanguo的值,linux2上不会再收到监听。因为注册一次,只能监听一次。想再次监听,需要再次注册。

6.4 节点的子节点变化监听(路径变化)

(1)在 linux2主机上注册监听/sanguo 节点的子节点变化

 ls -w /sanguo

(2)在 linux3主机/sanguo 节点上创建子节点

 create /sanguo/jin "simayi"

(3)观察 linux2主机收到子节点变化的监听

WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/sanguo

注意:节点的路径变化,也是注册一次,生效一次。想多次生效,就需要多次注册。

7、客户端 API 操作

7.1 创建IDEA 的Maven工程

  1. 导入pom依赖

    1. <dependencies>
          <dependency>
              <groupId>junit</groupId>
              <artifactId>junit</artifactId>
              <version>RELEASE</version>
          </dependency>
          <dependency>
              <groupId>org.apache.logging.log4j</groupId>
              <artifactId>log4j-core</artifactId>
              <version>2.8.2</version>
          </dependency>
          <dependency>
              <groupId>org.apache.zookeeper</groupId>
              <artifactId>zookeeper</artifactId>
              <version>3.5.7</version>
          </dependency>
      </dependencies>
      
  2. 在src/main/resources 目录下,新建 log4j.properties 文件,填入

    1. log4j.rootLogger=INFO, stdout
      log4j.appender.stdout=org.apache.log4j.ConsoleAppender
      log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
      log4j.appender.stdout.layout.ConversionPattern=%d %p [%c]- %m%n
      log4j.appender.logfile=org.apache.log4j.FileAppender
      log4j.appender.logfile.File=target/spring.log
      log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
      log4j.appender.logfile.layout.ConversionPattern=%d %p [%c]- %m%n
      

7.2 创建类 ZkClient客户端

  1. package com.codertl.zk;
    
    import org.apache.zookeeper.*;
    import org.junit.Before;
    import org.junit.Test;
    
    import java.io.IOException;
    
    /**
     * @Description zookeeper客户端
     * @Author Admin
     * @Date 2021/9/27 13:48
     * @Version 1.0
     */
    public class ZkClient {
    
        private static String connectString = "192.168.181.101:2181,192.168.181.102:2181,192.168.181.103:2181";
        private int sessionTimeout = 200000;
        private ZooKeeper zkClient = null;
    
        // 连接 zookeeper 服务器
        @Before
        public void init() throws IOException {
    
            zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
    
                }
            });
        }
    
        // 创建节点
        @Test
        public void create() throws InterruptedException, KeeperException {
            // 参数 1:要创建的节点的路径; 参数 2:节点数据 ; 参数 3:节点权限 ;参数 4:节点的类型
            String nodeCreated = zkClient.create("/codertl", "kk".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }
    

7.3 获取子节点并监听节点变化

@Before
public void init() throws Exception {
    zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
        @Override
        public void process(WatchedEvent watchedEvent) {
            System.out.println("==========================================");
            List<String> children = null;
            try {
                children = zkClient.getChildren("/", true);
                for (String child : children) {
                    System.out.println(child);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

@Test
public void getChildren() throws InterruptedException, KeeperException {
    List<String> children = zkClient.getChildren("/", true);

    for (String child : children) {
        System.out.println(child);
    }

    // 延时
    Thread.sleep(Long.MAX_VALUE);
}

7.4 判断节点是否存在

@Test
public void exist() throws InterruptedException, KeeperException {
    Stat stat = zkClient.exists("/codertl", false);

    System.out.println(stat == null ? "not exist" : "exist");
}

8、服务器动态上下线监听案例

8.1 需求

​ 某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知到主节点服务器的上下线。

8.2 实现

服务端向 Zookeeper 注册

package com.codertl.zk.case1;

import org.apache.zookeeper.*;

import java.io.IOException;

/**
 * @Description
 * @Author Admin
 * @Date 2021/9/27 14:43
 * @Version 1.0
 */
public class DistributeServer {
    private static String connectString = "192.168.181.101:2181,192.168.181.102:2181,192.168.181.103:2181";
    private int sessionTimeout = 200000;
    private ZooKeeper zkClient = null;

    public static void main(String[] args) throws Exception {

        DistributeServer server = new DistributeServer();

        // 1. 连接zookeeper 集群
        server.getConnect();

        // 2. 注册服务器到 zookeeper 集群
        server.register(args[0]);

        // 3. 启动业务逻辑
        server.business();
    }

    // 业务逻辑
    private void business() throws InterruptedException {
        Thread.sleep(Long.MAX_VALUE);
    }
	  // 注册服务器到 zookeeper 集群
    private void register(String hostName) throws InterruptedException, KeeperException {
        String created = zkClient.create("/servers/" + hostName, hostName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

        System.out.println(hostName + " is online");
    }
	 //  连接zookeeper 集群
    private void getConnect() throws IOException {
        zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {

            }
        });
    }
}

客户端获取

package com.codertl.zk.case1;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * @Description 客户端
 * @Author Admin
 * @Date 2021/9/27 14:56
 * @Version 1.0
 */
public class DistributeClient {
    private static String connectString = "192.168.181.101:2181,192.168.181.102:2181,192.168.181.103:2181";
    private int sessionTimeout = 200000;
    private ZooKeeper zk = null;

    public static void main(String[] args) throws Exception {

        DistributeClient client = new DistributeClient();

        // 1. 获取zookeeper 连接
        client.getConnect();

        // 2. 监听服务器 /servers 下面的子节点,增加和删除
        client.getServerList();

        // 3. 业务逻辑
        client.business();

    }
    // 业务功能
    private void business() throws InterruptedException {
        Thread.sleep(Long.MAX_VALUE);
    }
    // 获取服务器列表信息
    private void getServerList() throws InterruptedException, KeeperException {
        // 1 获取服务器子节点信息,并且对父节点进行监听
        List<String> children = zk.getChildren("/servers", true);

        // 2 存储服务器信息列表
        ArrayList<String> servers = new ArrayList<>();
        // 3 遍历所有节点,获取节点中的主机名称信息
        for (String child : children) {
            byte[] data = zk.getData("/servers/" + child, false, null);
            servers.add(new String(data));
        }

        // 4 打印服务器列表信息
        System.out.println(servers);
    }
    // 创建到 zk 的客户端连接
    private void getConnect() throws IOException {
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 再次启动监听
                try {
                    getServerList();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

9、Zookeeper 分布式锁

什么叫做分布式锁呢?

​ 比如说"进程 1"在使用该资源的时候,会先去获得锁,"进程 1"获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,"进程 1"用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁

9.1 原生API 实现

9.1.1 分布式锁实现

package com.codertl.zk.case2;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * @Description 分布式锁
 * @Author Admin
 * @Date 2021/9/27 15:29
 * @Version 1.0
 */
public class DistributedLock {
    private static String connectString = "192.168.181.101:2181,192.168.181.102:2181,192.168.181.103:2181";
    private int sessionTimeout = 200000;
    private ZooKeeper zkClient = null;

    private CountDownLatch connectLatch = new CountDownLatch(1);
    private CountDownLatch waitLatch = new CountDownLatch(1);

    private String waitPath;
    private String currentMode;


    public DistributedLock() throws Exception {

        // 1. 获取连接
        zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // connectLatch 如果连接上 zk 可以释放
                if (event.getState() == Event.KeeperState.SyncConnected) {
                    connectLatch.countDown();
                }
                // waitLatch 需要释放
                if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
                    waitLatch.countDown();
                }
            }
        });

        // 等待 zk 正常连接后,往下走
        connectLatch.await();

        // 2. 判断根节点 /locks 是否存在
        Stat stat = zkClient.exists("/locks", false);

        if (stat == null) {
            // 创建根节点
            zkClient.create("/locks", "locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }


    }

    // 对 zookeeper 加锁
    public void zkClock(){

        // 创建对应的 临时带序号节点
        try {
            currentMode = zkClient.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

            // 判断 创建的节点是否是最小序号节点,如果是:获取锁; 如果不是:监听当前序号前一个节点
            List<String> children = zkClient.getChildren("/locks", false);

            // 如果 children 只有一个值,那就直接获取锁; 如果有多个节点,需要进行判断
            if (children.size() == 1) {
                return;
            } else {
                Collections.sort(children);

                // 获取节点名称 seq-0000000x
                String thisNode = currentMode.substring("/locks/".length());

                // 通过节点名称 获取该节点在 children 集合的位置
                int index = children.indexOf(thisNode);

                // 判断
                if (index == -1) {
                    System.out.println("数据异常");
                } else if (index == 0){
                    // 就一个节点
                    return;
                } else {
                    // 需要监听前一个节点的变化
                    waitPath = "/locks/" + children.get(index - 1);
                    zkClient.getData(waitPath, true, null);

                    // 等待监听
                    waitLatch.await();

                    return;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 对 zookeeper 解锁
    public void zkUnlock() {

        // 删除节点
        try {
            zkClient.delete(currentMode, -1);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

9.1.2 分布式锁测试

package com.codertl.zk.case2;

/**
 * @Description 分布式锁的测试
 * @Author Admin
 * @Date 2021/9/27 16:00
 * @Version 1.0
 */
public class DistributedLockTest {

    public static void main(String[] args) throws Exception {

        final DistributedLock lock1 = new DistributedLock();
        final DistributedLock lock2 = new DistributedLock();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    lock1.zkClock();
                    System.out.println("线程1 启动,获取到锁");
                    Thread.sleep(5 * 1000);

                    lock1.zkUnlock();
                    System.out.println("线程1 释放锁");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    lock2.zkClock();
                    System.out.println("线程2 启动,获取到锁");
                    Thread.sleep(5 * 1000);

                    lock2.zkUnlock();
                    System.out.println("线程2 释放锁");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

9.1.3 控制台输出

线程1 启动,获取到锁
线程1 释放锁
线程2 启动,获取到锁
线程2 释放锁

9.2 Curator框架实现分布式锁案例

9.2.1 原生 Java API 开发存在的问题

(1)会话连接是异步的,需要自己去处理。比如使用 CountDownLatch

(2)Watch 需要重复注册,不然就不能生效

(3)开发的复杂性还是比较高的

(4)不支持多节点删除和创建。需要自己去递归

9.2.2 Curator框架

​ 1. Curator是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题。

​ 2. 详情请查看官方文档:https://curator.apache.org/index.html

9.2.3 Curator 框架案例

1、添加依赖
<dependency>
     <groupId>org.apache.curator</groupId>
     <artifactId>curator-framework</artifactId>
     <version>4.3.0</version>
</dependency>
<dependency>
     <groupId>org.apache.curator</groupId>
     <artifactId>curator-recipes</artifactId>
     <version>4.3.0</version>
</dependency>
<dependency>
     <groupId>org.apache.curator</groupId>
     <artifactId>curator-client</artifactId>
     <version>4.3.0</version>
</dependency>
2、代码实现
package com.codertl.zk.case3;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

/**
 * @Description Curator实现分布式锁
 * @Author Admin
 * @Date 2021/9/27 16:15
 * @Version 1.0
 */
public class CuratorLockTest {

    public static void main(String[] args) {

        // 创建分布式锁1
        InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");
        // 创建分布式锁2
        InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock1.acquire();
                    System.out.println("线程1 获取到锁");

                    lock1.acquire();
                    System.out.println("线程1 再次获取到锁");

                    Thread.sleep(5 * 1000);

                    lock1.release();
                    System.out.println("线程1   释放锁");

                    lock1.release();
                    System.out.println("线程1  再次释放锁");

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock2.acquire();
                    System.out.println("线程2 获取到锁");

                    lock2.acquire();
                    System.out.println("线程2 再次获取到锁");

                    Thread.sleep(5 * 1000);

                    lock2.release();
                    System.out.println("线程2   释放锁");

                    lock2.release();
                    System.out.println("线程2  再次释放锁");

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }

    private static CuratorFramework getCuratorFramework() {
        ExponentialBackoffRetry policy = new ExponentialBackoffRetry(300000, 3);

        CuratorFramework client = CuratorFrameworkFactory.builder().connectString("192.168.181.101:2181,192.168.181.102:2181,192.168.181.103:2181")
            .connectionTimeoutMs(200000)
            .sessionTimeoutMs(200000)
            .retryPolicy(policy).build();
        // 启动 客户端
        client.start();

        System.out.println("zookeeper 启动成功!");
        return client;

    }
}
3、 控制台打印
线程1 获取到锁
线程1 再次获取到锁
线程1   释放锁
线程1  再次释放锁
线程2 获取到锁
线程2 再次获取到锁
线程2   释放锁
线程2  再次释放锁

10、面试题

10.1 选举机制

半数机制,超过半数的投票通过,即通过。

(1)第一次启动选举规则:

投票过半数时,服务器 id 大的胜出

(2)第二次启动选举规则:

①EPOCH(leader任期id) 大的直接胜出

②EPOCH 相同,事务 id 大的胜出

③事务 id 相同,服务器 id 大的胜出

10.2 生产集群安装多少 zookeeper 合适

安装奇数

推荐:

  • 10台服务器: 3台 Zookeeper
  • 20台服务器:5台 Zookeeper
  • 100台服务器:11台 Zookeeper
  • 200台服务器:11台 Zookeeper

服务器台数多:好处,提高可靠性;坏处,提高通信延时

10.3 常用命令

  • ls:查看
  • get:获取
  • create:创建
  • delete:删除
posted @   CoderTL  阅读(39)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示