Hadoop学习教程,从入门到精通, ZooKeeper 分布式协调服务 — 全面知识点与案例代码(5)

ZooKeeper 分布式协调服务 --- 全面知识点与案例代码


5.1 ZooKeeper 简介

5.1.1 知识点概述

ZooKeeper 是 Apache 软件基金会的一个开源项目,最初由 Google 的 Chubby 论文启发而来。它是一个分布式的、开放源码的分布式应用程序协调服务,是 Google Chubby 的一个开源实现。

核心定位:

  • ZooKeeper 是一个分布式协调服务框架
  • 它提供统一的命名服务、配置管理、分布式同步、组服务等功能
  • 它本身就是一个分布式程序,只要半数以上节点存活就能正常服务
  • 适合存储和协调关键的分布式系统数据(但不适合做海量数据存储)

架构背景:

复制代码
┌─────────────────────────────────────────────┐
│            分布式系统面临的挑战               │
├─────────────────────────────────────────────┤
│  1. 分布式锁 --- 多节点竞争共享资源            │
│  2. 配置管理 --- 多节点配置需要统一管理        │
│  3. 命名服务 --- 统一的服务注册与发现          │
│  4. 选举机制 --- Leader 选举                   │
│  5. 分布式队列 --- 跨节点的任务协调            │
│  6. 集群管理 --- 节点上下线感知                │
└─────────────────────────────────────────────┘
                    ↓
         ZooKeeper 提供统一解决方案

5.2 ZooKeeper 特性

5.2.1 知识点详解

特性 说明
全局数据一致 每个 Server 保存一份相同的数据副本,Client 无论连接到哪个 Server,数据都是一致的
可靠性 如果消息被一台服务器接受,那么将被所有服务器接受
顺序性 包括全局有序和偏序两种,全局有序指如果消息 a 在消息 b 之前被发布,则 a 排在 b 前面
数据更新原子性 一次数据更新要么成功(半数以上节点成功),要么失败,不会出现中间状态
实时性 ZooKeeper 保证客户端将在一个时间间隔范围内获得服务器的更新信息或服务器失效的信息

5.2.2 ZooKeeper 设计目标

复制代码
目标1:简单的数据模型
    → 树形结构的命名空间(类似文件系统)

目标2:可以构建集群
    → 多台 ZooKeeper 服务器组成集群

目标3:顺序访问
    → 每次请求分配全局唯一的递增编号(ZXID)

目标4:高性能
    → 基于内存进行读写,适合读多写少的场景

5.3 ZooKeeper 集群架构

5.3.1 知识点详解

集群角色:

复制代码
┌──────────────────────────────────────────────────────────┐
│                   ZooKeeper 集群角色                      │
├──────────┬───────────────────────────────────────────────┤
│ Leader   │ 1. 处理事务请求(写请求)                      │
│ (领导者)  │ 2. 各个 follower 的调度者                     │
│          │ 3. 负责投票的发起和决议                        │
├──────────┼───────────────────────────────────────────────┤
│ Follower │ 1. 处理非事务请求(读请求)                    │
│ (跟随者)  │ 2. 参与 Leader 选举投票                      │
│          │ 3. 参与事务请求的投票                          │
├──────────┼───────────────────────────────────────────────┤
│ Observer │ 1. 处理非事务请求(读请求)                    │
│ (观察者)  │ 2. 不参与选举和投票                           │
│          │ 3. 用于扩展读能力,不影响写性能                │
└──────────┴───────────────────────────────────────────────┘

集群通信模型:

复制代码
     Client         Client         Client
       │               │               │
       │  读请求        │  写请求        │
       ▼               ▼               ▼
  ┌─────────┐   ┌─────────┐    ┌─────────┐
  │Follower1│   │Follower2│    │ Leader  │
  │         │   │         │    │         │
  └────┬────┘   └────┬────┘    └────┬────┘
       │               │              │
       │    ZAB 协议    │              │
       │◄─────────────►│◄────────────►│
       │               │              │
       ▼               ▼              ▼
  ┌─────────────────────────────────────┐
  │        统一的数据视图(一致)        │
  └─────────────────────────────────────┘

ZAB 协议(ZooKeeper Atomic Broadcast):

  • ZAB 协议是 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议
  • 所有写请求由 Leader 统一处理
  • Leader 将写请求转化为事务,广播给所有 Follower
  • 半数以上 Follower 确认后,Leader 才提交事务

5.4 ZooKeeper 数据模型

5.4.1 知识点详解

数据模型结构:

ZooKeeper 的数据模型是一棵树(层次化的命名空间),与标准文件系统类似。

复制代码
                    / (根节点)
                   /│\
                  / │ \
                 /  │  \
              /zk  /app  /config
              /│\   │      │
             / │ \  │      │
          c1 c2 c3 node1  db_host

节点(ZNode)说明:

属性 说明
path 节点路径,唯一标识,如 /app/config
data 节点存储的数据(默认最大 1MB)
stat 节点的状态信息(版本号、时间戳等)
children 子节点列表

ZNode 的四种类型:

复制代码
┌──────────────┬────────────────────────────────────────────────┐
│ 类型          │ 说明                                           │
├──────────────┼────────────────────────────────────────────────┤
│ 持久节点       │ 创建后一直存在,除非主动删除                    │
│ (PERSISTENT)  │ 命令: create /path data                        │
├──────────────┼────────────────────────────────────────────────┤
│ 持久顺序节点   │ 持久 + 自动编号后缀                            │
│(PERSISTENT_   │ 路径变为 /path0000000001                       │
│ SEQUENTIAL)   │ 命令: create -s /path data                     │
├──────────────┼────────────────────────────────────────────────┤
│ 临时节点       │ 会话结束后自动删除                             │
│ (EPHEMERAL)   │ 不能有子节点                                   │
│               │ 命令: create -e /path data                     │
├──────────────┼────────────────────────────────────────────────┤
│ 临时顺序节点   │ 临时 + 自动编号后缀                            │
│(EPHEMERAL_    │ 会话结束后自动删除                              │
│ SEQUENTIAL)   │ 命令: create -e -s /path data                  │
└──────────────┴────────────────────────────────────────────────┘

节点状态信息(Stat):

复制代码
cZxid = 0x0                     # 创建节点的事务ID
ctime = Thu Jan 01 00:00:00 CST 1970   # 创建时间
mZxid = 0x0                     # 最后一次修改的事务ID
mtime = Thu Jan 01 00:00:00 CST 1970   # 最后修改时间
pZxid = 0x0                     # 子节点最后修改的事务ID
cversion = 0                    # 子节点版本号
dataVersion = 0                 # 数据版本号
aclVersion = 0                  # ACL 版本号
ephemeralOwner = 0x0            # 临时节点的拥有者SessionID
dataLength = 0                  # 数据长度
numChildren = 0                 # 子节点数量

5.5 ZooKeeper 典型应用场景

5.5.1 应用场景一:分布式锁

原理:

利用 ZooKeeper 的临时顺序节点实现分布式锁,多个客户端在同一个节点下创建临时顺序节点,序号最小的获得锁。

java 复制代码
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * 基于 ZooKeeper 的分布式锁实现
 * 利用临时顺序节点(EPHEMERAL_SEQUENTIAL)实现公平锁
 */
public class DistributedLock {

    // ZooKeeper 连接地址
    private static final String CONNECT_ADDR = "192.168.1.100:2181,192.168.1.101:2181,192.168.1.102:2181";
    // 会话超时时间(毫秒)
    private static final int SESSION_TIMEOUT = 30000;
    // 锁的根路径
    private static final String LOCK_ROOT = "/distributed_lock";
    // 锁节点的前缀
    private static final String LOCK_PREFIX = "lock_";

    // ZooKeeper 客户端实例
    private ZooKeeper zk;
    // 当前客户端创建的锁节点路径
    private String currentLockPath;
    // 用于同步等待连接建立
    private CountDownLatch connectLatch = new CountDownLatch(1);
    // 用于同步等待获取锁
    private CountDownLatch lockLatch = new CountDownLatch(1);

    /**
     * 构造方法:初始化 ZooKeeper 连接
     */
    public DistributedLock() throws Exception {
        // 创建 ZooKeeper 客户端连接
        zk = new ZooKeeper(CONNECT_ADDR, SESSION_TIMEOUT, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 连接建立成功事件
                if (event.getState() == Event.KeeperState.SyncConnected) {
                    // 释放连接等待,表示连接已建立
                    connectLatch.countDown();
                }
            }
        });
        // 阻塞等待连接建立完成
        connectLatch.await();

        // 检查锁的根节点是否存在,不存在则创建
        Stat stat = zk.exists(LOCK_ROOT, false);
        if (stat == null) {
            // 创建持久节点作为锁的根节点
            zk.create(LOCK_ROOT, new byte[0],
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.PERSISTENT);
        }
    }

    /**
     * 获取分布式锁
     */
    public void lock() throws Exception {
        // 1. 在锁根节点下创建临时顺序节点
        currentLockPath = zk.create(
                LOCK_ROOT + "/" + LOCK_PREFIX,  // 节点路径(会自动添加序号后缀)
                new byte[0],                      // 节点数据为空
                ZooDefs.Ids.OPEN_ACL_UNSAFE,     // 开放权限
                CreateMode.EPHEMERAL_SEQUENTIAL   // 临时顺序节点类型
        );
        System.out.println("创建锁节点: " + currentLockPath);

        // 2. 获取锁根节点下的所有子节点,并排序
        List<String> children = zk.getChildren(LOCK_ROOT, false);
        // 对子节点进行自然排序(按序号从小到大)
        Collections.sort(children);

        // 3. 判断当前节点是否是最小的节点
        // 获取当前节点名称(去除路径前缀)
        String currentNodeName = currentLockPath.substring(LOCK_ROOT.length() + 1);
        // 获取排序后第一个(最小的)节点
        String smallestNode = children.get(0);

        // 4. 如果当前节点不是最小节点,则监听前一个节点
        if (!currentNodeName.equals(smallestNode)) {
            // 找到当前节点在排序列表中的位置
            int currentIndex = children.indexOf(currentNodeName);
            // 获取前一个节点的路径(即需要等待的节点)
            String prevNodePath = LOCK_ROOT + "/" + children.get(currentIndex - 1);

            // 5. 对前一个节点设置监听(Watcher)
            Stat prevStat = zk.exists(prevNodePath, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    // 当前一个节点被删除时(释放锁),通知当前线程
                    if (event.getType() == Event.EventType.NodeDeleted) {
                        // 唤醒等待的线程
                        lockLatch.countDown();
                    }
                }
            });

            // 如果前一个节点已经不存在(已被删除),直接获取锁
            if (prevStat == null) {
                return;
            }

            // 6. 阻塞等待前一个节点释放锁
            lockLatch.await();
        }
        // 当前节点是最小节点或被唤醒,获取到锁
        System.out.println("获取到锁: " + currentLockPath);
    }

    /**
     * 释放分布式锁
     */
    public void unlock() throws Exception {
        // 删除当前创建的锁节点,释放锁
        zk.delete(currentLockPath, -1);  // -1 表示忽略版本检查
        System.out.println("释放锁: " + currentLockPath);
        // 关闭 ZooKeeper 连接
        zk.close();
    }

    /**
     * 测试分布式锁
     */
    public static void main(String[] args) throws Exception {
        // 创建分布式锁实例
        DistributedLock lock = new DistributedLock();
        // 获取锁
        lock.lock();
        System.out.println("执行业务逻辑...");
        // 模拟业务处理
        Thread.sleep(5000);
        // 释放锁
        lock.unlock();
    }
}

5.5.2 应用场景二:配置管理

java 复制代码
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.concurrent.CountDownLatch;

/**
 * 基于 ZooKeeper 的分布式配置管理
 * 当配置发生变化时,所有客户端可以实时感知
 */
public class ConfigManager {

    // ZooKeeper 连接地址
    private static final String CONNECT_ADDR = "192.168.1.100:2181,192.168.1.101:2181,192.168.1.102:2181";
    // 会话超时时间
    private static final int SESSION_TIMEOUT = 30000;
    // 配置节点路径
    private static final String CONFIG_PATH = "/app/config";

    private ZooKeeper zk;
    private CountDownLatch connectLatch = new CountDownLatch(1);

    /**
     * 初始化 ZooKeeper 连接
     */
    public void init() throws Exception {
        zk = new ZooKeeper(CONNECT_ADDR, SESSION_TIMEOUT, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 连接建立成功
                if (event.getState() == Event.KeeperState.SyncConnected) {
                    connectLatch.countDown();
                }
                // 如果是节点数据变更事件
                if (event.getType() == Event.EventType.NodeDataChanged) {
                    try {
                        // 重新读取配置(同时注册新的 Watcher)
                        readConfig();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        // 等待连接建立
        connectLatch.await();
    }

    /**
     * 读取配置(同时注册 Watcher 监听配置变更)
     */
    public void readConfig() throws Exception {
        // getData 方法的第二个参数 true 表示注册默认 Watcher(即创建连接时指定的 Watcher)
        Stat stat = new Stat();
        byte[] data = zk.getData(CONFIG_PATH, true, stat);
        String config = new String(data, "UTF-8");
        System.out.println("当前配置内容: " + config);
        System.out.println("配置版本号: " + stat.getVersion());
    }

    /**
     * 更新配置
     */
    public void updateConfig(String newConfig) throws Exception {
        // setData 方法更新节点数据
        // 第三个参数 -1 表示忽略版本检查
        zk.setData(CONFIG_PATH, newConfig.getBytes("UTF-8"), -1);
        System.out.println("配置已更新为: " + newConfig);
    }

    /**
     * 创建配置节点
     */
    public void createConfigNode(String initialConfig) throws Exception {
        Stat stat = zk.exists(CONFIG_PATH, false);
        if (stat == null) {
            // 创建持久节点存储配置
            zk.create(CONFIG_PATH, initialConfig.getBytes("UTF-8"),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.PERSISTENT);
            System.out.println("配置节点已创建,初始值: " + initialConfig);
        }
    }
}

5.5.3 应用场景三:服务注册与发现

java 复制代码
import org.apache.zookeeper.*;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * 服务注册与发现
 * 服务提供者:将服务信息注册到 ZooKeeper
 * 服务消费者:从 ZooKeeper 获取可用服务列表,并监听变化
 */
public class ServiceDiscovery {

    private static final String CONNECT_ADDR = "192.168.1.100:2181,192.168.1.101:2181,192.168.1.102:2181";
    private static final int SESSION_TIMEOUT = 30000;
    // 服务注册的根路径
    private static final String SERVICE_ROOT = "/services";
    // 具体服务名称路径
    private String servicePath;

    private ZooKeeper zk;
    private CountDownLatch connectLatch = new CountDownLatch(1);

    /**
     * 初始化连接
     */
    public void init() throws Exception {
        zk = new ZooKeeper(CONNECT_ADDR, SESSION_TIMEOUT, event -> {
            if (event.getState() == Event.KeeperState.SyncConnected) {
                connectLatch.countDown();
            }
            // 监听子节点变化事件(服务上下线)
            if (event.getType() == Event.EventType.NodeChildrenChanged) {
                try {
                    // 重新获取服务列表(并重新注册 Watcher)
                    discoverServices();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        connectLatch.await();
    }

    /**
     * 服务注册(服务提供者调用)
     * @param serviceName 服务名称
     * @param address     服务地址(IP:Port)
     */
    public void register(String serviceName, String address) throws Exception {
        // 确保服务根节点存在
        servicePath = SERVICE_ROOT + "/" + serviceName;
        Stat stat = zk.exists(SERVICE_ROOT, false);
        if (stat == null) {
            // 创建持久的根节点
            zk.create(SERVICE_ROOT, new byte[0],
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.PERSISTENT);
        }
        // 确保具体服务节点存在
        stat = zk.exists(servicePath, false);
        if (stat == null) {
            zk.create(servicePath, new byte[0],
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.PERSISTENT);
        }

        // 创建临时顺序节点,代表一个服务实例
        // 使用临时节点:当服务宕机时,会话结束,节点自动删除
        String instancePath = zk.create(
                servicePath + "/instance_",       // 节点路径前缀
                address.getBytes("UTF-8"),         // 节点数据为服务地址
                ZooDefs.Ids.OPEN_ACL_UNSAFE,       // 开放权限
                CreateMode.EPHEMERAL_SEQUENTIAL    // 临时顺序节点
        );
        System.out.println("服务注册成功: " + instancePath + " -> " + address);
    }

    /**
     * 服务发现(服务消费者调用)
     * 获取当前可用的服务实例列表,并注册 Watcher 监听变化
     */
    public List<String> discoverServices() throws Exception {
        // getChildren 的第二个参数 true 表示注册 Watcher 监听子节点变化
        List<String> instances = zk.getChildren(servicePath, true);
        System.out.println("当前可用服务实例数量: " + instances.size());

        // 遍历每个实例,获取其存储的服务地址
        for (String instance : instances) {
            byte[] data = zk.getData(servicePath + "/" + instance, false, null);
            String address = new String(data, "UTF-8");
            System.out.println("  实例: " + instance + ", 地址: " + address);
        }
        return instances;
    }
}

5.5.4 应用场景四:命名服务

java 复制代码
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.concurrent.CountDownLatch;

/**
 * 命名服务 --- 利用 ZooKeeper 创建全局唯一名称
 * 例如:分布式 ID 生成器
 */
public class NamingService {

    private static final String CONNECT_ADDR = "192.168.1.100:2181";
    private static final int SESSION_TIMEOUT = 30000;
    // 命名空间根路径
    private static final String NAMESPACE_ROOT = "/naming";

    private ZooKeeper zk;
    private CountDownLatch connectLatch = new CountDownLatch(1);

    public void init() throws Exception {
        zk = new ZooKeeper(CONNECT_ADDR, SESSION_TIMEOUT, event -> {
            if (event.getState() == Event.KeeperState.SyncConnected) {
                connectLatch.countDown();
            }
        });
        connectLatch.await();

        // 确保命名空间根节点存在
        if (zk.exists(NAMESPACE_ROOT, false) == null) {
            zk.create(NAMESPACE_ROOT, new byte[0],
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.PERSISTENT);
        }
    }

    /**
     * 注册名称 --- 创建持久节点表示一个命名
     * @param name   名称
     * @param detail 名称对应的数据(如 IP、端口等)
     * @return 是否注册成功
     */
    public boolean registerName(String name, String detail) throws Exception {
        String path = NAMESPACE_ROOT + "/" + name;
        // 检查名称是否已被注册
        if (zk.exists(path, false) != null) {
            System.out.println("名称 [" + name + "] 已被占用!");
            return false;
        }
        // 创建节点,注册名称
        zk.create(path, detail.getBytes("UTF-8"),
                ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT);
        System.out.println("名称 [" + name + "] 注册成功!");
        return true;
    }

    /**
     * 查询名称
     * @param name 名称
     * @return 名称对应的数据
     */
    public String lookupName(String name) throws Exception {
        String path = NAMESPACE_ROOT + "/" + name;
        Stat stat = zk.exists(path, false);
        if (stat == null) {
            System.out.println("名称 [" + name + "] 不存在!");
            return null;
        }
        byte[] data = zk.getData(path, false, stat);
        return new String(data, "UTF-8");
    }

    /**
     * 注销名称
     */
    public boolean unregisterName(String name) throws Exception {
        String path = NAMESPACE_ROOT + "/" + name;
        if (zk.exists(path, false) == null) {
            System.out.println("名称 [" + name + "] 不存在!");
            return false;
        }
        zk.delete(path, -1);
        System.out.println("名称 [" + name + "] 已注销!");
        return true;
    }
}

5.6 ZooKeeper 的 Watcher 机制

5.6.1 知识点详解

ZooKeeper 提供了分布式数据的发布/订阅功能,通过 Watcher 机制来实现。

Watcher 工作流程:

复制代码
┌──────────┐                        ┌──────────┐
│  Client  │                        │ZooKeeper │
│          │  ① 注册 Watcher         │  Server  │
│          │  (getData/getChildren/  │          │
│          │   exists 方法传入)       │          │
│          ├───────────────────────►│          │
│          │                        │          │
│          │  ② 触发 Watcher 事件    │          │
│          │  (NodeCreated/          │          │
│          │   NodeDeleted/          │          │
│          │   NodeDataChanged/      │          │
│          │   NodeChildrenChanged)  │          │
│          │◄───────────────────────│          │
│          │                        │          │
│          │  ③ 回调 process()       │          │
│          │  (业务处理逻辑)          │          │
│          │                        │          │
└──────────┘                        └──────────┘

Watcher 特性:

特性 说明
一次性触发 Watcher 只会被触发一次,触发后需要重新注册
有序性 客户端先看到 Watcher 事件通知,再看到节点数据变化
轻量级 只包含事件类型、路径和通知状态,不包含数据

Watcher 事件类型:

事件类型 触发条件 说明
NodeCreated 节点被创建 通过 exists() 注册
NodeDeleted 节点被删除 通过 exists()getData() 注册
NodeDataChanged 节点数据变更 通过 exists()getData() 注册
NodeChildrenChanged 子节点变化 通过 getChildren() 注册

5.6.2 Watcher 案例代码

java 复制代码
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * ZooKeeper Watcher 机制综合示例
 * 演示三种注册 Watcher 的方式和 Watcher 的各种事件类型
 */
public class WatcherDemo {

    private static final String CONNECT_ADDR = "192.168.1.100:2181,192.168.1.101:2181,192.168.1.102:2181";
    private static final int SESSION_TIMEOUT = 30000;
    private static final String TEST_PATH = "/watcher_test";

    private ZooKeeper zk;
    private CountDownLatch connectLatch = new CountDownLatch(1);

    /**
     * 创建默认 Watcher(用于连接状态监控)
     * 所有未明确指定 Watcher 的操作都会使用这个默认 Watcher
     */
    private Watcher defaultWatcher = new Watcher() {
        @Override
        public void process(WatchedEvent event) {
            // 打印事件类型
            System.out.println("【默认Watcher】事件类型: " + event.getType());
            // 打印事件路径
            System.out.println("【默认Watcher】事件路径: " + event.getPath());
            // 打印连接状态
            System.out.println("【默认Watcher】连接状态: " + event.getState());

            // 处理连接状态变化
            if (event.getState() == Event.KeeperState.SyncConnected) {
                System.out.println("连接已建立");
                connectLatch.countDown();
            }
        }
    };

    /**
     * 初始化 ZooKeeper 连接
     */
    public void init() throws Exception {
        // 创建 ZooKeeper 客户端,传入默认 Watcher
        zk = new ZooKeeper(CONNECT_ADDR, SESSION_TIMEOUT, defaultWatcher);
        // 阻塞等待连接建立
        connectLatch.await();
    }

    /**
     * 演示1:使用 exists() 方法注册 Watcher ------ 监听节点创建和删除
     */
    public void watcherWithExists() throws Exception {
        System.out.println("\n===== exists() 注册 Watcher =====");

        // 使用 exists() 方法注册 Watcher
        // 可以监听:NodeCreated、NodeDeleted、NodeDataChanged
        Stat stat = zk.exists(TEST_PATH, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                System.out.println("【exists Watcher】事件: " + event.getType());

                // 重新注册 Watcher(因为 Watcher 是一次性的)
                try {
                    zk.exists(TEST_PATH, this);  // this 指当前 Watcher 对象
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        if (stat == null) {
            System.out.println("节点不存在,正在创建...");
            // 创建节点,将触发 NodeCreated 事件
            zk.create(TEST_PATH, "initial_data".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.PERSISTENT);
        }

        // 等待 Watcher 触发
        Thread.sleep(2000);

        // 修改数据,将触发 NodeDataChanged 事件
        System.out.println("修改节点数据...");
        zk.setData(TEST_PATH, "updated_data".getBytes(), -1);
        Thread.sleep(2000);

        // 删除节点,将触发 NodeDeleted 事件
        System.out.println("删除节点...");
        zk.delete(TEST_PATH, -1);
        Thread.sleep(2000);
    }

    /**
     * 演示2:使用 getData() 方法注册 Watcher ------ 监听数据变更
     */
    public void watcherWithGetData() throws Exception {
        System.out.println("\n===== getData() 注册 Watcher =====");

        // 确保节点存在
        if (zk.exists(TEST_PATH, false) == null) {
            zk.create(TEST_PATH, "hello".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.PERSISTENT);
        }

        // 使用 getData() 方法注册 Watcher
        // 可以监听:NodeDeleted、NodeDataChanged
        byte[] data = zk.getData(TEST_PATH, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                System.out.println("【getData Watcher】事件: " + event.getType());
                System.out.println("【getData Watcher】路径: " + event.getPath());

                // 重新注册 Watcher 并读取最新数据
                try {
                    byte[] newData = zk.getData(TEST_PATH, this, null);
                    System.out.println("最新数据: " + new String(newData));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, null);

        System.out.println("当前数据: " + new String(data));

        // 修改数据,将触发 NodeDataChanged 事件
        System.out.println("修改数据...");
        zk.setData(TEST_PATH, "world".getBytes(), -1);
        Thread.sleep(2000);
    }

    /**
     * 演示3:使用 getChildren() 注册 Watcher ------ 监听子节点变化
     */
    public void watcherWithGetChildren() throws Exception {
        System.out.println("\n===== getChildren() 注册 Watcher =====");

        // 确保父节点存在
        if (zk.exists(TEST_PATH, false) == null) {
            zk.create(TEST_PATH, "".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.PERSISTENT);
        }

        // 使用 getChildren() 注册 Watcher
        // 可以监听:NodeChildrenChanged(子节点创建或删除)
        List<String> children = zk.getChildren(TEST_PATH, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                System.out.println("【getChildren Watcher】事件: " + event.getType());

                // 重新注册 Watcher
                try {
                    List<String> newChildren = zk.getChildren(TEST_PATH, this);
                    System.out.println("当前子节点: " + newChildren);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        System.out.println("初始子节点: " + children);

        // 创建子节点,将触发 NodeChildrenChanged 事件
        System.out.println("创建子节点...");
        zk.create(TEST_PATH + "/child1", "data1".getBytes(),
                ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT);
        Thread.sleep(2000);

        // 删除子节点
        System.out.println("删除子节点...");
        zk.delete(TEST_PATH + "/child1", -1);
        Thread.sleep(2000);
    }

    public static void main(String[] args) throws Exception {
        WatcherDemo demo = new WatcherDemo();
        demo.init();
        demo.watcherWithExists();
        demo.watcherWithGetData();
        demo.watcherWithGetChildren();
        // 清理
        if (demo.zk.exists(TEST_PATH, false) != null) {
            demo.zk.delete(TEST_PATH, -1);
        }
        demo.zk.close();
    }
}

5.7 ZooKeeper 的选举机制

5.7.1 知识点详解

ZooKeeper 的 Leader 选举是保证集群高可用的核心机制。当集群启动或 Leader 宕机时,需要选举出新的 Leader。

选举相关概念:

概念 说明
SID(Server ID) 服务器的唯一标识,即 myid 文件中的值
ZXID(事务ID) 节点上最大事务 ID,值越大说明数据越新
Epoch 逻辑时钟(投票轮次),每一轮投票递增

选举流程(以集群首次启动为例):

复制代码
假设有5台服务器,SID分别为 1, 2, 3, 4, 5

阶段1:服务器1启动
    - 服务器1 投票给自己 (SID=1, ZXID=0)
    - 投票数未超过半数(3),选举未完成
    - 服务器1 状态: LOOKING

阶段2:服务器2启动
    - 服务器2 投票给自己 (SID=2, ZXID=0)
    - 服务器1 和 服务器2 交换投票
    - 比较 ZXID(相同)→ 比较 SID → 服务器2 > 服务器1
    - 两台服务器都投票给 服务器2
    - 投票数未超过半数(3),选举未完成

阶段3:服务器3启动
    - 服务器3 投票给自己 (SID=3, ZXID=0)
    - 三台服务器交换投票
    - 比较 ZXID(相同)→ 比较 SID → 服务器3 > 服务器2
    - 三台服务器都投票给 服务器3
    - 投票数超过半数(3/5) → 服务器3 成为 Leader
    - 服务器1,2 变为 Follower

阶段4:服务器4、5启动
    - 发现已有 Leader,直接成为 Follower

选举触发条件:

  • 集群初始化启动
  • Leader 服务器宕机
  • Follower 发现 Leader 响应超时
  • 集群中超过半数机器不可用后恢复

5.7.2 选举算法代码示例

java 复制代码
/**
 * ZooKeeper 选举机制模拟
 * 简化版的 FastLeaderElection 算法演示
 */
public class ElectionSimulation {

    // 投票信息
    static class Vote {
        int sid;       // 服务器ID(myid)
        long zxid;     // 最大事务ID
        int epoch;     // 投票轮次

        public Vote(int sid, long zxid, int epoch) {
            this.sid = sid;
            this.zxid = zxid;
            this.epoch = epoch;
        }

        /**
         * 比较两个投票的优先级
         * 优先比较 epoch → zxid → sid
         * @return 如果 this 优先级更高返回 true
         */
        public boolean isHigherPriorityThan(Vote other) {
            // 1. 先比较 epoch(逻辑时钟/投票轮次)
            if (this.epoch != other.epoch) {
                return this.epoch > other.epoch;
            }
            // 2. 再比较 zxid(事务ID越大说明数据越新)
            if (this.zxid != other.zxid) {
                return this.zxid > other.zxid;
            }
            // 3. 最后比较 sid(服务器ID越大优先级越高)
            return this.sid > other.sid;
        }

        @Override
        public String toString() {
            return "Vote{sid=" + sid + ", zxid=" + zxid + ", epoch=" + epoch + "}";
        }
    }

    /**
     * 模拟选举过程
     * @param servers 服务器列表,每个元素为 [sid, zxid]
     */
    public static void simulateElection(int[][] servers) {
        int totalCount = servers.length;       // 服务器总数
        int majority = totalCount / 2 + 1;     // 多数派数量(过半)
        int epoch = 1;                          // 初始投票轮次

        // 每台服务器当前的投票
        Vote[] currentVotes = new Vote[totalCount];

        // 第一轮:每台服务器投票给自己
        System.out.println("=== 第一轮:每台服务器投票给自己 ===");
        for (int i = 0; i < totalCount; i++) {
            int sid = servers[i][0];       // 服务器ID
            long zxid = servers[i][1];    // 事务ID
            currentVotes[i] = new Vote(sid, zxid, epoch);
            System.out.println("  服务器" + sid + " 投票给自己: " + currentVotes[i]);
        }

        // 模拟投票交换和更新
        boolean leaderElected = false;
        Vote leaderVote = null;

        while (!leaderElected) {
            System.out.println("\n=== 投票轮次 " + epoch + " ===");

            // 统计每台服务器获得的票数
            int[] voteCount = new int[totalCount + 1]; // 索引对应sid
            // 记录获得票的详情
            Vote winnerVote = null;

            // 每台服务器比较收到的投票,更新自己的投票
            for (int i = 0; i < totalCount; i++) {
                Vote myVote = currentVotes[i];

                // 比较其他服务器的投票
                for (int j = 0; j < totalCount; j++) {
                    if (i != j) {
                        Vote otherVote = currentVotes[j];
                        // 如果其他投票优先级更高,更新自己的投票
                        if (otherVote.isHigherPriorityThan(myVote)) {
                            currentVotes[i] = new Vote(
                                    otherVote.sid,    // 推举优先级更高的候选人
                                    otherVote.zxid,
                                    epoch
                            );
                            myVote = currentVotes[i];
                            System.out.println("  服务器" + servers[i][0]
                                    + " 更新投票为: " + currentVotes[i]);
                        }
                    }
                }

                // 统计投票
                voteCount[myVote.sid]++;
            }

            // 检查是否有服务器获得了多数派的投票
            for (int sid = 1; sid <= totalCount; sid++) {
                if (voteCount[sid] >= majority) {
                    leaderElected = true;
                    // 找到对应的投票信息
                    for (int i = 0; i < totalCount; i++) {
                        if (currentVotes[i].sid == sid) {
                            leaderVote = currentVotes[i];
                            break;
                        }
                    }
                    System.out.println("\n=== 选举结果 ===");
                    System.out.println("Leader 选举成功! Leader = 服务器" + sid
                            + ", 获得 " + voteCount[sid] + " 票");
                    System.out.println("Leader 投票详情: " + leaderVote);

                    // 打印每台服务器的角色
                    System.out.println("\n=== 集群角色 ===");
                    for (int i = 0; i < totalCount; i++) {
                        String role = (servers[i][0] == sid) ? "LEADER" : "FOLLOWER";
                        System.out.println("  服务器" + servers[i][0] + " -> " + role);
                    }
                    break;
                }
            }
            epoch++;
        }
    }

    /**
     * 主方法:模拟5台服务器的选举
     * 参数格式:{sid, zxid}
     */
    public static void main(String[] args) {
        // 5台服务器: [服务器ID, 最大事务ID]
        // 假设事务ID相同,按SID决定
        int[][] servers = {
                {1, 0},   // 服务器1,事务ID=0
                {2, 0},   // 服务器2,事务ID=0
                {3, 0},   // 服务器3,事务ID=0
                {4, 0},   // 服务器4,事务ID=0
                {5, 0}    // 服务器5,事务ID=0
        };
        simulateElection(servers);

        System.out.println("\n\n================================");
        System.out.println("模拟 Leader 宕机后重新选举");
        System.out.println("================================");

        // 模拟服务器3(Leader)宕机后,剩余4台重新选举
        // 服务器3的zxid=100(数据最新),但已宕机
        int[][] serversAfterCrash = {
                {1, 80},   // 服务器1,事务ID=80
                {2, 90},   // 服务器2,事务ID=90
                // 服务器3已宕机
                {4, 85},   // 服务器4,事务ID=85
                {5, 70}    // 服务器5,事务ID=70
        };
        simulateElection(serversAfterCrash);
    }
}

5.8 部署 ZooKeeper 集群

5.8.1 环境准备

bash 复制代码
# ============================================
# 环境要求
# ============================================
# 1. JDK 1.8+
# 2. ZooKeeper 3.6.x 或 3.7.x
# 3. 至少3台服务器(奇数台,如3、5、7)
# 4. 关闭防火墙或开放 2181、2888、3888 端口

# ============================================
# 端口说明
# ============================================
# 2181 : 客户端连接端口(Client请求端口)
# 2888 : Follower 与 Leader 通信端口(集群内部数据同步)
# 3888 : 选举端口(投票通信端口)

5.8.2 下载和安装

bash 复制代码
# ============================================
# 在所有节点上执行以下操作
# ============================================

# 1. 下载 ZooKeeper 安装包
cd /opt/software
wget https://archive.apache.org/dist/zookeeper/zookeeper-3.6.3/apache-zookeeper-3.6.3-bin.tar.gz

# 2. 解压到安装目录
tar -zxvf apache-zookeeper-3.6.3-bin.tar.gz -C /opt/module/

# 3. 重命名(方便操作)
mv /opt/module/apache-zookeeper-3.6.3-bin /opt/module/zookeeper-3.6.3

# 4. 配置环境变量(在 /etc/profile.d/my_env.sh 中添加)
echo 'export ZOOKEEPER_HOME=/opt/module/zookeeper-3.6.3' >> /etc/profile.d/my_env.sh
echo 'export PATH=$PATH:$ZOOKEEPER_HOME/bin' >> /etc/profile.d/my_env.sh

# 5. 使环境变量生效
source /etc/profile.d/my_env.sh

5.9 基于伪分布式模式部署 ZooKeeper 集群

5.9.1 知识点

伪分布式模式是指在一台物理机器上模拟多个 ZooKeeper 实例,每个实例使用不同的端口。

5.9.2 部署步骤

bash 复制代码
# ============================================
# 伪分布式部署(在一台机器上部署3个ZK实例)
# ============================================

# 1. 创建3个ZK实例的目录结构
mkdir -p /opt/module/zookeeper-pseudo/zk1
mkdir -p /opt/module/zookeeper-pseudo/zk2
mkdir -p /opt/module/zookeeper-pseudo/zk3

# 2. 复制ZooKeeper到每个实例目录
cp -r /opt/module/zookeeper-3.6.3/* /opt/module/zookeeper-pseudo/zk1/
cp -r /opt/module/zookeeper-3.6.3/* /opt/module/zookeeper-pseudo/zk2/
cp -r /opt/module/zookeeper-3.6.3/* /opt/module/zookeeper-pseudo/zk3/

# 3. 创建每个实例的数据目录
mkdir -p /opt/module/zookeeper-pseudo/zk1/data
mkdir -p /opt/module/zookeeper-pseudo/zk2/data
mkdir -p /opt/module/zookeeper-pseudo/zk3/data

# 4. 创建 myid 文件(每个实例的ID不同)
echo "1" > /opt/module/zookeeper-pseudo/zk1/data/myid
echo "2" > /opt/module/zookeeper-pseudo/zk2/data/myid
echo "3" > /opt/module/zookeeper-pseudo/zk3/data/myid

# 5. 配置实例1 ------ zoo.cfg
cat > /opt/module/zookeeper-pseudo/zk1/conf/zoo.cfg << 'EOF'
# 基本时间单元(毫秒),ZK中所有时间都是该值的倍数
tickTime=2000

# Follower 初始连接 Leader 时能容忍的最大心跳数
initLimit=10

# Follower 与 Leader 之间请求和应答能容忍的最大心跳数
syncLimit=5

# 数据目录
dataDir=/opt/module/zookeeper-pseudo/zk1/data

# 客户端连接端口(每个实例不同)
clientPort=2181

# 集群配置(server.编号=IP:集群通信端口:选举端口)
# 注意:伪分布式模式下端口必须不同
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890
EOF

# 6. 配置实例2 ------ zoo.cfg
cat > /opt/module/zookeeper-pseudo/zk2/conf/zoo.cfg << 'EOF'
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/module/zookeeper-pseudo/zk2/data
# 端口改为 2182
clientPort=2182
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890
EOF

# 7. 配置实例3 ------ zoo.cfg
cat > /opt/module/zookeeper-pseudo/zk3/conf/zoo.cfg << 'EOF'
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/module/zookeeper-pseudo/zk3/data
# 端口改为 2183
clientPort=2183
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890
EOF

# 8. 依次启动3个ZK实例
/opt/module/zookeeper-pseudo/zk1/bin/zkServer.sh start
/opt/module/zookeeper-pseudo/zk2/bin/zkServer.sh start
/opt/module/zookeeper-pseudo/zk3/bin/zkServer.sh start

# 9. 查看每个实例的状态
/opt/module/zookeeper-pseudo/zk1/bin/zkServer.sh status
/opt/module/zookeeper-pseudo/zk2/bin/zkServer.sh status
/opt/module/zookeeper-pseudo/zk3/bin/zkServer.sh status

# 预期输出:
# 实例1: Mode: follower 或 leader
# 实例2: Mode: follower 或 leader
# 实例3: Mode: follower 或 leader
# 其中一个为 leader,其余为 follower

# 10. 验证集群 ------ 连接任意实例创建节点,在其他实例查看
/opt/module/zookeeper-pseudo/zk1/bin/zkCli.sh -server 127.0.0.1:2181
# 在客户端中执行:
# create /test "hello"
# get /test
# quit

# 连接另一个实例验证数据一致性
/opt/module/zookeeper-pseudo/zk2/bin/zkCli.sh -server 127.0.0.1:2182
# get /test
# 应该看到数据 "hello"

5.10 基于完全分布式模式部署 ZooKeeper 集群

5.10.1 知识点

完全分布式模式是在多台物理(或虚拟)服务器上分别部署一个 ZooKeeper 实例。

5.10.2 部署步骤

bash 复制代码
# ============================================
# 完全分布式部署(3台服务器)
# 服务器规划:
#   node1: 192.168.1.101  (myid=1)
#   node2: 192.168.1.102  (myid=2)
#   node3: 192.168.1.103  (myid=3)
# ============================================

# ============================================
# 步骤1:在 node1 上配置
# ============================================

# 1.1 编辑配置文件
cat > /opt/module/zookeeper-3.6.3/conf/zoo.cfg << 'EOF'
# 基本时间单元 2 秒
tickTime=2000

# Follower 最多用 20 秒(10 * 2000ms)连接 Leader
initLimit=10

# Follower 最多用 10 秒(5 * 2000ms)与 Leader 同步
syncLimit=5

# 数据目录
dataDir=/opt/module/zookeeper-3.6.3/data

# 日志目录(可选,与数据目录分离可提升性能)
dataLogDir=/opt/module/zookeeper-3.6.3/logs

# 客户端连接端口
clientPort=2181

# 集群配置
server.1=192.168.1.101:2888:3888
server.2=192.168.1.102:2888:3888
server.3=192.168.1.103:2888:3888

# 最大客户端连接数
maxClientCnxns=120

# 自动清理快照和事务日志
# 保留最近3个快照
autopurge.snapRetainCount=3
# 清理间隔 0 表示不自动清理
autopurge.purgeInterval=1
EOF

# 1.2 创建数据目录
mkdir -p /opt/module/zookeeper-3.6.3/data
mkdir -p /opt/module/zookeeper-3.6.3/logs

# 1.3 创建 myid 文件
echo "1" > /opt/module/zookeeper-3.6.3/data/myid

# ============================================
# 步骤2:分发到其他节点
# ============================================

# 2.1 将整个ZooKeeper目录分发到 node2 和 node3
scp -r /opt/module/zookeeper-3.6.3 root@192.168.1.102:/opt/module/
scp -r /opt/module/zookeeper-3.6.3 root@192.168.1.103:/opt/module/

# 2.2 在 node2 上修改 myid
ssh root@192.168.1.102 "echo '2' > /opt/module/zookeeper-3.6.3/data/myid"

# 2.3 在 node3 上修改 myid
ssh root@192.168.1.103 "echo '3' > /opt/module/zookeeper-3.6.3/data/myid"

# ============================================
# 步骤3:配置所有节点的环境变量
# ============================================

# 在每台节点上执行
cat >> /etc/profile.d/my_env.sh << 'EOF'
export ZOOKEEPER_HOME=/opt/module/zookeeper-3.6.3
export PATH=$PATH:$ZOOKEEPER_HOME/bin
EOF
source /etc/profile.d/my_env.sh

# ============================================
# 步骤4:关闭防火墙(或开放端口)
# ============================================

# 方式1:关闭防火墙(三台都执行)
systemctl stop firewalld
systemctl disable firewalld

# 方式2:开放端口(更安全的做法)
firewall-cmd --permanent --add-port=2181/tcp
firewall-cmd --permanent --add-port=2888/tcp
firewall-cmd --permanent --add-port=3888/tcp
firewall-cmd --reload

# ============================================
# 步骤5:启动集群
# ============================================

# 在所有节点上启动 ZooKeeper
# node1:
zkServer.sh start
# node2:
ssh root@192.168.1.102 "source /etc/profile; zkServer.sh start"
# node3:
ssh root@192.168.1.103 "source /etc/profile; zkServer.sh start"

# ============================================
# 步骤6:查看集群状态
# ============================================

# 在每台节点上查看角色
zkServer.sh status

# 预期输出示例:
# node1: Mode: leader
# node2: Mode: follower
# node3: Mode: follower

# ============================================
# 步骤7:编写集群管理脚本
# ============================================

cat > /usr/local/bin/zk.sh << 'SCRIPT'
#!/bin/bash
# ZooKeeper 集群管理脚本
# 用法: zk.sh start|stop|status

# 集群节点列表
SERVERS=("192.168.1.101" "192.168.1.102" "192.168.1.103")
# ZooKeeper 安装目录
ZK_HOME="/opt/module/zookeeper-3.6.3"

case $1 in
    "start")
        echo "============ 启动 ZooKeeper 集群 ============"
        for server in ${SERVERS[@]}; do
            echo "---------- 启动 $server ----------"
            ssh $server "source /etc/profile; $ZK_HOME/bin/zkServer.sh start"
        done
        ;;
    "stop")
        echo "============ 停止 ZooKeeper 集群 ============"
        for server in ${SERVERS[@]}; do
            echo "---------- 停止 $server ----------"
            ssh $server "source /etc/profile; $ZK_HOME/bin/zkServer.sh stop"
        done
        ;;
    "status")
        echo "============ 查看 ZooKeeper 集群状态 ============"
        for server in ${SERVERS[@]}; do
            echo "---------- $server 状态 ----------"
            ssh $server "source /etc/profile; $ZK_HOME/bin/zkServer.sh status"
        done
        ;;
    *)
        echo "用法: zk.sh {start|stop|status}"
        ;;
esac
SCRIPT

chmod +x /usr/local/bin/zk.sh

5.11 ZooKeeper 的 Shell 操作

5.11.1 知识点详解

ZooKeeper 提供了命令行客户端工具 zkCli.sh,用于与 ZooKeeper 集群交互。

连接方式:

bash 复制代码
# 连接本地默认端口(2181)
zkCli.sh

# 连接指定服务器和端口
zkCli.sh -server 192.168.1.101:2181

# 连接集群(可以指定多个服务器,用逗号分隔)
zkCli.sh -server 192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181

5.11.2 Shell 命令大全及示例

bash 复制代码
# ============================================
# 1. 节点操作命令
# ============================================

# ---------- 1.1 查看根节点下的子节点 ----------
ls /
# 输出: [zookeeper, myapp, config]

# ---------- 1.2 查看子节点(递归查看) ----------
# ls -s 或 ls2 已被弃用,使用 ls -R 递归查看
ls -R /
# 输出:
# /
# /zookeeper
# /zookeeper/config
# /zookeeper/quota
# /myapp
# /myapp/nodes

# ---------- 1.3 创建持久节点 ----------
# 语法: create [-s] [-e] [-c] [-t ttl] 路径 [数据] [acl]
# -s: 顺序节点
# -e: 临时节点
# -c: 容器节点(ZK 3.6+)
# -t: TTL节点(ZK 3.6+)

# 创建持久节点
create /myapp "myapp_data"
# 输出: Created /myapp

# 创建持久节点并设置数据
create /myapp/config "db_host=192.168.1.200"
create /myapp/config/port "3306"

# ---------- 1.4 创建临时节点 ----------
create -e /myapp/temp "临时数据"
# 注意: 临时节点在客户端会话结束后自动删除

# ---------- 1.5 创建顺序节点 ----------
create -s /myapp/seq/task_ "任务数据"
# 输出: Created /myapp/seq/task_0000000001
# 节点名自动添加10位递增序号

# ---------- 1.6 创建临时顺序节点 ----------
create -e -s /myapp/lock/lock_ "锁数据"
# 输出: Created /myapp/lock/lock_0000000001

# ---------- 1.7 获取节点数据 ----------
get /myapp
# 输出:
# myapp_data                                # 节点数据
# cZxid = 0x4                              # 创建事务ID
# ctime = Mon Jun 07 10:00:00 CST 2026     # 创建时间
# mZxid = 0x4                              # 最后修改事务ID
# mtime = Mon Jun 07 10:00:00 CST 2026     # 最后修改时间
# pZxid = 0x5                              # 子节点最后修改事务ID
# cversion = 1                             # 子节点版本号
# dataVersion = 0                          # 数据版本号
# aclVersion = 0                           # ACL版本号
# ephemeralOwner = 0x0                     # 临时节点所有者
# dataLength = 10                          # 数据长度
# numChildren = 1                          # 子节点数

# ---------- 1.8 获取节点状态信息 ----------
# 使用 -s 参数可以在 get 的同时返回状态
get -s /myapp

# ---------- 1.9 修改节点数据 ----------
set /myapp "new_data"
# 输出:
# cZxid = 0x4
# ctime = Mon Jun 07 10:00:00 CST 2026
# mZxid = 0x6                              # 修改事务ID已变化
# mtime = Mon Jun 07 10:05:00 CST 2026     # 修改时间已变化
# dataVersion = 1                          # 数据版本号递增

# 带版本号的修改(乐观锁机制)
# 语法: set 路径 数据 版本号
# 只有当前版本号匹配时才修改成功
set /myapp "version_data" 1
# 如果版本号不匹配会报错:
# KeeperErrorCode = BadVersion for /myapp

# ---------- 1.10 删除节点 ----------
# 删除叶子节点(没有子节点的节点)
delete /myapp/config/port

# 强制删除节点及其所有子节点(递归删除)
deleteall /myapp/config
# 注意: deleteall 会删除指定节点及所有子节点,慎用!

# ---------- 1.11 检查节点是否存在 ----------
# 使用 exists 命令检查
stat /myapp
# 存在则返回状态信息,不存在则报错:
# Node does not exist: /nonexistent

# ---------- 1.12 设置配额 ----------
# 设置节点的子节点数量配额
setquota -n 3 /myapp
# 设置节点的数据长度配额(字节)
setquota -b 1024 /myapp

# 查看配额
listquota /myapp
# 输出:
# absolute path is /zookeeper/quota/myapp/zookeeper_limits
# Output quota for /myapp count=3,bytes=-1
# Output stat for /myapp count=1,bytes=10

# 删除配额
delquota -n /myapp
delquota -b /myapp

# ============================================
# 2. 监听命令
# ============================================

# ---------- 2.1 监听节点数据变化 ----------
# get -w 命令设置一次性 Watcher
get -w /myapp
# 当 /myapp 的数据被修改时,客户端会收到通知:
# WatchedEvent state:SyncConnected type:NodeDataChanged path:/myapp

# ---------- 2.2 监听子节点变化 ----------
ls -w /myapp
# 当 /myapp 的子节点发生变化时,客户端会收到通知:
# WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/myapp

# ============================================
# 3. ACL 权限控制命令
# ============================================

# ---------- 3.1 权限模式 ----------
# world: 默认模式,所有用户都可以访问
# auth: 认证用户可以访问
# digest: 用户名:密码 方式认证
# ip: IP地址方式认证
# super: 超级管理员

# ---------- 3.2 权限位 ----------
# CREATE (c) - 创建子节点
# READ   (r) - 读取节点数据和子节点列表
# WRITE  (w) - 修改节点数据
# DELETE (d) - 删除子节点
# ADMIN  (a) - 设置权限

# ---------- 3.3 world 模式(默认) ----------
# 所有人都有全部权限
create /acl_test "data"
getAcl /acl_test
# 输出: 'world,'anyone : cdrwa

# ---------- 3.4 auth 模式 ----------
# 先添加认证用户
addauth digest user1:password1
# 创建节点时设置权限
create /acl_auth "auth_data" auth:user1:password1:cdrwa
# 查看权限
getAcl /acl_auth
# 输出: 'digest,'user1:xRJgjKaSIHMK3mfv8vbLKPixFiQ= : cdrwa

# ---------- 3.5 digest 模式 ----------
# 创建节点时指定用户:密码(密码需要SHA1+Base64编码)
create /acl_digest "digest_data" digest:user1:xxxxxxxx:cdrwa

# ---------- 3.6 ip 模式 ----------
# 只允许特定IP访问
create /acl_ip "ip_data" ip:192.168.1.101:cdrwa

# ---------- 3.7 修改节点权限 ----------
setAcl /acl_test world:anyone:r
# 修改后只有读权限

# ============================================
# 4. 其他常用命令
# ============================================

# 查看当前连接信息
connect 192.168.1.101:2181

# 重新连接
reconnect

# 关闭连接
close

# 历史命令
history

# 重复执行历史命令
redo 3  # 执行第3条历史命令

# 帮助
help

# 退出客户端
quit

5.11.3 Shell 命令速查表

bash 复制代码
# ============================================
# ZooKeeper Shell 命令速查表
# ============================================
# 命令                    说明                    示例
# ----                    ----                    ----
# create                 创建节点                 create /node "data"
# create -s              创建顺序节点             create -s /node "data"
# create -e              创建临时节点             create -e /node "data"
# get                    获取节点数据             get /node
# get -s                 获取数据+状态            get -s /node
# set                    修改节点数据             set /node "new"
# delete                 删除叶子节点             delete /node
# deleteall              递归删除节点             deleteall /node
# ls                     列出子节点               ls /node
# ls -R                  递归列出子节点           ls -R /
# stat                   查看节点状态             stat /node
# getAcl                 查看权限                 getAcl /node
# setAcl                 设置权限                 setAcl /node acl
# addauth               添加认证                 addauth digest user:pwd
# setquota              设置配额                  setquota -n 3 /node
# listquota             查看配额                  listquota /node
# sync                  同步节点数据              sync /node
# quit                  退出                     quit
# help                  帮助                     help

5.12 ZooKeeper 的 Java API 操作

5.12.1 知识点:Maven 依赖配置

xml 复制代码
<!-- pom.xml 中添加 ZooKeeper 依赖 -->
<dependencies>
    <!-- ZooKeeper 客户端核心依赖 -->
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.6.3</version>
    </dependency>

    <!-- 日志依赖(ZooKeeper 需要 SLF4J) -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.30</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</version>
        <version>1.7.30</version>
    </dependency>

    <!-- JUnit 测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

5.13 创建会话

5.13.1 知识点详解

ZooKeeper 客户端通过创建 ZooKeeper 对象来建立与服务端的会话(Session)。

创建方式:

java 复制代码
/**
 * ZooKeeper 会话创建详解
 * ZooKeeper 类提供了 4 种构造方法
 */
public class ZooKeeperSessionExample {

    // ZooKeeper 集群连接地址
    private static final String CONNECT_STRING =
            "192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181";
    // 会话超时时间(毫秒),建议至少 2 倍 tickTime
    private static final int SESSION_TIMEOUT = 30000;

    // 用于阻塞主线程,等待连接建立
    private static CountDownLatch connectedLatch = new CountDownLatch(1);

    /**
     * 方式1:最常用的创建方式
     * 参数说明:
     *   connectString - ZooKeeper 集群地址(多个用逗号分隔)
     *   sessionTimeout - 会话超时时间(毫秒)
     *   watcher - 默认 Watcher,用于监听会话状态变化
     */
    public static void createSessionWay1() throws Exception {
        ZooKeeper zk = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT,
                new Watcher() {
                    @Override
                    public void process(WatchedEvent event) {
                        // 获取事件的连接状态
                        Event.KeeperState state = event.getState();

                        if (state == Event.KeeperState.SyncConnected) {
                            // 连接成功建立
                            System.out.println("会话已建立,SessionID: " + zk.getSessionId());
                            // 释放等待
                            connectedLatch.countDown();
                        } else if (state == Event.KeeperState.Disconnected) {
                            // 连接断开(可能是网络抖动,ZK 会自动重连)
                            System.out.println("连接已断开,正在重连...");
                        } else if (state == Event.KeeperState.Expired) {
                            // 会话过期(超时未收到心跳)
                            System.out.println("会话已过期,需要重新创建连接");
                        } else if (state == Event.KeeperState.Closed) {
                            // 连接已关闭
                            System.out.println("连接已关闭");
                        }
                    }
                });

        // 阻塞等待连接建立完成
        // 注意:ZooKeeper 构造方法是异步的,不会阻塞
        connectedLatch.await();
        System.out.println("连接建立成功!");
    }

    /**
     * 方式2:使用 ZKClientConfig 配置更多参数
     */
    public static void createSessionWay2() throws Exception {
        // 创建 ZK 配置对象
        ZKClientConfig config = new ZKClientConfig();
        // 设置是否在客户端关闭时清除临时节点(默认 true)
        config.setProperty(ZKClientConfig.DISABLE_AUTO_WATCH_RESET, "false");

        ZooKeeper zk = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT,
                event -> {
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        System.out.println("连接成功!");
                        connectedLatch.countDown();
                    }
                },
                config);

        connectedLatch.await();
    }

    /**
     * 方式3:指定 chrootPath(根路径隔离)
     * chroot 类似于 Linux 的 chroot,指定一个命名空间根
     * 之后的所有操作都在该路径下进行
     */
    public static void createSessionWithChroot() throws Exception {
        // 在连接字符串中指定 chroot 路径 "/myapp"
        String connectStringWithChroot = CONNECT_STRING + "/myapp";

        ZooKeeper zk = new ZooKeeper(connectStringWithChroot, SESSION_TIMEOUT,
                event -> {
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        System.out.println("连接成功(chroot=/myapp)!");
                        connectedLatch.countDown();
                    }
                });

        connectedLatch.await();

        // 此时创建节点 "/config" 实际上创建的是 "/myapp/config"
        zk.create("/config", "data".getBytes(),
                ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT);
        // 真实路径: /myapp/config
    }

    /**
     * 获取会话信息
     */
    public static void getSessionInfo(ZooKeeper zk) {
        System.out.println("Session ID: " + zk.getSessionId());
        System.out.println("Session Timeout: " + zk.getSessionTimeout());
        System.out.println("是否连接: " + zk.getState().isConnected());
        System.out.println("客户端状态: " + zk.getState());
    }
}

5.13.2 会话状态生命周期

复制代码
                     创建 ZooKeeper 对象
                            │
                            ▼
                    ┌───────────────┐
                    │  CONNECTING   │  正在连接中...
                    └───────┬───────┘
                            │ 连接成功
                            ▼
                    ┌───────────────┐
            ┌──────►│  CONNECTED    │◄─────────┐
            │       └───────┬───────┘          │
            │               │ 网络断开          │ 重连成功
            │               ▼                  │
            │       ┌───────────────┐          │
            │       │ DISCONNECTED  │──────────┘
            │       └───────┬───────┘
            │               │ 会话超时
            │               ▼
            │       ┌───────────────┐
            │       │   EXPIRED     │ 会话过期,需要重建
            │       └───────┬───────┘
            │               │
            │               ▼
            │       ┌───────────────┐
            └───────│   CLOSED      │ 连接关闭
                    └───────────────┘

5.14 操作 ZooKeeper

5.14.1 CRUD 操作案例代码

java 复制代码
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * ZooKeeper Java API CRUD 操作完整示例
 */
public class ZKOperations {

    // 集群连接地址
    private static final String CONNECT_STRING =
            "192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181";
    // 会话超时时间
    private static final int SESSION_TIMEOUT = 30000;

    private ZooKeeper zk;
    private CountDownLatch connectLatch = new CountDownLatch(1);

    /**
     * 建立 ZooKeeper 连接
     */
    public void connect() throws Exception {
        zk = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, event -> {
            // 连接状态监听
            if (event.getState() == Event.KeeperState.SyncConnected) {
                System.out.println("ZooKeeper 连接成功!");
                connectLatch.countDown();
            }
        });
        // 阻塞等待连接建立
        connectLatch.await();
    }

    /**
     * 关闭 ZooKeeper 连接
     */
    public void close() throws Exception {
        if (zk != null) {
            zk.close();
            System.out.println("ZooKeeper 连接已关闭");
        }
    }

    // =============================================
    // 1. 创建节点
    // =============================================

    /**
     * 创建持久节点
     * @param path 节点路径
     * @param data 节点数据
     * @return 创建的节点路径
     */
    public String createPersistentNode(String path, String data) throws Exception {
        // create() 方法参数说明:
        // 参数1: path - 节点路径(如 "/app/config")
        // 参数2: data - 节点存储的字节数组数据
        // 参数3: acl - 访问控制列表(ACL)
        //         ZooDefs.Ids.OPEN_ACL_UNSAFE 表示开放所有权限
        // 参数4: createMode - 创建模式
        //         CreateMode.PERSISTENT 表示持久节点
        String result = zk.create(
                path,                                       // 节点路径
                data.getBytes("UTF-8"),                     // 节点数据(字节数组)
                ZooDefs.Ids.OPEN_ACL_UNSAFE,               // ACL 权限(开放)
                CreateMode.PERSISTENT                       // 节点类型(持久)
        );
        System.out.println("创建持久节点成功: " + result);
        return result;
    }

    /**
     * 创建临时节点
     * @param path 节点路径
     * @param data 节点数据
     * @return 创建的节点路径
     */
    public String createEphemeralNode(String path, String data) throws Exception {
        // CreateMode.EPHEMERAL 表示临时节点
        // 临时节点在创建它的客户端会话结束时自动删除
        // 临时节点不能有子节点
        String result = zk.create(
                path,                                       // 节点路径
                data.getBytes("UTF-8"),                     // 节点数据
                ZooDefs.Ids.OPEN_ACL_UNSAFE,               // ACL 权限
                CreateMode.EPHEMERAL                        // 节点类型(临时)
        );
        System.out.println("创建临时节点成功: " + result);
        return result;
    }

    /**
     * 创建持久顺序节点
     * @param path 节点路径前缀
     * @param data 节点数据
     * @return 创建的节点实际路径(包含序号后缀)
     */
    public String createPersistentSequentialNode(String path, String data) throws Exception {
        // CreateMode.PERSISTENT_SEQUENTIAL 表示持久顺序节点
        // ZooKeeper 会在路径后面自动追加一个10位的递增数字后缀
        // 例如 path="/task/task_" 实际创建为 "/task/task_0000000001"
        String result = zk.create(
                path,                                       // 节点路径前缀
                data.getBytes("UTF-8"),                     // 节点数据
                ZooDefs.Ids.OPEN_ACL_UNSAFE,               // ACL 权限
                CreateMode.PERSISTENT_SEQUENTIAL            // 节点类型(持久顺序)
        );
        System.out.println("创建持久顺序节点成功: " + result);
        return result;
    }

    /**
     * 创建临时顺序节点
     * @param path 节点路径前缀
     * @param data 节点数据
     * @return 创建的节点实际路径(包含序号后缀)
     */
    public String createEphemeralSequentialNode(String path, String data) throws Exception {
        // CreateMode.EPHEMERAL_SEQUENTIAL 表示临时顺序节点
        // 同时具备临时节点和顺序节点的特性
        // 常用于分布式锁的实现
        String result = zk.create(
                path,                                       // 节点路径前缀
                data.getBytes("UTF-8"),                     // 节点数据
                ZooDefs.Ids.OPEN_ACL_UNSAFE,               // ACL 权限
                CreateMode.EPHEMERAL_SEQUENTIAL             // 节点类型(临时顺序)
        );
        System.out.println("创建临时顺序节点成功: " + result);
        return result;
    }

    /**
     * 异步创建节点
     * 使用回调方式,在后台线程中创建节点,不阻塞当前线程
     */
    public void createNodeAsync(String path, String data) throws Exception {
        // create() 的异步版本
        // 参数说明:
        // 参数1: path - 节点路径
        // 参数2: data - 节点数据
        // 参数3: acl - ACL权限
        // 参数4: createMode - 创建模式
        // 参数5: cb - 回调接口 (String rc, String path, Object ctx, String name)
        //          rc: 返回码(0表示成功)
        //          path: 请求的路径
        //          ctx: 上下文对象(用户自定义数据)
        //          name: 实际创建的节点名称
        // 参数6: ctx - 传递给回调的上下文对象
        zk.create(path, data.getBytes("UTF-8"),
                ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT,
                // 异步回调
                (rc, p, ctx, name) -> {
                    if (rc == 0) {
                        // 创建成功
                        System.out.println("异步创建成功: " + name);
                        System.out.println("上下文: " + ctx);
                    } else {
                        // 创建失败
                        System.out.println("异步创建失败,返回码: " + rc);
                    }
                },
                "这是上下文对象"  // 传递给回调的上下文
        );
        System.out.println("异步创建请求已发送,主线程继续执行...");
    }

    // =============================================
    // 2. 读取操作
    // =============================================

    /**
     * 读取节点数据
     * @param path 节点路径
     * @return 节点数据字符串
     */
    public String getData(String path) throws Exception {
        // getData() 方法参数说明:
        // 参数1: path - 节点路径
        // 参数2: watch - 是否使用默认 Watcher(创建连接时指定的)
        //         true: 注册默认 Watcher
        //         false: 不注册 Watcher
        // 参数3: stat - 用于接收节点状态信息(输出参数)
        // 返回值: 节点数据的字节数组
        Stat stat = new Stat();
        byte[] data = zk.getData(path, false, stat);
        String result = (data != null) ? new String(data, "UTF-8") : null;
        System.out.println("节点数据: " + result);
        System.out.println("节点状态 - 数据版本: " + stat.getVersion()
                + ", 子节点数: " + stat.getNumChildren()
                + ", 数据长度: " + stat.getDataLength());
        return result;
    }

    /**
     * 带 Watcher 的读取
     * @param path    节点路径
     * @param watcher 自定义 Watcher
     * @return 节点数据
     */
    public String getDataWithWatcher(String path, Watcher watcher) throws Exception {
        // 使用自定义 Watcher 替代默认 Watcher
        // 参数2 传入 Watcher 对象
        byte[] data = zk.getData(path, watcher, null);
        return (data != null) ? new String(data, "UTF-8") : null;
    }

    /**
     * 获取子节点列表
     * @param path 父节点路径
     * @return 子节点名称列表
     */
    public List<String> getChildren(String path) throws Exception {
        // getChildren() 方法参数说明:
        // 参数1: path - 父节点路径
        // 参数2: watch - 是否使用默认 Watcher
        // 返回值: 子节点名称列表(不含完整路径)
        List<String> children = zk.getChildren(path, false);
        System.out.println("节点 " + path + " 的子节点: " + children);
        return children;
    }

    /**
     * 带 Watcher 的获取子节点
     */
    public List<String> getChildrenWithWatcher(String path, Watcher watcher) throws Exception {
        List<String> children = zk.getChildren(path, watcher);
        System.out.println("节点 " + path + " 的子节点: " + children);
        return children;
    }

    /**
     * 检查节点是否存在
     * @param path 节点路径
     * @return 节点状态信息,如果节点不存在返回 null
     */
    public Stat checkExists(String path) throws Exception {
        // exists() 方法参数说明:
        // 参数1: path - 节点路径
        // 参数2: watch - 是否注册默认 Watcher
        // 返回值: Stat 对象(节点存在时),null(节点不存在时)
        Stat stat = zk.exists(path, false);
        if (stat != null) {
            System.out.println("节点 " + path + " 存在");
            System.out.println("  创建事务ID: " + stat.getCzxid());
            System.out.println("  创建时间: " + stat.getCtime());
            System.out.println("  修改事务ID: " + stat.getMzxid());
            System.out.println("  数据版本: " + stat.getVersion());
        } else {
            System.out.println("节点 " + path + " 不存在");
        }
        return stat;
    }

    // =============================================
    // 3. 更新操作
    // =============================================

    /**
     * 更新节点数据(指定版本)
     * @param path        节点路径
     * @param newData     新数据
     * @param version     期望的版本号(-1 表示忽略版本检查)
     * @return 更新后的节点状态
     */
    public Stat setData(String path, String newData, int version) throws Exception {
        // setData() 方法参数说明:
        // 参数1: path - 节点路径
        // 参数2: data - 新数据(字节数组)
        // 参数3: version - 期望的版本号
        //         -1: 不检查版本,直接更新
        //         >= 0: 乐观锁机制,只有版本号匹配才更新
        // 返回值: 更新后的节点状态
        Stat stat = zk.setData(path, newData.getBytes("UTF-8"), version);
        System.out.println("更新成功,新版本号: " + stat.getVersion());
        return stat;
    }

    /**
     * 使用乐观锁更新节点数据
     * 先读取当前版本,再基于该版本更新
     */
    public boolean updateWithOptimisticLock(String path, String newData) throws Exception {
        // 1. 先读取当前数据和版本号
        Stat currentStat = new Stat();
        zk.getData(path, false, currentStat);
        int currentVersion = currentStat.getVersion();
        System.out.println("当前版本号: " + currentVersion);

        // 2. 尝试用当前版本号更新
        try {
            zk.setData(path, newData.getBytes("UTF-8"), currentVersion);
            System.out.println("乐观锁更新成功!");
            return true;
        } catch (KeeperException.BadVersionException e) {
            // 版本不匹配,说明在读取和更新之间数据已被其他客户端修改
            System.out.println("乐观锁更新失败:数据已被其他客户端修改");
            return false;
        }
    }

    // =============================================
    // 4. 删除操作
    // =============================================

    /**
     * 删除节点
     * @param path    节点路径
     * @param version 版本号(-1 表示忽略版本检查)
     */
    public void deleteNode(String path, int version) throws Exception {
        // delete() 方法参数说明:
        // 参数1: path - 要删除的节点路径
        // 参数2: version - 期望的版本号
        //         -1: 不检查版本,直接删除
        //         >= 0: 只有版本号匹配才删除
        // 注意: 只能删除叶子节点(没有子节点的节点)
        //       如果节点有子节点,会抛出 KeeperException.NotEmptyException
        zk.delete(path, version);
        System.out.println("节点 " + path + " 已删除");
    }

    /**
     * 递归删除节点及其所有子节点
     * @param path 要删除的节点路径
     */
    public void deleteRecursive(String path) throws Exception {
        // 1. 获取所有子节点
        List<String> children = zk.getChildren(path, false);

        // 2. 递归删除每个子节点
        for (String child : children) {
            deleteRecursive(path + "/" + child);
        }

        // 3. 删除当前节点(此时已没有子节点)
        Stat stat = zk.exists(path, false);
        if (stat != null) {
            zk.delete(path, -1);
            System.out.println("已删除节点: " + path);
        }
    }

    // =============================================
    // 5. ACL 权限操作
    // =============================================

    /**
     * 创建带 ACL 权限的节点
     */
    public String createNodeWithACL(String path, String data) throws Exception {
        // 创建一个需要认证的 ACL 列表
        List<ACL> aclList = ZooDefs.Ids.CREATOR_ALL_ACL;
        // CREATOR_ALL_ACL = [CREATE, READ, WRITE, DELETE, ADMIN] 仅创建者有全部权限

        String result = zk.create(path, data.getBytes("UTF-8"),
                aclList, CreateMode.PERSISTENT);
        System.out.println("创建带 ACL 的节点: " + result);
        return result;
    }

    /**
     * 获取节点的 ACL 权限
     */
    public void getNodeACL(String path) throws Exception {
        // getACL() 方法返回节点的 ACL 列表
        List<ACL> aclList = zk.getACL(path, new Stat());
        for (ACL acl : aclList) {
            System.out.println("权限: " + acl.getPerms()
                    + ", Schema: " + acl.getId().getScheme()
                    + ", ID: " + acl.getId().getId());
        }
    }

    /**
     * 设置节点的 ACL 权限
     */
    public void setNodeACL(String path, List<ACL> aclList) throws Exception {
        // setACL() 方法参数:
        // 参数1: path - 节点路径
        // 参数2: acl - 新的 ACL 列表
        // 参数3: version - ACL 版本号(-1 忽略)
        zk.setACL(path, aclList, -1);
        System.out.println("ACL 权限已更新");
    }

    // =============================================
    // 6. 综合测试
    // =============================================

    public static void main(String[] args) {
        ZKOperations ops = new ZKOperations();
        try {
            // 1. 建立连接
            System.out.println("========== 建立连接 ==========");
            ops.connect();

            // 2. 创建节点
            System.out.println("\n========== 创建节点 ==========");
            // 确保父节点存在
            if (ops.checkExists("/myapp") == null) {
                ops.createPersistentNode("/myapp", "my application");
            }
            // 创建子节点
            ops.createPersistentNode("/myapp/config", "db_host=localhost");
            ops.createPersistentNode("/myapp/config/port", "3306");
            // 创建顺序节点
            ops.createPersistentSequentialNode("/myapp/tasks/task_", "task1_data");
            ops.createPersistentSequentialNode("/myapp/tasks/task_", "task2_data");
            // 创建临时节点
            ops.createEphemeralNode("/myapp/online/node1", "192.168.1.101");

            // 3. 读取节点
            System.out.println("\n========== 读取节点 ==========");
            ops.getData("/myapp/config");
            ops.getChildren("/myapp");
            ops.getChildren("/myapp/config");
            ops.checkExists("/myapp/config/port");

            // 4. 更新节点
            System.out.println("\n========== 更新节点 ==========");
            ops.setData("/myapp/config", "db_host=192.168.1.200", -1);
            ops.getData("/myapp/config");  // 验证更新

            // 5. 使用乐观锁更新
            System.out.println("\n========== 乐观锁更新 ==========");
            ops.updateWithOptimisticLock("/myapp/config", "db_host=10.0.0.1");

            // 6. 删除节点
            System.out.println("\n========== 删除节点 ==========");
            ops.deleteNode("/myapp/config/port", -1);
            ops.checkExists("/myapp/config/port");  // 验证已删除

            // 7. 递归删除
            System.out.println("\n========== 递归删除 ==========");
            ops.deleteRecursive("/myapp");

            // 8. 关闭连接
            System.out.println("\n========== 关闭连接 ==========");
            ops.close();

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

5.14.2 Watcher + API 综合案例

java 复制代码
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * ZooKeeper Java API + Watcher 综合案例
 * 实现配置中心:监听配置变更,自动获取最新配置
 */
public class ZKConfigCenter {

    private static final String CONNECT_STRING =
            "192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181";
    private static final int SESSION_TIMEOUT = 30000;
    private static final String CONFIG_PATH = "/app/database";

    private ZooKeeper zk;
    private CountDownLatch connectLatch = new CountDownLatch(1);

    // 存储当前配置
    private volatile String dbHost;
    private volatile String dbPort;
    private volatile String dbName;

    /**
     * 初始化连接
     */
    public void init() throws Exception {
        zk = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, event -> {
            if (event.getState() == Event.KeeperState.SyncConnected) {
                connectLatch.countDown();
            }
        });
        connectLatch.await();
    }

    /**
     * 加载配置(同时注册 Watcher)
     * 通过 Watcher 实现配置变更的自动感知
     */
    public void loadConfig() throws Exception {
        // 检查配置路径是否存在
        Stat stat = zk.exists(CONFIG_PATH, false);
        if (stat == null) {
            // 创建配置节点
            zk.create("/app", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            zk.create(CONFIG_PATH, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            zk.create(CONFIG_PATH + "/host", "localhost".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            zk.create(CONFIG_PATH + "/port", "3306".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            zk.create(CONFIG_PATH + "/dbname", "mydb".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }

        // 读取配置子节点,并注册 Watcher 监听子节点变化
        loadAllConfigItems();
    }

    /**
     * 加载所有配置项并注册 Watcher
     */
    private void loadAllConfigItems() throws Exception {
        // 获取配置目录下的所有子节点,并注册 Watcher
        List<String> configItems = zk.getChildren(CONFIG_PATH, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 子节点发生变化时触发
                if (event.getType() == Event.EventType.NodeChildrenChanged) {
                    System.out.println("检测到配置子节点变化,重新加载配置...");
                    try {
                        loadAllConfigItems();  // 重新加载并重新注册 Watcher
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        // 为每个配置项注册数据变更 Watcher
        for (String item : configItems) {
            String itemPath = CONFIG_PATH + "/" + item;
            byte[] data = zk.getData(itemPath, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    // 配置数据变更时触发
                    if (event.getType() == Event.EventType.NodeDataChanged) {
                        System.out.println("检测到配置变更: " + event.getPath());
                        try {
                            // 重新读取该配置项(并重新注册 Watcher)
                            reloadSingleConfig(event.getPath());
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }, null);

            String value = new String(data, "UTF-8");
            // 更新本地配置
            updateLocalConfig(item, value);
        }
        System.out.println("当前配置: host=" + dbHost + ", port=" + dbPort + ", dbname=" + dbName);
    }

    /**
     * 重新加载单个配置项
     */
    private void reloadSingleConfig(String path) throws Exception {
        byte[] data = zk.getData(path, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                if (event.getType() == Event.EventType.NodeDataChanged) {
                    try {
                        reloadSingleConfig(event.getPath());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }, null);

        String itemName = path.substring(CONFIG_PATH.length() + 1);
        String value = new String(data, "UTF-8");
        updateLocalConfig(itemName, value);
        System.out.println("配置已更新: " + itemName + " = " + value);
        System.out.println("最新配置: host=" + dbHost + ", port=" + dbPort + ", dbname=" + dbName);
    }

    /**
     * 更新本地配置缓存
     */
    private void updateLocalConfig(String key, String value) {
        switch (key) {
            case "host":
                this.dbHost = value;
                break;
            case "port":
                this.dbPort = value;
                break;
            case "dbname":
                this.dbName = value;
                break;
            default:
                System.out.println("未知配置项: " + key);
        }
    }

    /**
     * 模拟配置变更(用于测试)
     */
    public void updateConfig(String key, String value) throws Exception {
        String path = CONFIG_PATH + "/" + key;
        Stat stat = zk.exists(path, false);
        if (stat != null) {
            // 更新配置
            zk.setData(path, value.getBytes("UTF-8"), -1);
            System.out.println("手动更新配置: " + key + " = " + value);
        } else {
            // 创建新配置项
            zk.create(path, value.getBytes("UTF-8"),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            System.out.println("新增配置项: " + key + " = " + value);
        }
    }

    /**
     * 关闭连接
     */
    public void close() throws Exception {
        zk.close();
    }

    /**
     * 测试入口
     */
    public static void main(String[] args) throws Exception {
        ZKConfigCenter configCenter = new ZKConfigCenter();

        // 1. 初始化连接
        configCenter.init();
        System.out.println("配置中心初始化完成\n");

        // 2. 加载配置
        configCenter.loadConfig();

        // 3. 模拟配置变更
        System.out.println("\n--- 模拟配置变更 ---");
        Thread.sleep(3000);
        configCenter.updateConfig("host", "192.168.1.200");

        Thread.sleep(3000);
        configCenter.updateConfig("port", "5432");

        Thread.sleep(3000);
        configCenter.updateConfig("dbname", "production_db");

        // 等待所有 Watcher 回调完成
        Thread.sleep(5000);

        // 4. 关闭连接
        configCenter.close();
    }
}

5.14.3 Curator 框架操作示例(补充)

xml 复制代码
<!-- pom.xml 中添加 Curator 依赖 -->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>5.2.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.2.0</version>
</dependency>
java 复制代码
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.*;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.data.Stat;
import java.util.List;

/**
 * 使用 Curator 框架操作 ZooKeeper
 * Curator 是 Apache 开源的 ZooKeeper 客户端框架
 * 提供了更高层次的 API,简化了 ZooKeeper 的使用
 */
public class CuratorOperations {

    private static final String CONNECT_STRING =
            "192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181";

    private CuratorFramework client;

    /**
     * 初始化 Curator 客户端
     */
    public void init() {
        // 创建 CuratorFramework 客户端
        client = CuratorFrameworkFactory.builder()
                // 连接字符串
                .connectString(CONNECT_STRING)
                // 会话超时时间
                .sessionTimeoutMs(30000)
                // 连接超时时间
                .connectionTimeoutMs(15000)
                // 重试策略:指数退避重试
                // 参数1: baseSleepTimeMs - 初始休眠时间(毫秒)
                // 参数2: maxRetries - 最大重试次数
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                // 命名空间(所有操作的根路径)
                .namespace("curator-app")
                // 构建客户端
                .build();

        // 启动客户端
        client.start();
        System.out.println("Curator 客户端已启动");
    }

    /**
     * 创建节点
     */
    public void createNode() throws Exception {
        // 1. 创建持久节点(自动创建父节点)
        // creatingParentsIfNeeded() 表示如果父节点不存在则自动创建
        String path1 = client.create()
                .creatingParentsIfNeeded()           // 自动创建父节点
                .withMode(CreateMode.PERSISTENT)      // 持久节点
                .forPath("/app/config", "hello".getBytes());
        System.out.println("创建节点: " + path1);

        // 2. 创建临时节点
        String path2 = client.create()
                .withMode(CreateMode.EPHEMERAL)       // 临时节点
                .forPath("/online/server1", "192.168.1.101".getBytes());
        System.out.println("创建临时节点: " + path2);

        // 3. 创建顺序节点
        String path3 = client.create()
                .creatingParentsIfNeeded()
                .withMode(CreateMode.PERSISTENT_SEQUENTIAL)
                .forPath("/tasks/task-", "task_data".getBytes());
        System.out.println("创建顺序节点: " + path3);

        // 4. 创建空数据节点
        String path4 = client.create()
                .creatingParentsIfNeeded()
                .forPath("/empty_node");
        System.out.println("创建空节点: " + path4);
    }

    /**
     * 读取节点
     */
    public void readNode() throws Exception {
        // 1. 读取节点数据
        Stat stat = new Stat();
        byte[] data = client.getData()
                .storingStatIn(stat)      // 将状态信息存入 stat 变量
                .forPath("/app/config");   // 指定路径
        System.out.println("节点数据: " + new String(data));
        System.out.println("数据版本: " + stat.getVersion());

        // 2. 检查节点是否存在
        Stat checkStat = client.checkExists().forPath("/app/config");
        if (checkStat != null) {
            System.out.println("节点存在,版本: " + checkStat.getVersion());
        }

        // 3. 获取子节点列表
        List<String> children = client.getChildren().forPath("/app");
        System.out.println("子节点: " + children);
    }

    /**
     * 更新节点
     */
    public void updateNode() throws Exception {
        // 1. 更新节点数据
        Stat stat = client.setData()
                .forPath("/app/config", "world".getBytes());
        System.out.println("更新成功,新版本: " + stat.getVersion());

        // 2. 基于版本号更新(乐观锁)
        client.setData()
                .withVersion(stat.getVersion())  // 指定期望的版本号
                .forPath("/app/config", "versioned_data".getBytes());
        System.out.println("基于版本的更新成功");
    }

    /**
     * 删除节点
     */
    public void deleteNode() throws Exception {
        // 1. 删除叶子节点
        client.delete().forPath("/empty_node");

        // 2. 递归删除(删除节点及其所有子节点)
        // deletingChildrenIfNeeded() 表示如果有子节点则一起删除
        client.delete()
                .deletingChildrenIfNeeded()   // 递归删除子节点
                .forPath("/app");
        System.out.println("递归删除完成");

        // 3. 强制保证删除(guaranteed 机制)
        // 如果删除失败,Curator 会在后台持续重试直到成功
        client.delete()
                .guaranteed()                 // 保证删除成功
                .forPath("/tasks");
        System.out.println("保证删除成功");
    }

    /**
     * 使用 Curator 的 TreeCache 监听节点变化
     * TreeCache 是 Curator 提供的高级监听器,可以监听整个子树的变化
     */
    public void watchWithTreeCache() throws Exception {
        // 创建 TreeCache,监听 "/app" 路径下的所有变化
        TreeCache treeCache = new TreeCache(client, "/app");

        // 添加监听器
        treeCache.getListenable().addListener((framework, event) -> {
            // event.getType() 返回事件类型
            switch (event.getType()) {
                case NODE_ADDED:
                    // 节点创建
                    System.out.println("节点创建: " + event.getData().getPath());
                    System.out.println("数据: " + new String(event.getData().getData()));
                    break;
                case NODE_UPDATED:
                    // 节点数据更新
                    System.out.println("节点更新: " + event.getData().getPath());
                    System.out.println("新数据: " + new String(event.getData().getData()));
                    break;
                case NODE_REMOVED:
                    // 节点删除
                    System.out.println("节点删除: " + event.getData().getPath());
                    break;
                case INITIALIZED:
                    // 初始化完成
                    System.out.println("TreeCache 初始化完成");
                    break;
            }
        });

        // 启动 TreeCache
        treeCache.start();
        System.out.println("TreeCache 已启动,开始监听 /app 路径");
    }

    /**
     * 使用 PathChildrenCache 监听子节点变化
     * 只监听指定路径的子节点(不递归)
     */
    public void watchWithPathChildrenCache() throws Exception {
        // 创建 PathChildrenCache
        // 参数3: cacheData - 是否缓存子节点数据
        PathChildrenCache pathCache = new PathChildrenCache(client, "/app", true);

        // 添加监听器
        pathCache.getListenable().addListener((framework, event) -> {
            switch (event.getType()) {
                case CHILD_ADDED:
                    System.out.println("子节点添加: " + event.getData().getPath());
                    break;
                case CHILD_UPDATED:
                    System.out.println("子节点更新: " + event.getData().getPath());
                    break;
                case CHILD_REMOVED:
                    System.out.println("子节点删除: " + event.getData().getPath());
                    break;
                case CONNECTION_SUSPENDED:
                    System.out.println("连接挂起");
                    break;
                case CONNECTION_RECONNECTED:
                    System.out.println("连接恢复");
                    break;
                case CONNECTION_LOST:
                    System.out.println("连接丢失");
                    break;
            }
        });

        // 启动缓存
        pathCache.start();
        System.out.println("PathChildrenCache 已启动");
    }

    /**
     * 使用 NodeCache 监听单个节点变化
     */
    public void watchWithNodeCache() throws Exception {
        // 创建 NodeCache,监听单个节点
        NodeCache nodeCache = new NodeCache(client, "/app/config");

        // 添加监听器
        nodeCache.getListenable().addListener(() -> {
            // 获取最新的节点数据
            byte[] data = nodeCache.getCurrentData().getData();
            System.out.println("节点数据变化: " + new String(data));
        });

        // 启动缓存
        nodeCache.start(true);  // true 表示在启动时就加载节点数据
        System.out.println("NodeCache 已启动");
    }

    /**
     * 关闭客户端
     */
    public void close() {
        if (client != null) {
            client.close();
            System.out.println("Curator 客户端已关闭");
        }
    }

    /**
     * 测试入口
     */
    public static void main(String[] args) {
        CuratorOperations ops = new CuratorOperations();
        try {
            // 初始化
            ops.init();

            // CRUD 操作
            ops.createNode();
            ops.readNode();
            ops.updateNode();
            ops.readNode();  // 验证更新

            // 设置监听
            ops.watchWithNodeCache();

            // 修改数据,触发监听
            Thread.sleep(3000);
            client.setData().forPath("/app/config", "trigger_watch".getBytes());

            Thread.sleep(5000);

            // 清理
            ops.deleteNode();
            ops.close();

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

5.15 本章小结

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                    第5章 知识点总结                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  1. ZooKeeper 是分布式协调服务,解决分布式系统中的协调问题           │
│                                                                     │
│  2. 核心特性:全局一致性、可靠性、顺序性、原子性、实时性             │
│                                                                     │
│  3. 集群角色:Leader(处理写请求)、Follower(处理读+投票)、       │
│              Observer(处理读,不参与投票)                          │
│                                                                     │
│  4. 数据模型:树形结构的命名空间,每个节点称为 ZNode                │
│              ZNode 类型:持久、临时、持久顺序、临时顺序              │
│                                                                     │
│  5. Watcher 机制:一次性的事件监听机制                              │
│              支持 exists/getData/getChildren 三种注册方式            │
│              事件类型:NodeCreated/Deleted/DataChanged/              │
│                       ChildrenChanged                               │
│                                                                     │
│  6. Leader 选举:基于 SID、ZXID、Epoch 的比较算法                   │
│              过半机制保证集群高可用                                   │
│                                                                     │
│  7. 部署方式:                                                      │
│              伪分布式(单机多实例)                                  │
│              完全分布式(多机多实例)                                │
│              关键配置:myid、zoo.cfg                                │
│                                                                     │
│  8. Shell 操作:create/get/set/delete/ls/stat/                      │
│              getAcl/setAcl/addauth 等命令                            │
│                                                                     │
│  9. Java API:                                                       │
│              原生 API(ZooKeeper 类)                                │
│              Curator 框架(高级封装,推荐使用)                      │
│              核心操作:创建会话、CRUD、Watcher、ACL                  │
│                                                                     │
│  10. 典型应用场景:                                                  │
│              分布式锁、配置管理、服务注册发现、命名服务、            │
│              分布式队列、集群管理                                    │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
相关推荐
Solis程序员1 小时前
Kafka 灾难回放机制:基于事件事实流的计数全量恢复方案
分布式·kafka
Elias不吃糖1 小时前
RabbitMQ vs Kafka 简单总结
java·分布式·kafka·rabbitmq
xyz_CDragon2 小时前
把旧电脑变成AI算力:llama.cpp RPC 局域网分布式推理验证与实战
人工智能·分布式·python·rpc·llama
中议视控2 小时前
网络可编程中央控制系统与4K坐席分布式节点的TCP/UDP协议对接技术
网络·分布式·tcp/ip
Lyyaoo.2 小时前
kafka消息的可靠性及幂等性
分布式·kafka
MXsoft6182 小时前
**分布式 vs 集中式:哪个更适合你的跨区域运维?**
运维·分布式
梁辰兴3 小时前
计算机网络基础:具有全分布式结构的 P2P 文件共享程序
网络·分布式·计算机网络·p2p·计算机网络基础·梁辰兴·文件共享程序
闪电悠米13 小时前
黑马点评-Redis 消息队列-03_stream_consumer_group
开发语言·数据库·redis·分布式·缓存·junit·lua
z落落17 小时前
C# 事件(Event)+自定义带参数事件例子
开发语言·分布式·c#