Zookeeper 入门

1 Zookeeper 入门

1.1 概述

Zookeeper 是一个开源的分布式的,为分布式框架提供协调服务的 Apache 项目。

工作机制: Zookeeper从设计模式角度来理解,是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理数据接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper 就将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应。
在这里插入图片描述

1.2 特点

  1. Zookeeper:一个领导者(Leader),多个跟随者(Follower)组成的集群。
  2. 集群中只要有半数以上节点存活,Zookeeper 集群就能正常服务。所以 Zookeeper 适合安装奇数台服务器。
  3. 全局数据一致:每个 Server 保存一份相同的数据副本,Client 无论连接到哪个 Server,数据都是一致的。
  4. 更新请求顺序执行,来自同一个 Client 的更新请求按其发送顺序依次执行。
  5. 数据更新原子性,一次数据更新要么成功,要么失败。
  6. 实时性,在一定时间范围内,Client 能读到最新数据。
  7. 符合 CAP 定理中的 CP。C 即一致性(Consistency),A 即有效性(Availability),P即分区容错性(Partition tolerance) 。

1.3 数据结构

ZooKeeper 数据模型的结构与 Unix 文件系统很类似,整体上可以看作是一棵树,每个节点称做一个 ZNode。每一个 ZNode 默认能够存储 1MB 的数据,每个 ZNode 都可以通过其路径唯一标识
在这里插入图片描述

1.4 应用场景

统一命名服务
在分布式环境下,经常需要对应用/服务进行统一命名,便于识别。
在这里插入图片描述
统一配置管理
分布式环境下,配置文件同步非常常见。配置管理可交由ZooKeeper实现。

  1. 可将配置信息写入ZooKeeper上的一个Znode。
  2. 各个客户端服务器监听这个Znode。
  3. 一旦Znode中的数据被修改,ZooKeeper将通知各个客户端服务器。

统一集群管理
分布式环境中,实时掌握每个节点的状态是必要的。ZooKeeper可以实现实时监控节点状态变化。

  1. 可将节点信息写入ZooKeeper上的一个ZNode。
  2. 监听这个ZNode可获取它的实时状态变化。

服务器动态上下线
客户端能实时洞察到服务器上下线的变化。

软负载均衡
在 Zookeeper 中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求。

1.5 下载地址

  1. 官网:https://zookeeper.apache.org
  2. 归档地址:http://archive.apache.org/dist/zookeeper/

2 Zookeeper 集群操作

2.1 集群操作

2.1.1 集群安装

1)集群规划
在 hadoop102、hadoop103 和 hadoop104 三个节点上都部署 Zookeeper。

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

[liyibin@hadoop102 software]$ tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module/

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

[liyibin@hadoop102 module]$ mv apache-zookeeper-3.5.7-bin/ zookeeper-3.5.7

3)配置服务器编号
(1)在/opt/module/zookeeper-3.5.7/这个目录下创建 zkData

[liyibin@hadoop102 zookeeper-3.5.7]$ mkdir zkData/

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

[liyibin@hadoop102 zkData]$ vim myid 
2

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

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

[liyibin@hadoop102 module]$ xsync zookeeper-3.5.7/

分别在 hadoop103、hadoop104 上修改 myid 文件中内容为 3、4

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

[liyibin@hadoop102 conf]$ cp zoo_sample.cfg  zoo.cfg

(2)修改配置文件

[liyibin@hadoop102 conf]$ vim zoo.cfg

# 修改数据存储路径
[liyibin@hadoop102 conf]$ vim zoo.cfg

# 增加集群配置消息
#######################cluster##########################
server.2=hadoop102:2888:3888
server.3=hadoop103:2888:3888
server.4=hadoop104: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 配置文件

[liyibin@hadoop102 conf]$ xsync zoo.cfg

5)集群操作
(1)分别启动 zookeeper

[liyibin@hadoop102 zookeeper-3.5.7]$ bin/zkServer.sh start
[liyibin@hadoop103 zookeeper-3.5.7]$ bin/zkServer.sh start
[liyibin@hadoop104 zookeeper-3.5.7]$ bin/zkServer.sh start

(2)查看状态

[liyibin@hadoop102 zookeeper-3.5.7]$ bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/module/zookeeper-3.5.7/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower

[liyibin@hadoop103 zookeeper-3.5.7]$ bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/module/zookeeper-3.5.7/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader

[liyibin@hadoop104 zookeeper-3.5.7]$ bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/module/zookeeper-3.5.7/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower

2.1.2 Zookeeper 集群启停脚本

1)在 hadoop102 的 /home/atguigu/bin 目录下创建脚本

[liyibin@hadoop102 bin]$ vim zk.sh

编辑如下内容

#!/bin/bash

case $1 in
"start"){
        for i in hadoop102 hadoop103 hadoop104
        do
                echo ---------- zookeeper $i start ----------
                ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"
        done
}
;;
"stop"){
        for i in hadoop102 hadoop103 hadoop104
        do
                echo ---------- zookeeper $i stop ----------
                ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh stop"
        done
}
;;
"status"){
        for i in hadoop102 hadoop103 hadoop104
        do
                echo ---------- zookeeper $i status ----------
                ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status"
        done
}
;;
esac

2)增加脚本执行权限

[liyibin@hadoop102 bin]$ chmod u+x zk.sh

3)Zookeeper 集群启动脚本

[liyibin@hadoop102 module]$ zk.sh start

4)Zookeeper 集群停止脚本

[liyibin@hadoop102 module]$ zk.sh stop

2.1.3 选举机制

1)选举参考字段
(1)SID服务器ID。用来唯一标识一台ZooKeeper集群中的机器,每台机器不能重复,和 myid 一致

(2)ZXID事务 IDZXID是一个事务ID,用来标识一次服务器状态的变更。在某一时刻,集群中的每台机器的ZXID值不一定完全一致,这和ZooKeeper服务器对于客户端“更新请求”的处理逻辑有关。

(3)Epoch每个 Leader 任期的代号。没有 Leader 时同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加

2)第一次启动
在这里插入图片描述

(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)非第一次启动
(1)当 ZooKeeper 集群中的一台服务器出现以下两种情况之一时,就会开始进入 Leader 选举

  • 服务器初始化启动
  • 服务器运行期间无法和 Leader 保持连接

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

  • 集群中本来就一级存在一个 Leader。对于第一种已经存在 Leader 的情况,机器试图去选举 Leade r时,会被告知当前服务器的 Leader 信息,对于该机器来说,仅仅需要和Leader 机器建立连接,并进行状态同步即可。
  • 集群中确实不存在 Leader。假设 ZooKeeper 由 5 台服务器组成,SID分别为 1、2、3、4、5,ZXID分别为8、8、8、7、7,并且此时SID为3的服务器是Leader。某一时刻,3 和 5 服务器出现故障,因此开始进行Leader选举。

SID为1、2、4的机器投票情况:
在这里插入图片描述
选举 Leader 规则:

  1. EPOCH 大的直接胜出
  2. EPOCH 相同,事务 id 大的胜出
  3. 事务 id 相同,服务器 id 大的胜出

最终服务器2成为新的 Leader。

2.2 客户端命令行操作

2.2.1 命令行语法

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

1)启动客户端

[liyibin@hadoop102 zookeeper-3.5.7]$ bin/zkCli.sh -server hadoop102:2181

2)显示所有操作命令

[zk: hadoop102:2181(CONNECTED) 0] help

2.2.2 znode 节点数据信息

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

[zk: hadoop102:2181(CONNECTED) 1] ls /
[zookeeper]

2)查看当前节点的详细数据

[zk: hadoop102:2181(CONNECTED) 2] 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 = 0x300000021
cversion = 7
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 3

(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 子节点数量

2.2.3 节点类型(持久/短暂/有序号/无须号)

在这里插入图片描述

(1)持久化目录节点
客户端与Zookeeper断开连接后,该节点依旧存在。

(2)持久化顺序编号目录节点
客户端与Zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号。

(3)临时目录节点
客户端与Zookeeper断开连接后,该节点被删除。

(4)临时顺序编号目录节点
客户端与 Zookeeper 断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号。

说明:创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护。

注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序。

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

[zk: hadoop102:2181(CONNECTED) 3] create /sanguo 'diaochan'
Created /sanguo

[zk: hadoop102:2181(CONNECTED) 4] create /sanguo/shuguo 'liubei'
Created /sanguo/shuguo

2)获取节点的值

[zk: hadoop102:2181(CONNECTED) 5] get -s /sanguo
diaochan
cZxid = 0x500000004
ctime = Mon Nov 22 21:20:36 CST 2021
mZxid = 0x500000004
mtime = Mon Nov 22 21:20:36 CST 2021
pZxid = 0x500000005
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 1

[zk: hadoop102:2181(CONNECTED) 6] get -s /sanguo/shuguo
liubei
cZxid = 0x500000005
ctime = Mon Nov 22 21:21:01 CST 2021
mZxid = 0x500000005
mtime = Mon Nov 22 21:21:01 CST 2021
pZxid = 0x500000005
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 6
numChildren = 0

3)创建带序号的节点(永久节点 + 带序号)

# 先创建一个普通的根节点/
[zk: hadoop102:2181(CONNECTED) 7] create /sanguo/weiguo 'caocao'
Created /sanguo/weiguo

# 创建带序号的节点
[zk: hadoop102:2181(CONNECTED) 8] create -s /sanguo/weiguo/zhangliao 'zhangliao'
Created /sanguo/weiguo/zhangliao0000000000
[zk: hadoop102:2181(CONNECTED) 9] create -s /sanguo/weiguo/zhangliao 'zhangliao'
Created /sanguo/weiguo/zhangliao0000000001
[zk: hadoop102:2181(CONNECTED) 10] create -s /sanguo/weiguo/zhangliao 'zhangliao'
Created /sanguo/weiguo/zhangliao0000000002

如果原来没有序号节点,序号从 0 开始依次递增。

4)创建临时节点(短暂节点 + 不带序号 or 带序号)
(1)创建临时不带序号节点

[zk: hadoop102:2181(CONNECTED) 11] create -e /sanguo/wuguo 'zhouyu'
Created /sanguo/wuguo

(2)创建临时带序号节点

[zk: hadoop102:2181(CONNECTED) 12] create -e -s /sanguo/wuguo 'zhouyu'
Created /sanguo/wuguo0000000003

(3)在当前客户端是能查看到的临时节点

[zk: hadoop102:2181(CONNECTED) 23] ls /sanguo
[shuguo, weiguo, wuguo, wuguo0000000003]

(4)退出当前客户端后再进入

[zk: hadoop102:2181(CONNECTED) 0] ls /sanguo
[shuguo, weiguo]

临时节点已被删除

5)修改节点的数值

[zk: hadoop102:2181(CONNECTED) 1] set /sanguo/weiguo 'simayi'

2.2.4 监听器原理

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

详解:

  1. 首先有一个 main线程
  2. 再 main 线程中创建 zookeeper 客户端,这是会创建两个线程,一个负责网络连接通信(connect),一个负责监听(listener)。
  3. 通过 connect 线程将注册的监听事件发送给 zookeeper。
  4. 在 zookeeper 的注册监听器列表中将注册的监听事件添加到列表中。
  5. zookeeper 监听到有数据或路径变化,就会将这个消息发送给 listener 线程。
  6. listener 线程内部调用了 process() 方法。

常用的监听:

  1. 监听节点数据的变化 get path [watch]
  2. 监听子节点增减的变化 ls path [watch]

1)节点值的变化监听
(1)在 hadoop104 上监听 /sanguo 节点数据变化

[zk: localhost:2181(CONNECTED) 0] get -w /sanguo
diaochan

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

[zk: hadoop102:2181(CONNECTED) 2] set /sanguo 'xisi'

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

[zk: localhost:2181(CONNECTED) 1] get -w /sanguo
diaochan
[zk: localhost:2181(CONNECTED) 2] 
WATCHER::

WatchedEvent state:SyncConnected type:NodeDataChanged path:/sanguo

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

[zk: localhost:2181(CONNECTED) 0] ls -w /sanguo
[shuguo, weiguo]

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

[zk: hadoop102:2181(CONNECTED) 3] create /sanguo/jinguo 'simayi'
Created /sanguo/jinguo

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

[zk: localhost:2181(CONNECTED) 1] 
WATCHER::

WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/sanguo

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

2.2.5 节点删除与查看

1)删除节点

[zk: hadoop102:2181(CONNECTED) 5] delete /sanguo/jinguo

2)递归删除节点

[zk: hadoop102:2181(CONNECTED) 6] deleteall /sanguo/shuguo

3)查看节点状态

[zk: hadoop102:2181(CONNECTED) 7] stat /sanguo
cZxid = 0x500000004
ctime = Mon Nov 22 21:20:36 CST 2021
mZxid = 0x500000017
mtime = Mon Nov 22 22:11:12 CST 2021
pZxid = 0x50000001d
cversion = 9
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 1

2.3 客户端 API 操作

2.3.1 IDEA 环境搭建

1)创建一个 maven 工程

2)pom 文件添加依赖

<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>

3)在项目根目录创建 log4j.properties

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

4)创建测试类 ZkClientTest

2.3.2 创建 Zookeeper 客户端

private final int sessionTimeout = 2000;
private final String connectString = "hadoop102,hadoop103,hadoop104";
private ZooKeeper zkClient;

@Before
public void init() throws IOException {
    zkClient = new ZooKeeper(connectString, sessionTimeout, watchedEvent -> {
        // 收到事件通知后的回调函数(用户的业务逻辑)
        System.out.println(watchedEvent.getType() + "--" + watchedEvent.getPath());

        try {
            List<String> children = zkClient.getChildren("/", true);

            System.out.print("[");
            for (int i = 0; i < children.size(); i++) {
                if (i != 0) {
                    System.out.print(", ");
                }
                System.out.print(children.get(i));
            }

            System.out.println("]");
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }
    });
}

2.3.3 创建子节点

@Test
public void create() throws KeeperException, InterruptedException {
    String path = zkClient.create(
            "/test", "liyibin".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    System.out.println(path);
}

在 hadoo102 上查看创建情况

[zk: hadoop102:2181(CONNECTED) 9] get -s /test
liyibin

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

@Test
public void watchChild() throws KeeperException, InterruptedException {
    // 监听节点
    zkClient.getChildren("/", true);
	// 延时阻塞
    Thread.sleep(Integer.MAX_VALUE);
}

1)在 IDEA 控制台看到看到如下节点

None--null
[servers, zookeeper, test, locks, sanguo]

2)在 hadoop102 的客户端创建一个节点

[zk: hadoop102:2181(CONNECTED) 10] create /test1 'liyibin'

观察 IDEA 控制台

NodeChildrenChanged--/
[servers, zookeeper, test, locks, test1, sanguo]

3)在 hadoop102 的客户端上删除节点 /test1

[zk: hadoop102:2181(CONNECTED) 11] delete /test1

观察 IDEA 控制台

NodeChildrenChanged--/
[servers, zookeeper, test, locks, sanguo]

2.3.4 判断节点是否存在

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

    System.out.println(stat != null);
}

2.4 客户端向服务端写数据流程

1)直接发送给 Leader 节点
在这里插入图片描述
2)发送给 follower 节点
在这里插入图片描述

4 服务器动态上下线案例

4.1 需求

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

4.2 代码实现

1)先在集群上创建 /servers 节点

[zk: localhost:2181(CONNECTED) 5] create /servers 'servers'
Created /servers

2)服务端代码

public class DistributeServer {

    private int sessionTimeout = 2000;
    private String connectString = "hadoop102,hadoop103,hadoop104";
    private final String parent = "/servers";

    private ZooKeeper zkServer;

    public DistributeServer() throws IOException {
        init();
    }

    public void init() throws IOException {
        zkServer = new ZooKeeper(connectString, sessionTimeout, watchedEvent -> {
        });
    }

    /**
     * 注册服务器
     */
    public void register(String hostname) throws KeeperException, InterruptedException {
        String name = zkServer.create(
                parent + "/" + hostname, hostname.getBytes(),
                ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

        System.out.println(hostname + " is online in" + name);
    }

    /**
     * 业务功能
     */
    private void business(String hostname) throws InterruptedException {
        System.out.println(hostname + " is working...");
        Thread.sleep(Integer.MAX_VALUE);
    }

    public static void main(String[] args) throws KeeperException, InterruptedException, IOException {
        DistributeServer server = new DistributeServer();
        server.register(args[0]);
        server.business(args[0]);
    }
}

3)客户端代码

public class DistributeClient {

    private int sessionTimeout = 2000;
    private String connectString = "hadoop102,hadoop103,hadoop104";
    private final String parent = "/servers";

    private ZooKeeper zkServer;

    public DistributeClient() throws IOException {
        init();
    }

    public void init() throws IOException {
        zkServer = new ZooKeeper(connectString, sessionTimeout, watchedEvent -> {
            try {
                // 再次监听
                listen();
            } catch (KeeperException | InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

    public void listen() throws KeeperException, InterruptedException {
        // 1 获取服务器子节点信息,并且对父节点进行监听
        List<String> paths = zkServer.getChildren(parent, true);

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

            servers.add(new String(data));
        }
        // 4 打印服务器列表信息
        System.out.println(servers);
    }

    private void business() throws InterruptedException {
        System.out.println("client is working ...");
        Thread.sleep(Long.MAX_VALUE);
    }

    public static void main(String[] args) throws KeeperException, InterruptedException, IOException {
        DistributeClient client = new DistributeClient();

        client.listen();
        client.business();
    }
}

4)测试
(1)启动 DistributeClient 客户端

(2)在 hadoop102 上 zk 的客户端 /servers 目录上创建临时带序号节点

[zk: localhost:2181(CONNECTED) 6] create -s -e /servers/hadoop102 'hadoop102'
Created /servers/hadoop1020000000000
[zk: localhost:2181(CONNECTED) 7] create -s -e /servers/hadoop103 'hadoop103'
Created /servers/hadoop1030000000001

(3)观察 Idea 控制台变化

[hadoop102]
[hadoop102, hadoop103]

(4)删除操作

[zk: localhost:2181(CONNECTED) 8] delete /servers/hadoop1020000000000
[zk: localhost:2181(CONNECTED) 9] delete /servers/hadoop1030000000001

(5)观察 Idea 控制台变化

[hadoop103]
[]

(6)启动 DistributeServer 服务

hadoop102 is online in/servers/hadoop1020000000002
hadoop102 is working...

(7)观察 Idea 控制台变化

[hadoop102]

5 Zookeeper 分布式锁案例

5.1 实现原理

在这里插入图片描述
1)接收到请求,再 /locks 节点下创建一个临时顺序节点
2)判断自己是否是当前节点下最小节点,是获取锁,不是,对前一个节点进行监听
3)获取锁后,处理业务。delete 节点释放锁,然后下面的节点收到通知,重复第二步判断

5.2 原生 Zookeeper 实现

1)代码

public class DistributeLock {

    private final int timeout = 2000;
    private final String connect = "hadoop103,hadoop103,hadoop104";

    private final String subNode = "/seq-";
    private final String parentNode = "/locks";
    private final ZooKeeper zk;

    /**
     * 等待连接创建完成
     */
    private CountDownLatch connectLatch = new CountDownLatch(1);
    private CountDownLatch waitLatch = new CountDownLatch(1);

    /**
     * 目前获取锁的节点
     */
    private String waitNode;
    /**
     * 当前创建的节点
     */
    private String currentNode;

    public DistributeLock() throws Exception {
        zk = new ZooKeeper(connect, timeout, watchedEvent -> {
            if (watchedEvent.getState() == Watcher.Event.KeeperState.SyncConnected) {
                connectLatch.countDown();
            }

            // 发生了 waitPath 节点的删除时间
            if (watchedEvent.getType() == Watcher.Event.EventType.NodeDeleted &&
                    watchedEvent.getPath().equals(waitNode)) {
                waitLatch.countDown();
            }
        });

        // 等待连接完成
        connectLatch.await();

        // 建立根节点
        Stat stat = zk.exists(parentNode, false);
        if (stat == null) {
            // 创建根节点
            zk.create(parentNode, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }

    public void lock() {
        try {
            // 创建一个临时顺序节点
            currentNode = zk.create(
                    parentNode + subNode, null,
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

            // 查询子节点
            List<String> children = zk.getChildren(parentNode, false);

            // 如果只有一个子节点,表示获得了锁
            if (children.size() == 1) {
                return;
            }

            // 从小到大排序
            Collections.sort(children);
            // 获取当前节点
            String curNode = currentNode.substring((parentNode + "/").length());
            // 当前节点的索引
            int index = children.indexOf(curNode);
            if (index == -1) {
                throw new IllegalArgumentException();
            }

            // 表示是序号最小的子节点,获取锁
            if (index == 0) {
                return;
            }

            // 上一个节点 - 需要监听该节点,知道被删除
            String preChildNode = children.get(index - 1);

            waitNode = parentNode + "/" + preChildNode;

            zk.getData(waitNode, true, new Stat());

            // 等待释放执行
            waitLatch.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void unlock() {
        try {
            zk.delete(currentNode, -1);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }
}

2)测试

public static void main(String[] args) throws Exception {
    DistributeLock lock1 = new DistributeLock();

    DistributeLock lock2 = new DistributeLock();

    new Thread(() -> {
        lock1.lock();
        System.out.println("lock1 获取锁");
        try {
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock1.unlock();
        System.out.println("lock1 释放锁");
    }).start();

    new Thread(() -> {
        lock2.lock();
        System.out.println("lock2 获取锁");
        try {
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock2.unlock();
        System.out.println("lock2 释放锁");
    }).start();
}

运行结果

lock1 获取锁
lock1 释放锁
lock2 获取锁
lock2 释放锁

5.3 Curator 框架实现

1)官网地址:https://curator.apache.org/index.html

2)代码实操
(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)测试代码

public class CuratorLockDemo {

    private final int connectTimeout = 2000;
    private final int sessionTimeout = 2000;
    private final String connectString = "hadoop103:2181,hadoop103:2181,hadoop104:2181";

    private final String parentNode = "/locks";

    public void test() {
        InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), parentNode);
        InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), parentNode);

        new Thread(() -> {
            try {
                // 获取锁对象
                lock1.acquire();
                System.out.println("lock1 获取锁");
                Thread.sleep(5 * 1000);

                // 释放锁对象
                lock1.release();
                System.out.println("lock1 释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                // 获取锁对象
                lock2.acquire();
                System.out.println("lock2 获取锁");
                Thread.sleep(5 * 1000);

                // 释放锁对象
                lock2.release();
                System.out.println("lock2 释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }

    private CuratorFramework getCuratorFramework() {
        // 通过工厂创建 Curator
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(connectString)
                .connectionTimeoutMs(connectTimeout)
                .sessionTimeoutMs(sessionTimeout)
                // 重试策略,初试时间 3 秒,重试 3 次
                .retryPolicy(new ExponentialBackoffRetry(3000, 3))
                .build();

        // 开启连接
        client.start();
        System.out.println("zookeeper 初始化完成");

        return client;
    }

    public static void main(String[] args) {
        new CuratorLockDemo().test();
    }
}

运行结果

lock2 获取锁
lock2 释放锁
lock1 获取锁
lock1 释放锁
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值