ZooKeeper Java客户端与分布式应用实战

1. ZooKeeper Java客户端实战

ZooKeeper应用开发主要通过Java客户端API连接和操作ZooKeeper集群,有官方和第三方两种客户端选择。

1.1 ZooKeeper原生Java客户端

依赖引入
xml 复制代码
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.8.0</version>
</dependency>

注意:客户端版本需与服务端保持一致,避免兼容性问题

基本使用
java 复制代码
public class ZkClientDemo {
    private static final String CLUSTER_CONNECT_STR = "192.168.22.156:2181,192.168.22.190:2181,192.168.22.200:2181";

    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        
        ZooKeeper zooKeeper = new ZooKeeper(CLUSTER_CONNECT_STR, 4000, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                if (Event.KeeperState.SyncConnected == event.getState() && 
                    event.getType() == Event.EventType.None) {
                    countDownLatch.countDown();
                    System.out.println("连接建立");
                }
            }
        });
        
        countDownLatch.await();
        System.out.println(zooKeeper.getState()); // CONNECTED
        
        // 创建持久节点
        zooKeeper.create("/user", "fox".getBytes(), 
                        ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    }
}
原生API的局限性
  • Watcher监测为一次性,需重复注册
  • 无自动重连机制
  • 异常处理复杂
  • 仅提供byte[]接口,缺少POJO序列化支持
  • 需手动检查节点存在性
  • 不支持级联删除
常用方法
  • create(path, data, acl, createMode):创建节点
  • delete(path, version):删除节点
  • exists(path, watch):判断节点存在性
  • getData(path, watch):获取节点数据
  • setData(path, data, version):设置节点数据
  • getChildren(path, watch):获取子节点列表
  • sync(path):同步客户端与leader节点

所有方法都提供同步和异步两个版本,且支持条件更新(通过version参数控制)。

同步创建节点
java 复制代码
@Test
public void createTest() throws KeeperException, InterruptedException {
    String path = zooKeeper.create(ZK_NODE, "data".getBytes(), 
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    log.info("created path: {}", path);
}
异步创建节点
java 复制代码
@Test
public void createAsyncTest() throws InterruptedException {
    zooKeeper.create(ZK_NODE, "data".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
            CreateMode.PERSISTENT,
            (rc, path, ctx, name) -> log.info("rc {}, path {}, ctx {}, name {}", rc, path, ctx, name),
            "context");
}
修改节点数据
java 复制代码
@Test
public void setTest() throws KeeperException, InterruptedException {
    Stat stat = new Stat();
    byte[] data = zooKeeper.getData(ZK_NODE, false, stat);
    log.info("修改前: {}", new String(data));
    
    zooKeeper.setData(ZK_NODE, "changed!".getBytes(), stat.getVersion());
    
    byte[] dataAfter = zooKeeper.getData(ZK_NODE, false, stat);
    log.info("修改后: {}", new String(dataAfter));
}

1.2 Curator开源客户端(常用)

依赖引入
xml 复制代码
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.8.0</version>
</dependency>

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.1.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
客户端创建
java 复制代码
// 方式一:使用newClient方法
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient(zookeeperConnectionString, retryPolicy);
client.start();

// 方式二:使用builder模式(推荐)
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
        .connectString("192.168.128.129:2181")
        .sessionTimeoutMs(5000)
        .connectionTimeoutMs(5000)
        .retryPolicy(retryPolicy)
        .namespace("base") // 命名空间隔离
        .build();
client.start();
重试策略类型
  • ExponentialBackoffRetry:重试间隔按指数增长
  • RetryNTimes:最大重试次数
  • RetryOneTime:只重试一次
  • RetryUntilElapsed:在指定时间内重试
基本操作
java 复制代码
// 创建节点
@Test
public void testCreate() throws Exception {
    String path = curatorFramework.create().forPath("/curator-node");
    curatorFramework.create().withMode(CreateMode.PERSISTENT)
            .forPath("/curator-node", "some-data".getBytes());
    log.info("curator create node :{} successfully.", path);
}

// 创建层级节点
@Test
public void testCreateWithParent() throws Exception {
    String pathWithParent = "/node-parent/sub-node-1";
    String path = curatorFramework.create().creatingParentsIfNeeded().forPath(pathWithParent);
    log.info("curator create node :{} successfully.", path);
}

// 获取数据
@Test
public void testGetData() throws Exception {
    byte[] bytes = curatorFramework.getData().forPath("/curator-node");
    log.info("get data from node :{} successfully.", new String(bytes));
}

// 更新数据
@Test
public void testSetData() throws Exception {
    curatorFramework.setData().forPath("/curator-node", "changed!".getBytes());
    byte[] bytes = curatorFramework.getData().forPath("/curator-node");
    log.info("get data from node /curator-node :{} successfully.", new String(bytes));
}

// 删除节点
@Test
public void testDelete() throws Exception {
    String pathWithParent = "/node-parent";
    curatorFramework.delete().guaranteed().deletingChildrenIfNeeded().forPath(pathWithParent);
}
异步接口
java 复制代码
@Test
public void testAsync() throws Exception {
    // 默认在EventThread中执行
    curatorFramework.getData().inBackground((item1, item2) -> {
        log.info("background: {}", item2);
    }).forPath(ZK_NODE);

    // 指定自定义线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    curatorFramework.getData().inBackground((item1, item2) -> {
        log.info("background: {}", item2);
    }, executorService).forPath(ZK_NODE);
}
监听器机制

Curator提供了三种Cache监听模式:

  1. NodeCache - 监听单个节点
java 复制代码
public class NodeCacheTest {
    public static final String NODE_CACHE = "/node-cache";

    @Test
    public void testNodeCacheTest() throws Exception {
        createIfNeed(NODE_CACHE);
        NodeCache nodeCache = new NodeCache(curatorFramework, NODE_CACHE);
        nodeCache.getListenable().addListener(() -> {
            log.info("{} path nodeChanged: ", NODE_CACHE);
            printNodeData();
        });
        nodeCache.start();
    }
}
  1. PathChildrenCache - 监听子节点(不包含二级子节点)
java 复制代码
public class PathCacheTest {
    public static final String PATH = "/path-cache";

    @Test
    public void testPathCache() throws Exception {
        createIfNeed(PATH);
        PathChildrenCache pathChildrenCache = new PathChildrenCache(curatorFramework, PATH, true);
        pathChildrenCache.getListenable().addListener((client, event) -> {
            log.info("event: {}", event);
        });
        pathChildrenCache.start(true);
    }
}
  1. TreeCache - 监听当前节点及所有递归子节点
java 复制代码
public class TreeCacheTest {
    public static final String TREE_CACHE = "/tree-path";

    @Test
    public void testTreeCache() throws Exception {
        createIfNeed(TREE_CACHE);
        TreeCache treeCache = new TreeCache(curatorFramework, TREE_CACHE);
        treeCache.getListenable().addListener((client, event) -> {
            log.info("tree cache: {}", event);
        });
        treeCache.start();
    }
}

2. ZooKeeper在分布式命名服务中的实战

2.1 分布式API目录

Dubbo框架使用ZooKeeper实现分布式JNDI功能:

  • 服务提供者在启动时向/dubbo/${serviceName}/providers节点写入API地址
  • 服务消费者订阅该节点下的URL地址,获取所有服务提供者的API

2.2 分布式节点命名

动态节点命名方案:

  1. 使用数据库自增ID特性
  2. 使用ZooKeeper持久顺序节点的顺序特性

ZooKeeper方案流程:

  • 启动服务,连接ZooKeeper,检查/创建根节点
  • 在根节点下创建临时顺序节点,取回编号作为NodeId
  • 根据需要删除临时顺序节点

2.3 分布式ID生成器

方案对比
  1. Java UUID
  2. Redis INCR/INCRBY操作
  3. Twitter SnowFlake算法
  4. ZooKeeper顺序节点
  5. MongoDB ObjectId
基于ZooKeeper的实现
java 复制代码
public class IDMaker extends CuratorBaseOperations {
    private String createSeqNode(String pathPefix) throws Exception {
        CuratorFramework curatorFramework = getCuratorFramework();
        String destPath = curatorFramework.create()
                .creatingParentsIfNeeded()
                .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                .forPath(pathPefix);
        return destPath;
    }

    public String makeId(String path) throws Exception {
        String str = createSeqNode(path);
        if (null != str) {
            int index = str.lastIndexOf(path);
            if (index >= 0) {
                index += path.length();
                return index <= str.length() ? str.substring(index) : "";
            }
        }
        return str;
    }
}
基于SnowFlake算法的实现
java 复制代码
public class SnowflakeIdGenerator {
    private static final long START_TIME = 1483200000000L;
    private static final int WORKER_ID_BITS = 13;
    private static final int SEQUENCE_BITS = 10;
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
    private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private static final long TIMESTAMP_LEFT_SHIFT = WORKER_ID_BITS + SEQUENCE_BITS;

    private long workerId;
    private long lastTimestamp = -1L;
    private long sequence = 0L;

    public synchronized void init(long workerId) {
        if (workerId > MAX_WORKER_ID) {
            throw new IllegalArgumentException("worker Id wrong: " + workerId);
        }
        this.workerId = workerId;
    }

    private synchronized long generateId() {
        long current = System.currentTimeMillis();

        if (current < lastTimestamp) {
            return -1; // 时钟回拨
        }

        if (current == lastTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == MAX_SEQUENCE) {
                current = this.nextMs(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = current;
        long time = (current - START_TIME) << TIMESTAMP_LEFT_SHIFT;
        long workerId = this.workerId << WORKER_ID_SHIFT;

        return time | workerId | sequence;
    }
}

3. ZooKeeper实现分布式队列

3.1 设计思路

  1. 创建持久节点作为队列根节点
  2. 入队:在根节点下创建临时有序节点
  3. 出队:获取最小序号节点,读取数据后删除

3.2 Curator实现

java 复制代码
public class CuratorDistributedQueueDemo {
    private static final String QUEUE_ROOT = "/curator_distributed_queue";

    public static void main(String[] args) throws Exception {
        CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181",
                new ExponentialBackoffRetry(1000, 3));
        client.start();

        // 序列化器
        QueueSerializer<String> serializer = new QueueSerializer<String>() {
            @Override
            public byte[] serialize(String item) {
                return item.getBytes();
            }

            @Override
            public String deserialize(byte[] bytes) {
                return new String(bytes);
            }
        };

        // 消费者
        QueueConsumer<String> consumer = new QueueConsumer<String>() {
            @Override
            public void consumeMessage(String message) throws Exception {
                System.out.println("消费消息: " + message);
            }

            @Override
            public void stateChanged(CuratorFramework curatorFramework, ConnectionState connectionState) {
            }
        };

        // 创建队列(可指定锁路径保证原子性)
        DistributedQueue<String> queue = QueueBuilder.builder(client, consumer, serializer, QUEUE_ROOT)
                .lockPath("/orderlock") // 可选:分布式锁路径
                .buildQueue();
        queue.start();

        // 生产消息
        for (int i = 0; i < 5; i++) {
            String message = "Task-" + i;
            System.out.println("生产消息: " + message);
            queue.put(message);
            Thread.sleep(1000);
        }

        Thread.sleep(10000);
        queue.close();
        client.close();
    }
}

3.3 注意事项

  • ZooKeeper不适合大数据量存储,官方不推荐作为队列使用
  • 在吞吐量不高的小型系统中较为适用
  • 使用锁路径(lockPath)可保证操作的原子性和顺序性
  • 不指定锁路径可提高性能,但可能面临并发问题

总结

ZooKeeper提供了强大的分布式协调能力,通过原生API或Curator客户端可以实现多种分布式场景下的解决方案。在选择方案时需要根据具体需求权衡性能、一致性和复杂性,特别是在高并发场景下需要考虑ZooKeeper的适用性和局限性。

相关推荐
死也不注释2 小时前
【Unity UGUI 交互组件——Dropdown(TMP版本)(10)】
java·unity·交互
狼爷2 小时前
凌晨 4 点的线上 CPU 告警:一场历时 4 小时的故障排查与架构优化全记录
java
渣哥2 小时前
Java 线程池中的 submit 和 execute 有何不同
java
电商API_180079052472 小时前
淘宝商品视频批量自动化获取的常见渠道分享
java·爬虫·自动化·网络爬虫·音视频
IT乐手2 小时前
java 里 Consumer 和 Supplier 用法
java
崎岖Qiu3 小时前
leetcode380:RandomizedSet - O(1)时间插入删除和获取随机元素(数组+哈希表的巧妙结合)
java·数据结构·算法·leetcode·力扣·散列表
快乐肚皮3 小时前
Redis消息队列演进史
java·redis
AppleWebCoder3 小时前
Java大厂面试实录:AIGC与虚拟互动场景下的微服务与AI落地(附知识详解)
java·spring boot·微服务·ai·消息队列·aigc·虚拟互动
ybq195133454313 小时前
javaEE-Spring IOC&DI
java·spring·java-ee