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监听模式:
- 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();
}
}
- 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);
}
}
- 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 分布式节点命名
动态节点命名方案:
- 使用数据库自增ID特性
- 使用ZooKeeper持久顺序节点的顺序特性
ZooKeeper方案流程:
- 启动服务,连接ZooKeeper,检查/创建根节点
- 在根节点下创建临时顺序节点,取回编号作为NodeId
- 根据需要删除临时顺序节点
2.3 分布式ID生成器
方案对比
- Java UUID
- Redis INCR/INCRBY操作
- Twitter SnowFlake算法
- ZooKeeper顺序节点
- 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 设计思路
- 创建持久节点作为队列根节点
- 入队:在根节点下创建临时有序节点
- 出队:获取最小序号节点,读取数据后删除
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的适用性和局限性。