最近公司项目中使用了分布式Zookeeper及Dubbo,为了弄清楚这些框架在项目中的使用,在我业余时间中学习了一些Zookeeper的简单用法,分享出来,若有不足之处,望大家给与建议......
一、什么是分布式系统?
我的理解:将原有的系统拆分为多个子系统组成一个庞大的系统,这个庞大系统对用户不透明;可以分为3点介绍:
- 很多台计算机组成一个整体,一个整体一致对外并且处理同一请求;
- 内部的每台计算机都可以相互通信(request,response);
- 客户端到服务器的一次请求到响应结束会经历多台计算机。
二、那什么是Zookeeper呢?Zookeeper在分布式中有什么作用呢?有什么优势?
Zookeeper是一个高可用的分布式数据管理与系统协调框架,主要是用来维护和监控存储数据的状态变化,通过监控这些数据状态的变化,从而达到基于数据的集群管理;可分为点介绍:
1.发布与订阅模型,Zookeeper可以将发布节点上的数据提供给订阅者,供订阅者获取,实现配置信息的管理和更新;
2.负载均衡,在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就需要在这些对等的服务器中选择一个来执行相关的业务逻辑,其中比较典型的是消息中间件中的生产者,消费者负载均衡;Zookeeper通常来做到生产者、消费者的负载均衡;
3.命令服务,Zookeeper中,客户端可根据指定的名字来获取资源或服务地址,提供者信息等;
4.分布式锁,Zookeeper可以通过分布式锁来保证数据的强一致性。(两类:保持独占锁,控制时序锁)
特性:
- 一致性:数据一致性,数据按照顺序分批入库;
- 原子性:事务要么成功要么失败,不会局部化;
- 单一视图:客户端连接集群中的任一ZK节点,数据都是一致的;
- 可靠性:每次对ZK的操作都会保存在服务端;
- 实时性:客户端可以读取到ZK服务端的最新数据。
三、Zookeeper客户端Curator简单API详解:
(1)实例化Zookeeper客户端:
/**
* 实例化Zookeeper客户端Curator实例
*/
public class CuratorOperator {
public CuratorFramework client = null;
public static final String zkServerPath = "192.168.43.190:2181";
/**
* 实例化zk客户端
*/
public CuratorOperator() {
/**
* 同步创建zk示例,原生api是异步的
*
* curator链接zookeeper的策略:ExponentialBackoffRetry
* baseSleepTimeMs:初始sleep的时间
* maxRetries:最大重试次数
* maxSleepMs:最大重试时间
*/
// RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 5);
/**
* curator链接zookeeper的策略:RetryNTimes
* n:重试的次数
* sleepMsBetweenRetries:每次重试间隔的时间
*/
RetryPolicy retryPolicy = new RetryNTimes(3, 5000);
/**
* curator链接zookeeper的策略:RetryOneTime
* sleepMsBetweenRetry:每次重试间隔的时间
*/
// RetryPolicy retryPolicy2 = new RetryOneTime(3000);
client = CuratorFrameworkFactory.builder()
.connectString(zkServerPath)
.sessionTimeoutMs(10000).retryPolicy(retryPolicy)
.namespace("workspace").build(); //在其workspace命令空间中操作ZK中节点
client.start();
}
/**
* @Description: 关闭zk客户端连接
*/
public void closeZKClient() {
if (client != null) {
this.client.close();
}
}
}
(2) 创建节点:
/**
* 创建节点
* @throws Exception
*/
public void createZKNode() throws Exception{
// 实例化
CuratorOperator cto = new CuratorOperator();
boolean isZkCuratorStarted = cto.client.isStarted();
System.out.println("当前客户的状态:" + (isZkCuratorStarted ? "连接中" : "已关闭"));
// 创建节点(creatingParentsIfNeeded()可以递归创建节点)
String nodePath = "/super/directory";
byte[] data = "superme".getBytes();
cto.client.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath(nodePath, data);
}
(3)更新删除节点:
/**
* 更新删除节点
* @throws Exception
*/
public void updateAndDeleteZKNode() throws Exception{
// 实例化
CuratorOperator cto = new CuratorOperator();
boolean isZkCuratorStarted = cto.client.isStarted();
System.out.println("当前客户的状态:" + (isZkCuratorStarted ? "连接中" : "已关闭"));
String nodePath = "/super/directory";
//更新节点数据
byte[] newData = "newData".getBytes();
cto.client.setData().withVersion(1).forPath(nodePath, newData);
// 删除节点
cto.client.delete()
.guaranteed() // 如果删除失败,那么在后端还是继续会删除,直到成功
.deletingChildrenIfNeeded() // 如果有子节点,递归删除(原生API是不支持的)
.withVersion(2)
.forPath(nodePath);
}
(4)读取节点及子节点的内容:
/**
* 获取当前节点及子节点内容
*/
public void getParentAndChildNodeContent() throws Exception{
// 实例化
CuratorOperator cto = new CuratorOperator();
boolean isZkCuratorStarted = cto.client.isStarted();
System.out.println("当前客户的状态:" + (isZkCuratorStarted ? "连接中" : "已关闭"));
String nodePath = "/super/directory";
// 判断节点是否存在,如果不存在则为空
Stat statExist = cto.client.checkExists().forPath(nodePath);
System.out.println(statExist);
// 读取节点数据
Stat stat = new Stat();
//storingStatIn(stat)即为获取节点信息的同时获取节点的状态信息并存储
byte[] data = cto.client.getData().storingStatIn(stat).forPath(nodePath);
System.out.println("节点" + nodePath + "的数据为: " + new String(data));
System.out.println("该节点的版本号为: " + stat.getVersion());
// 查询当前路径下的子节点(而不是递归子节点,只是子节点的第一层)
List<String> childNodes = cto.client.getChildren().forPath(nodePath);
System.out.println("开始打印子节点:");
childNodes.stream().forEach(System.out::println);
}
(5) 只能对其ZK节点修改进行多次监控:
/**
* 只能对其节点的修改进行多次监控
* @throws Exception
*/
public void watchUpdateZKNode() throws Exception{
// 实例化
CuratorOperator cto = new CuratorOperator();
boolean isZkCuratorStarted = cto.client.isStarted();
System.out.println("当前客户的状态:" + (isZkCuratorStarted ? "连接中" : "已关闭"));
String nodePath = "/super/directory";
/**
* 为节点添加watcher
* 当客户端和服务端建立连接的时候,会将服务端zk中当前路径下的节点数据缓存到本地 NodeCache 中
* NodeCache: 创建一个缓存节点,监听数据节点的变更,会触发事件
*/
final NodeCache nodeCache = new NodeCache(cto.client, nodePath);
// buildInitial : 初始化的时候获取该路径下node的值并且缓存到本地
nodeCache.start(true);
if (nodeCache.getCurrentData() != null) {
System.out.println("节点初始化数据为:" + new String(nodeCache.getCurrentData().getData()));
} else {
System.out.println("节点初始化数据为空...");
}
/**
* 这样就可以对其节点改变进行多次监听
*/
nodeCache.getListenable().addListener(() -> {
//先要进行nodeCache节点判空操作,防止该节点已被删除过后进行其他操作,报空指针异常
if (nodeCache.getCurrentData() == null) {
System.out.println("空");
return;
}
String data = new String(nodeCache.getCurrentData().getData());
System.out.println("节点路径:" + nodeCache.getCurrentData().getPath() + "数据:" + data);
}
);
}
(6)对其ZK节点的增删改都可进行多次监控:
public final static String ADD_PATH = "/super/directory/child1";
/**
* 可以多次对其ZK节点增删改进行监控
*/
public void multiWatchZkNodeADUNode() throws Exception{
// 实例化
CuratorOperator cto = new CuratorOperator();
boolean isZkCuratorStarted = cto.client.isStarted();
System.out.println("当前客户的状态:" + (isZkCuratorStarted ? "连接中" : "已关闭"));
String nodePath = "/super/directory";
/**
* 若要监听的节点是修改、删除、添加操作,nodeCacheListener是不支持这种操作的;
* 可以使用PathChildrenCache进行当前节点的父节点进行监听,可达到同样的效果;
* 为子节点添加watcher,PathChildrenCache: 监听数据节点的增删改,会触发事件
*/
String childNodePathCache = nodePath;
// cacheData: 设置缓存节点的数据状态
final PathChildrenCache childrenCache = new PathChildrenCache(cto.client, childNodePathCache, true);
/**
* StartMode: 初始化方式
* POST_INITIALIZED_EVENT:异步初始化,初始化之后会触发事件
* NORMAL:异步初始化,不会触发事件
* BUILD_INITIAL_CACHE:同步初始化,可以直接获取当前节点所有子节点列表数据(异步方式不可以)
*/
childrenCache.start(StartMode.POST_INITIALIZED_EVENT);
/**
* BUILD_INITIAL_CACHE,同步监听才可获取子节点列表中的数据
*/
List<ChildData> childDataList = childrenCache.getCurrentData();
System.out.println("当前数据节点的子节点数据列表:");
for (ChildData cd : childDataList) {
String childData = new String(cd.getData());
System.out.println(childData);
}
/**
* 异步监听当前节点,可转换为监听当前节点的父节点,对其子节点的增删改查操作,可达到同样的效果
*/
childrenCache.getListenable().addListener((child, event) -> {
if (event.getType().equals(PathChildrenCacheEvent.Type.INITIALIZED)) {
System.out.println("子节点初始化ok...");
} else if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_ADDED)) {
String path = event.getData().getPath();
if (path.equals(ADD_PATH)) {
System.out.println("添加子节点:" + event.getData().getPath());
System.out.println("子节点数据:" + new String(event.getData().getData()));
} else if (path.equals("/super/directory/child2")) {
System.out.println("添加不正确...");
}
} else if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)) {
System.out.println("删除子节点:" + event.getData().getPath());
} else if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) {
System.out.println("修改子节点路径:" + event.getData().getPath());
System.out.println("修改子节点数据:" + new String(event.getData().getData()));
}
}
);
}
四、总结:
(1)相比原生Zookeeper的API,Curator客户端可以解决Watcher注册一次就失效的问题;
(2)API简单易用,提供了更多的解决方案且实现简单,并且还支持递归的创建节点;
(3)Zookeeper可以更好的服务于Dubbo框架,ZK负责保存了服务提供方和服务消费方的的URI(dubbo自定义的一种URI),服务消费方找到zookeeper,向zookeeper要到服务提供方的URI,然后就找到提供方,并调用提供方的服务;框架中要完成调度必须要有一个分布式的注册中心,储存所有服务的元数据。