Zookeeper之旅

  • Zookeeper介绍
  • 概述
  • Zookeeper工作机制
  • 特点
  • 数据结构
  • 应用场景
  • 统一命名服务
  • 统一配置管理
  • 统一集群管理
  • 服务器动态上下线
  • 软负载均衡
  • 官网地址
  • Zookeeper 本地安装
  • 本地模式安装
  • 准备
  • 配置修改
  • 操作 Zookeeper
  • 配置参数解读
  • Zookerper 集群
  • 集群安装
  • 选举机制
  • Zookeeper选举机制——第一次启动
  • Zookeeper选举机制——非第一次启动
  • 客户端命令行操作
  • 命令行语法
  • znode 节点数据信息
  • 节点类型(持久/短暂/有序号/无序号)
  • 监听器原理
  • 节点的值变化监听
  • 节点的子节点变化监听(路径变化)
  • 节点删除与查看
  • 客户端 API 操作
  • IDEA 环境搭建
  • 创建 ZooKeeper 客户端
  • 创建子节点
  • 获取子节点并监听节点变化
  • 判断 Znode 是否存在
  • 客户端向服务端写数据流程
  • 写流程之写入请求直接发送给Leader节点
  • 写流程之写入请求发送给follower节点
  • 动态上下线监听
  • 需求
  • 需求分析
  • 具体实现
  • 测试
  • 在 Linux 命令行上操作增加减少服务器
  • 在 Idea 上操作增加减少服务器
  • Zookeeper 分布式锁
  • 原生 Zookeeper 实现分布式锁案例
  • Curator 框架实现分布式锁案例


Zookeeper介绍

概述

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

zookeeper心跳探活_zookeeper

Zookeeper工作机制

Zookeeper = 文件系统 + 通知机制

Zookeeper 从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架, 它负责存储和管理大家都关心的数据, 然后接受观察者的注册, 一旦这些数据的状态发生变化, Zookeeper 就将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应

zookeeper心跳探活_java_02

特点

  • Zookeeper:一个领导者(Leader) , 多个跟随者(Follower) 组成的集群
  • 集群中只要有半数以上节点存活, Zookeeper集群就能正常服务。 所以 Zookeeper 适合安装奇数台服务器
  • 全局数据一致:每个Server保存一份相同的数据副本, Client无论连接到哪个Server, 数据都是一致的
  • 更新请求顺序执行, 来自同一个Client的更新请求按其发送顺序依次执行
  • 数据更新原子性, 一次数据更新要么成功, 要么失败
  • 实时性, 在一定时间范围内, Client能读到最新数据

zookeeper心跳探活_分布式_03

数据结构

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

zookeeper心跳探活_分布式_04

应用场景

  • 统一命名服务
  • 统一配置管理
  • 统一集群管理
  • 服务器节点动态上下线
  • 软负载均衡

统一命名服务

在分布式环境下, 经常需要对应用 / 服务进行统一命名, 便于识别
例如: IP不容易记住, 而域名容易记住

zookeeper心跳探活_分布式_05

统一配置管理

分布式环境下, 配置文件同步非常常见

  • 一般要求一个集群中, 所有节点的配置信息是一致的, 比如 Kafka 集群
  • 对配置文件修改后, 希望能够快速同步到各个节点上

配置管理可交由 ZooKeeper 实现

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

zookeeper心跳探活_zookeeper心跳探活_06

统一集群管理

分布式环境中, 实时掌握每个节点的状态是必要的

  • 可根据节点实时状态做出一些调整

ZooKeeper可以实现实时监控节点状态变化

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

zookeeper心跳探活_java_07

服务器动态上下线

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

zookeeper心跳探活_zookeeper_08

软负载均衡

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

zookeeper心跳探活_zookeeper_09

官网地址

https://zookeeper.apache.org/

zookeeper心跳探活_cloud native_10

zookeeper心跳探活_zookeeper_11

zookeeper心跳探活_java_12

下载 Linux 环境安装的 tar 包

zookeeper心跳探活_java_13

Zookeeper 本地安装

本地模式安装

准备

安装 JDK

java -version

zookeeper心跳探活_cloud native_14

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

解压到指定目录

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

zookeeper心跳探活_zookeeper_15

修改名称

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

zookeeper心跳探活_cloud native_16

配置修改

在 /opt/module/zookeeper-3.5.7/ 这个目录上创建 zkData 文件夹

mkdir zkData

zookeeper心跳探活_cloud native_17

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

mv zoo_sample.cfg zoo.cfg

zookeeper心跳探活_zookeeper_18

打开 zoo.cfg 文件,修改 dataDir 路径

vim zoo.cfg

修改内容 :

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

zookeeper心跳探活_分布式_19

操作 Zookeeper

启动 Zookeeper

bin/zkServer.sh start

zookeeper心跳探活_分布式_20

查看进程是否启动

jps

zookeeper心跳探活_zookeeper心跳探活_21

查看状态

bin/zkServer.sh status

zookeeper心跳探活_java_22

启动客户端

bin/zkCli.sh

zookeeper心跳探活_zookeeper心跳探活_23

退出客户端

quit

zookeeper心跳探活_分布式_24

停止 Zookeeper

bin/zkServer.sh stop

zookeeper心跳探活_zookeeper心跳探活_25

配置参数解读

Zookeeper中的配置文件zoo.cf

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

zookeeper心跳探活_cloud native_26

zookeeper心跳探活_分布式_27

initLimit = 10: LF初始通信时限

Leader和Follower初始连接时能容忍的最多心跳数(tickTime的数量)

zookeeper心跳探活_zookeeper_28

zookeeper心跳探活_java_29

syncLimit = 5: LF同步通信时限

Leader和Follower之间通信时间如果超过syncLimit * tickTime, Leader认为Follwer死掉,从服务器列表中删除Follwer。

zookeeper心跳探活_java_30

zookeeper心跳探活_zookeeper_31

dataDir: 保存Zookeeper中的数据

注意: 默认的tmp目录,容易被Linux系统定期删除,所以一般不用默认的tmp目录

zookeeper心跳探活_zookeeper心跳探活_32

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

zookeeper心跳探活_zookeeper心跳探活_33

Zookerper 集群

集群安装


选举机制

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

zookeeper心跳探活_zookeeper心跳探活_34

  • SID: 服务器ID
  • ZXID:事务ID
  • Epoch: 每个Leader任期的代号

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一样当小弟

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

zookeeper心跳探活_cloud native_35

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

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

而当一台机器进入 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选举

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

(EPOCH, ZXID, SID )

(EPOCH, ZXID, SID )

(EPOCH, ZXID, SID )

(1, 8, 1)

(1, 8, 2)

(1, 7, 4)

选举Leader规则:

  • EPOCH大的直接胜出
  • EPOCH相同,事务id大的胜出
  • 事务id相同,服务器id大的胜出

客户端命令行操作

命令行语法

命令基本语法

功能描述

help

显示所有操作命令

ls path

使用 ls 命令来查看当前 znode 的子节点 [可监听] -w 监听子节点变化 -s 附加次级信息

create

普通创建 -s 含有序列 -e 临时(重启或者超时消失)

get path

获得节点的值 [可监听] -w 监听节点内容变化 -s 附加次级信息

set

设置节点的具体值

stat

查看节点状态

delete

删除节点

deleteall

递归删除节点

启动客户端

bin/zkCli.sh -server cpucode100:2181

zookeeper心跳探活_zookeeper_36

显示所有操作命令

help

zookeeper心跳探活_cloud native_37

znode 节点数据信息

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

ls /

zookeeper心跳探活_cloud native_38

查看当前节点详细数据

ls -s /

zookeeper心跳探活_java_39

  • czxid: 创建节点的事务 zxid

每次修改 ZooKeeper 状态都会产生一个 ZooKeeper 事务 ID。事务 ID 是 ZooKeeper 中所有修改总的次序。每次修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之前发生

  • ctime: znode 被创建的毫秒数(从 1970 年开始)
  • mzxid: znode 最后更新的事务 zxid
  • mtime: znode 最后修改的毫秒数(从 1970 年开始)
  • pZxid: znode 最后更新的子节点 zxid
  • cversion: znode 子节点变化号, znode 子节点修改次数
  • dataversion: znode 数据变化号
  • aclVersion: znode 访问控制列表的变化号
  • ephemeralOwner: 如果是临时节点,这个是 znode 拥有者的 session id。如果不是临时节点则是 0
  • dataLength: znode 的数据长度
  • numChildren: znode 子节点数量

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

持久(Persistent) :客户端和服务器端断开连接后, 创建的节点不删除

短暂(Ephemeral) :客户端和服务器端断开连接后, 创建的节点自己删除

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

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

zookeeper心跳探活_cloud native_40

  • 持久化目录节点

客户端与 Zookeeper 断开连接后, 该节点依旧存在

  • 持久化顺序编号目录节点

客户端与 Zookeeper 断开连接后, 该节点依旧存在, 只是 Zookeeper 给该节点名称进行顺序编号

  • 临时目录节点

客户端与 Zookeeper 断开连接后, 该节点被删除

  • 临时顺序编号目录节点

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

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

create /cpu "java"

zookeeper心跳探活_分布式_41

create /cpu/code "scala"

zookeeper心跳探活_zookeeper_42

注意:创建节点时, 要赋值

获得节点的值

get -s /cpu

zookeeper心跳探活_分布式_43

get -s /cpu/code

zookeeper心跳探活_cloud native_44

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

先创建一个普通的根节点 cpu/hello

create /cpu/hello "hi"

zookeeper心跳探活_zookeeper_45

创建带序号的节点

create -s /cpu/hello/boby "handsome"

zookeeper心跳探活_分布式_46

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

创建短暂节点(短暂节点 + 不带序号 or 带序号)

创建短暂的不带序号的节点

create -e /cpu/hello "head"

zookeeper心跳探活_java_47

创建短暂的带序号的节点

create -e -s /cpu/yellow "good"

在当前客户端是能查看到的

ls /cpu

zookeeper心跳探活_分布式_48

退出当前客户端然后再重启客户端

quit

zookeeper心跳探活_java_49

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

bin/zkCli.sh -server cpucode100:2181
ls /cpu

zookeeper心跳探活_zookeeper_50

修改节点数据值

set /cpu/hello "cpuCode"

zookeeper心跳探活_分布式_51

监听器原理

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

监听原理详解

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

zookeeper心跳探活_cloud native_52

常见的监听

监听节点数据的变化

get path [watch]

监听子节点增减的变化

ls path [watch]
节点的值变化监听

在 cpucode102 主机上注册监听 /cpu 节点数据变化

bin/zkCli.sh -server cpucode102:2181
get -w /cpu

zookeeper心跳探活_zookeeper心跳探活_53

在 cpucode100 主机上修改 /cpu 节点的数据

set /cpu "mini"

zookeeper心跳探活_zookeeper_54

观察 cpucode102 主机收到数据变化的监听

zookeeper心跳探活_java_55

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

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

在 cpucode102 主机上注册监听 /cpu 节点的子节点变化

ls -w /cpu

zookeeper心跳探活_cloud native_56

在 cpucode100 主机 /cpu 节点上创建子节点

create /cpu/xin "beautiful"

zookeeper心跳探活_cloud native_57

观察 cpucode102 主机收到子节点变化的监听

zookeeper心跳探活_cloud native_58

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

节点删除与查看

查看节点状态

stat /cpu

zookeeper心跳探活_zookeeper心跳探活_59

删除节点

delete /cpu

zookeeper心跳探活_zookeeper_60

递归删除节点

deleteall /cpu

zookeeper心跳探活_java_61

客户端 API 操作

前提:保证 cpucode100、 cpucode101、 cpucode102 服务器上 Zookeeper 集群服务端启动

zookeeper心跳探活_cloud native_62

IDEA 环境搭建

创建一个工程: zookeeper

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

zookeeper心跳探活_java_63

拷贝log4j.properties文件到项目根目录

需要在项目的 src/main/resources 目录下,新建一个文件,命名为“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

zookeeper心跳探活_zookeeper_64

创建包名com.cpucode.zk

创建类名称zkClient

zookeeper心跳探活_zookeeper_65

创建 ZooKeeper 客户端

package com.cpucode.zk;

import org.apache.zookeeper.*;
import org.junit.Before;
import org.junit.Test;

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

/**
 * @author : cpucode
 * @date : 2021/11/28 15:07
 * @github : https://github.com/CPU-Code
 * @csdn : 
 */
public class ZkClient {
    // 注意:逗号左右不能有空格
    // 添加 ip映射
    private String connectString = "cpucode100:2181,cpucode101:2181,cpucode102:2181";
    private int sessionTimeOut = 2000;
    private ZooKeeper zkClient = null;

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

                // 再次启动监听
                try {
                    List<String> children = zkClient.getChildren("/", true);
                    for (String child : children) {
                        System.out.println(child);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

创建子节点

/**
     * 创建子节点
     * @throws KeeperException
     * @throws InterruptedException
     */
    @Test
    public void create() throws KeeperException, InterruptedException {
        /**
         * path : 要创建的节点的路径
         * data : 节点数据
         * acl : 节点权限
         * createMode : 节点的类型
         */
        String nodeCreated = zkClient.create("/createTest1",
                "handsome".getBytes(),
                ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT);
    }

测试:在 cpucode100 的 zk 客户端上查看创建节点情况

get -s /createTest1

zookeeper心跳探活_分布式_66

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

/**
     * 获取子节点
     * @throws KeeperException
     * @throws InterruptedException
     */
    @Test
    public void getChildren() throws KeeperException, InterruptedException {
        List<String> children = zkClient.getChildren("/", true);
        for (String child : children) {
            System.out.println(child);
        }

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

在 IDEA 控制台上看到如下节点:

zookeeper心跳探活_zookeeper_67

在 cpucode100 的客户端上创建再创建一个节点/cpuCode,观察 IDEA 控制台

create /cpuCode "handsome"

zookeeper心跳探活_zookeeper_68

在 cpucode100 的客户端上删除节点/cpuCode,观察 IDEA 控制台

delete /cpuCode

zookeeper心跳探活_cloud native_69

判断 Znode 是否存在

/**
     * 判断 znode 是否存在
     * @throws KeeperException
     * @throws InterruptedException
     */
    @Test
    public void exist() throws KeeperException, InterruptedException {
        Stat stat = zkClient.exists("/createTest1", false);

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

zookeeper心跳探活_java_70

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

写流程之写入请求直接发送给Leader节点

zookeeper心跳探活_java_71

写流程之写入请求发送给follower节点

zookeeper心跳探活_zookeeper心跳探活_72

动态上下线监听

需求

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

需求分析

zookeeper心跳探活_分布式_73

具体实现

先在集群上创建/servers 节点

create /servers "servers"

zookeeper心跳探活_分布式_74

在 Idea 中创建包名: com.cpucode.distributeTest

zookeeper心跳探活_java_75

服务器端向 Zookeeper 注册代码

package com.cpucode.distributeTest;

import org.apache.zookeeper.*;
import org.omg.CORBA.UserException;

import java.io.IOException;

/**
 * @author : cpucode
 * @date : 2021/11/28 16:25
 * @github : https://github.com/CPU-Code
 * @csdn : 
 */
public class DistributeServer {
    private static String connectString = "cpucode100:2181,cpucode101:2181,cpucode102:2181";
    private static int sessionTimeOut = 2000;
    private ZooKeeper zk = null;
    private String parentNode = "/servers";

    public static void main(String[] args) throws Exception{
        // 1 获取 zk 连接
        DistributeServer server = new DistributeServer();
        server.getConnect();

        // 2 利用 zk 连接注册服务器信息
        server.registerServer(args[0]);
        // 3 启动业务功能
        server.business(args[0]);
    }

    /**
     * 创建到 zk 的客户端连接
     * @throws IOException
     */
    public void getConnect() throws IOException {
        zk = new ZooKeeper(connectString, sessionTimeOut, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {

            }
        });
    }

    /**
     *  注册服务器
     * @param hostname
     * @throws Exception
     */
    public void registerServer(String hostname) throws Exception {
        String create = zk.create(parentNode + "/server",
                hostname.getBytes(),
                ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL);

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

    /**
     * 业务功能
     * @param hostname
     * @throws Exception
     */
    public void business(String hostname) throws Exception {
        System.out.println(hostname + "is working ...");

        Thread.sleep(Long.MAX_VALUE);
    }
}

客户端代码

package com.cpucode.distributeTest;

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;

/**
 * @author : cpucode
 * @date : 2021/11/28 16:25
 * @github : https://github.com/CPU-Code
 * @csdn : 
 */
public class DistributeClient {
    private static String connectString = "cpucode100:2181,cpucode101:2181,cpucode102:2181";
    private static int sessionTimeout = 2000;
    private ZooKeeper zk = null;
    private String parentNode = "/servers";

    public static void main(String[] args) throws Exception{
        // 1 获取 zk 连接
        DistributeClient client = new DistributeClient();
        client.getConnect();

        // 2 获取 servers 的子节点信息,从中获取服务器信息列表
        client.getServerList();
        // 3 业务进程启动
        client.business();
    }

    /**
     * 创建到 zk 的客户端连接
     * @throws IOException
     */
    public void getConnect() throws IOException {
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                // 再次启动监听
                try {
                    getServerList();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * 获取服务器列表信息
     * @throws Exception
     */
    public void getServerList() throws Exception {
        // 1 获取服务器子节点信息,并且对父节点进行监听
        List<String> children = zk.getChildren(parentNode, true);

        // 2 存储服务器信息列表
        ArrayList<String> servers = new ArrayList<>();

        // 3 遍历所有节点,获取节点中的主机名称信息
        for (String child : children) {
            byte[] data = zk.getData(parentNode + "/" + child, false, null);
            servers.add(new String(data));
        }

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

    /**
     * 业务功能
     * @throws Exception
     */
    public void business() throws Exception{
        System.out.println("client is working ...");
        Thread.sleep(Long.MAX_VALUE);
    }
}

测试

在 Linux 命令行上操作增加减少服务器

启动 DistributeClient 客户端

zookeeper心跳探活_java_76

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

create -e -s /servers/cpucode100 "cpucode100"

zookeeper心跳探活_cloud native_77

观察 Idea 控制台变化

zookeeper心跳探活_分布式_78

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

create -e -s /servers/cpucode101 "cpucode101"

zookeeper心跳探活_zookeeper心跳探活_79

观察 Idea 控制台变化

zookeeper心跳探活_zookeeper心跳探活_80

执行删除操作

delete /servers/hadoop102

zookeeper心跳探活_zookeeper_81

观察 Idea 控制台变化

zookeeper心跳探活_java_82

在 Idea 上操作增加减少服务器

启动 DistributeClient 客户端(如果已经启动过,不需要重启)

启动 DistributeServer 服务

点击 Edit Configurations…

zookeeper心跳探活_zookeeper心跳探活_83

观察 DistributeServer 控制台

zookeeper心跳探活_zookeeper_84

观察 DistributeClient 控制台

zookeeper心跳探活_cloud native_85

Zookeeper 分布式锁

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

zookeeper心跳探活_java_86

原生 Zookeeper 实现分布式锁案例

分布式锁实现

package com.cpucode.distributeLock;

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

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

/**
 * @author : cpucode
 * @date : 2021/11/28 17:30
 * @github : https://github.com/CPU-Code
 * @csdn : 
 */
public class DistributeLock {
    // zookeeper server 列表
    private final String connectString = "cpucode100:2181,cpucode101:2181,cpucode102:2181";
    // 超时时间
    private final int sessionTimeout = 2000;
    private ZooKeeper zk = null;

    private String rootNode = "locks";
    private String subNode = "seq-";

    //ZooKeeper 连接
    private CountDownLatch connectLatch = new CountDownLatch(1);
    //ZooKeeper 节点等待
    private CountDownLatch waitLatch = new CountDownLatch(1);

    // 当前 client 等待的子节点
    private String waitPath;
    // 当前 client 创建的子节点
    private String currentNode;

    /**
     *  和 zk 服务建立连接,并创建根节点
     * @throws IOException
     * @throws InterruptedException
     * @throws KeeperException
     */
    public DistributeLock() throws IOException, InterruptedException, KeeperException {
        // 获取连接
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                // 连接建立时, 打开 latch, 唤醒 wait 在该 latch 上的线程
                if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
                    connectLatch.countDown();
                }

                // 发生了 waitPath 的删除事件
                if (watchedEvent.getType() == Event.EventType.NodeDeleted &&
                        watchedEvent.getPath().equals(waitPath)) {
                    waitLatch.countDown();
                }
            }
        });

        // 等待连接建立
        connectLatch.await();

        // 获取根节点状态
        Stat stat = zk.exists("/" + rootNode, false);

        // 如果根节点不存在,则创建根节点,根节点类型为永久节点
        if (stat == null) {
            System.out.println("根节点不存在");

            // 创建一下根节点
            zk.create("/" + rootNode,
                    new byte[0],
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.PERSISTENT);
        }
    }

    /**
     * 对zk加锁
     */
    public void zkLock() {
        // 创建对应的临时带序号节点
        try {
            //在根节点下创建临时顺序节点,返回值为创建的节点路径
            currentNode = zk.create("/" + rootNode + "/" + subNode,
                    null,
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            // wait一小会, 让结果更清晰一些
            Thread.sleep(10);

            // 判断创建的节点是否是最小的序号节点,如果是获取到锁;如果不是,监听他序号前一个节点
            List<String> childrenNodes = zk.getChildren("/" + rootNode, false);

            // 如果children 只有一个值,那就直接获取锁; 如果有多个节点,需要判断,谁最小
            if (childrenNodes.size() == 1) {
                return;
            } else {
                // 对根节点下的所有临时顺序节点进行从小到大排序
                Collections.sort(childrenNodes);

                // 获取节点名称 seq-00000000
                String thisNode = currentNode.substring(("/" + rootNode + "/").length());

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

                // 判断
                if (index == -1){
                    System.out.println("数据异常");
                } else if (index == 0){
                    // index == 0, 说明 thisNode 在列表中最小, 当前client 获得锁
                    return;
                } else {
                    // 获得排名比 currentNode 前 1 位的节点
                    this.waitPath = "/" + rootNode + "/" + childrenNodes.get(index - 1);

                    // 在 waitPath 上注册监听器, 当 waitPath 被删除时, zookeeper 会回调监听器的 process 方法
                    zk.getData(waitPath, true, new Stat());

                    //进入等待锁状态
                    waitLatch.await();

                    return;
                }
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解锁
     */
    public void unZkLock() {
        // 删除节点
        try {
            zk.delete(this.currentNode, -1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }
}

分布式锁测试

package com.cpucode.distributeLock;

import org.apache.zookeeper.KeeperException;

import java.io.IOException;

/**
 * @author : cpucode
 * @date : 2021/11/28 17:38
 * @github : https://github.com/CPU-Code
 * @csdn : 
 */
public class DistributeLockTest {
    public static void main(String[] args) throws InterruptedException, IOException, KeeperException {
        // 创建分布式锁 1
        final DistributeLock lock1 = new DistributeLock();
        // 创建分布式锁 2
        final DistributeLock lock2 = new DistributeLock();

        new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取锁对象
                try {
                    lock1.zkLock();

                    System.out.println("线程 1 获取锁");
                    Thread.sleep(5 * 1000);

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

        new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取锁对象
                try {
                    lock2.zkLock();

                    System.out.println("线程 2 获取锁");
                    Thread.sleep(5 * 1000);

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

观察控制台变化 :

zookeeper心跳探活_cloud native_87

Curator 框架实现分布式锁案例

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

  • 会话连接是异步的,需要自己去处理。比如使用 CountDownLatch
  • Watch 需要重复注册,不然就不能生效
  • 开发的复杂性还是比较高的
  • 不支持多节点删除和创建。需要自己去递归

Curator 是一个专门解决分布式锁的框架,解决了原生 Java API 开发分布式遇到的问题

官方文档: https://curator.apache.org/index.html

Curator 案例实操

<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>
package com.cpucode.curatorLock;

import org.apache.curator.RetryPolicy;
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;

/**
 * @author : cpucode
 * @date : 2021/11/28 21:01
 * @github : https://github.com/CPU-Code
 * @csdn : 
 */
public class CuratorLockTest {

    private static String rootNode = "/locks";
    // zookeeper server 列表
    private static String connectString = "cpucode100:2181,cpucode101:2181,cpucode102:2181";
    // connection 超时时间
    private static int connectionTimeout = 2000;
    // session 超时时间
    private static int sessionTimeOut = 2000;

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

    // 测试
    private void test() {
        // 创建分布式锁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();
    }

    /**
     *  分布式锁初始化
     * @return
     */
    private static CuratorFramework getCuratorFramework() {
        //重试策略,初试时间 3 秒,重试 3 次
        RetryPolicy policy = new ExponentialBackoffRetry(3000, 3);

        //通过工厂创建 Curator
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(connectString)
                        .connectionTimeoutMs(connectionTimeout)
                        .sessionTimeoutMs(sessionTimeOut)
                        .retryPolicy(policy).build();

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

观察控制台变化:

zookeeper心跳探活_java_88