一、zookeeper是什么
集中式存储数据服务,通过监听通知机制来实现来实现分布式应用的协调。
1、目前比较常见应用场景有:
分布式锁(临时节点)
服务注册与订阅(共用节点)
分布式通知(监听znode)
服务命名(znode特性)
数据订阅、发布(watcher)
2、zookeeper数据模型
zookeeper 维护了一个类似文件系统的数据结构,每个子目录(/znode1、/znode1/leaf)都被称作为 znode 即节点。和文件系统一样,我们可以很轻松的对 znode 节点进行增加、删除等操作,而且还可以在一个znode下增加、删除子znode,区别在于文件系统的是,znode可以存储数据(严格说是必须存放数据,默认是个空字符)。
这些节点分为4中类型:
1、持久化节点(zk断开节点还在)
2、持久化顺序编号目录节点
3、临时目录节点(客户端断开后节点就删除了)
4、临时目录编号目录节点
zkClient链接zookeeper,首先执行如下命令,连接到zookeeper server
./zkCli.sh -server ip:2181
./zkCli.sh -server 192.168.25.128:2181
创建持久化节点
create /zk_test
创建临时节点
create -e /zk_test/e_node
创建临时顺序化节点
create -e -s /zk_test/es_node
创建顺序化节点
create -s /zk_test/s_node
退出重新进入,临时节点消失。
1、分布式锁
zookeeper基于watcher机制和znode的有序节点。首先创建一个/zk_test/lock父节点作为一把锁,尽量是持久节点(PERSISTENT类型),每个尝试获取这把锁的客户端,在/zk_test/lock父节点下创建临时顺序子节点。
由于序号的递增性,我们规定序号最小的节点即获得锁。例如:客户端来获取锁,在/zk_test/lock节点下创建节点为/zk_test/lock/seq-00000001,它是最小的所以它优先拿到了锁,其它节点等待通知再次获取锁。/test/lock/seq-00000001执行完自己的逻辑后删除节点释放锁。
那么节点/test/lock/seq-00000002想要获取锁等谁的通知呢?
这里我们让/test/lock/seq-00000002节点监听/test/lock/seq-00000001节点,一旦/test/lock/seq-00000001节点删除,则通知/test/lock/seq-00000002节点,让它再次判断自己是不是最小的节点,是则拿到锁,不是继续等通知。
以此类推/test/lock/seq-00000003节点监听/test/lock/seq-00000002节点,总是让后一个节点监听前一个节点,不用让所有节点都监听最小的节点,避免设置不必要的监听,以免造成大量无效的通知,形成“羊群效应”。
zookeeper分布式锁和redis分布式锁相比,因为大量的创建、删除节点性能上比较差,并不是很推荐。
public class Zk implements Lock,Runnable {
private static final String IP_PORT = "192.168.25.128:2181";
private static final String Z_NODE = "/lock1";
// private volatile String beforePath;
private static ThreadLocal<String> beforePathLocal = new ThreadLocal<>();
private volatile String path;
private ZkClient zkClient = new ZkClient(IP_PORT);
public Zk() {
if (!zkClient.exists(Z_NODE)) {
zkClient.createPersistent(Z_NODE);
}
}
@Override
public void lock() {
if (tryLock()) {
System.out.println("获得锁");
} else {
// 尝试加锁
// 进入等待 监听
waitForLock();
// 再次尝试
lock();
}
}
@Override
public synchronized boolean tryLock() {
// 第一次就进来创建自己的临时节点,(如果已经有其他节点创建,下面获取节点更大)
if (StringUtils.isBlank(path)) {
path = zkClient.createEphemeralSequential(Z_NODE + "/", "lock");
}
// 对节点排序
List<String> children = zkClient.getChildren(Z_NODE);
//节点排序
Collections.sort(children);
// 当前的是最小节点说明当前节点,
if (path.equals(Z_NODE + "/" + children.get(0))) {
System.out.println("当前路径是创建最早,锁这个节点");
return true;
} else {
// 不是最小节点 就找到自己的前一个 ,用于监听上一个节点,而不是最小节点,防止羊群效应。
// 依次类推 释放也是一样
int i = Collections.binarySearch(children, path.substring(Z_NODE.length() + 1));
//获取当前节点上一个节点地址
String beforePath = Z_NODE + "/" + children.get(i - 1);
beforePathLocal.set(beforePath);
}
return false;
}
@Override
public void unlock() {
//防止内存泄漏
beforePathLocal.remove();
zkClient.delete(path);
}
public void waitForLock() {
CountDownLatch cdl = new CountDownLatch(1);
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
System.out.println(Thread.currentThread().getName() + ":监听到节点被删(由于这里监听是该路径的上一个节点,所以是上一个节点被删)");
cdl.countDown();
}
};
// 监听
String beforePath = beforePathLocal.get();
this.zkClient.subscribeDataChanges(beforePath, listener);
if (zkClient.exists(beforePath)) {
try {
System.out.println("加锁失败 等待");
//一直等到 该节点监听上一个节点被删
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 释放监听
zkClient.unsubscribeDataChanges(beforePath, listener);
}
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
public void lockInterruptibly() throws InterruptedException {
}
public Condition newCondition() {
return null;
}
static volatile int inventory = 10;
@Override
public void run() {
Zk zk = new Zk();
try {
zk.lock();
if (inventory > 0) {
inventory--;
}
System.out.println(inventory);
return;
} finally {
zk.unlock();
System.out.println("释放锁");
}
}
private static final int NUM = 5;
public static void main(String[] args) {
try {
for (int i = 1; i <= NUM; i++) {
new Thread(new Zk()).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
2、配置管理
现在有很多开源项目都在使用Zookeeper来维护配置,像消息队列Kafka中,就使用Zookeeper来维护broker的信息;dubbo中管理服务的配置信息。原理也是基于watcher机制,例如:创建一个/config节点存放一些配置,客户端监听这个节点,一点修改/config节点的配置信息,通知各个客户端数据变更重新拉取配置信息。
可以看到dubbo在注册provider信息,任然是创建节点
AbstractZookeeperClient
public void create(String path, boolean ephemeral) {
if (!ephemeral) {
if (this.persistentExistNodePath.contains(path)) {
return;
}
if (this.checkExists(path)) {
this.persistentExistNodePath.add(path);
return;
}
}
int i = path.lastIndexOf(47);
if (i > 0) {
this.create(path.substring(0, i), false);
}
if (ephemeral) {
this.createEphemeral(path);
} else {
this.createPersistent(path);
this.persistentExistNodePath.add(path);
}
}