zokkepeer watcher机制
1.简介
Zookeeper采用了Watcher机制实现数据的发布/订阅功能。该机制在被订阅对象发生变化时会异步通知客户端。
可以看作观察者模式在分布式场景下的实现,特征如下。
- 一次性:3.6版本之前所有事件是一次性的,3.6新增持久watcher和持久递归watcher。
- 轻量级:WatchEvent是最小的通信单元,结构上只包含通知状态、事件类型和节点路径。
- 客户端串行触发执行:注意回调函数的处理时间,避免阻塞。
- 时效性:会话有效。
Watcher内部包含了两个枚举类
- KeeperState:Disconnected、SyncConnected、AuthFailed、ConnectedReadOnly、SaslAuthenticated、Expired、Closed
- EventType:None、NodeCreated、NodeDeleted、NodeDataChanged、NodeChildrenChanged、DataWatchRemoved、ChildWatchRemoved、PersistentWatchRemoved
注册使用:可以通过 getData(),getChildren() 和 exists() 三个方法来设置 watcher
2.使用demo
官方demo:https://zookeeper.apache.org/doc/r3.6.0/javaExample.html
3.原理
简单理解就是一个分布式的观察者模式实现。涉及到跨主机通信,所以内部使用nio和netty两种方式进行rpc。
以getData注册watcher为例:
客户端通过rpc向服务端进行watcher注册,成功后在本地的ZKWatchManager保存节点路径到watcher的映射。同理,服务端也会在自己的WatchManager里面保存一份映射。
客户端调用setData后,服务端WatchManager找到对应节点的watcher,通过rcp通知客户端执行watcher的process方法实现回调逻辑。
(1)两个WatchManager
客户端 ZKWatchManager
//ZKWatchManager维护了5个map,key代表数据节点的绝对路径,value代表注册在当前节点上的watcher集合
//节点上内容数据、状态信息变更相关监听
private final Map<String, Set<Watcher>> dataWatches = new HashMap<String, Set<Watcher>>();
//节点变更相关监听
private final Map<String, Set<Watcher>> existWatches = new HashMap<String, Set<Watcher>>();
//节点子列表变更相关监听
private final Map<String, Set<Watcher>> childWatches = new HashMap<String, Set<Watcher>>();
//3.6版本新增了持久watcher和持久递归watcher
private final Map<String, Set<Watcher>> persistentWatches = new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> persistentRecursiveWatches = new HashMap<String, Set<Watcher>>();
服务端 WatchManager
//节点路径对应的所有watcher映射
private final Map<String, Set<Watcher>> watchTable = new HashMap<>();
//watcher对应的所有节点路径的映射
private final Map<Watcher, Set<String>> watch2Paths = new HashMap<>();
(2)rpc通信
nio和netty两种方式
(3)wacher注册和触发流程
图片来自:参考(2)
4常见的问题
(1)watcher是一次性的吗?
3.6版本之前的都是一次性的,3.6新增持久watcher和持久递归watcher。注意其他封装类型的客户端的版本支持(如cursor从5.0开始支持)
一次性是因为WatchManager每次处理完后都会remove掉对应的watcher,这么设计是为了减轻系统负担。
(2)zkClient使用监听器封装了watcher
直接实现IZkChildListener、IZkChildListener、IZkStateListener等接口。而且不需要重复注册(watcher的一次性)
(3)已经注册的watcher会丢吗?什么情况下会丢
有可能。
发生CONNECTIONLOSS之后,只要在session_timeout之内再次连接上(即不发生SESSIONEXPIRED),那么这个连接注册的watches依然在,否则就会丢失。
(4)能否收到每次节点变化的通知
在分布式多客户端情况下节点数据的更新频率很高的话,不能。
因为处理watcher回调到再次注册watcher期间,服务端节点可能变化很多次了。
(5)同一个zk客户端对某一个节点注册相同的watch,只会收到一次通知。
zookeeper使用watcher实现分布式锁
package com.wanna.zk.zkstudy;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.LockSupport;
/**
* 利用zk实现分布式锁思路:使用临时顺序节点
* 1.在执行加锁之前,先判断/LOCKS有没有被创建,如果没有则先创建
* 2.如果创建了/LOCKS,则创建/LOCKS/LOCK_xxxxxxxx这样子的临时顺序节点
* 3.尝试去加锁的逻辑,先获取/LOCKS/下所有的孩子节点,并进行排序
* ---3.1如果当前锁在的位置是0,也就是第一个元素,那么就获取锁成功,开始执行业务代码
* ---3.2如果当前锁所在的位置不是0,那么就获取它的前一个节点,并进行监控
* ------如果前一个节点已经被删了,那么就继续尝试去获取锁
* ------如果前一个节点还没被删,那么就阻塞当前线程,直到监听器(Watcher)对象将其唤醒才继续执行
* 4.解锁逻辑,删除自己创建的LOCK这条记录,因为是临时顺序节点,因此就算机器宕机了也会自动删除锁
*/
public class ZKLock {
private static final String LOCK_ROOT = "/LOCKS"; //锁的根路径
private static final String LOCK_NODE_NAME = "LOCK_"; //锁的名称,使用临时顺序节点
private String lockPath; //完整的锁路径
//集群的连接字符串,机器ip:port中间用逗号分隔即可
String connectString = "localhost:2181,localhost:2182,localhost:2183";
private ZooKeeper zooKeeper; //zk对象
private final Thread currentThread = Thread.currentThread(); //获取创建对象的线程,用来进行唤醒和阻塞
private int zkSessionTimeout = 5000; //sessionTimeout
//判断某个元素是否被删除,如果删除了就将当前线程唤醒
Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
//如果获取到删除节点的事件,那么就唤醒当前线程
if (event.getType() == Event.EventType.NodeDeleted) {
LockSupport.unpark(currentThread); //将当前线程唤醒
}
}
};
public ZKLock() {
try {
zooKeeper = new ZooKeeper(connectString, zkSessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.None) {
//如果链接建立成功,打印相关信息
if (event.getState() == Event.KeeperState.SyncConnected) {
System.out.println("ZK连接建立成功");
LockSupport.unpark(currentThread);
}
}
}
});
LockSupport.park(); //因为是异步去进行连接,因此这里需要等待
} catch (Exception e) {
e.printStackTrace();
}
}
public void lock() {
try {
createLock(); //createLock
attemptLock(); //尝试去获取锁
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
public void createLock() throws KeeperException, InterruptedException {
//判断LOCKS节点是否存在
final Stat exists = zooKeeper.exists(LOCK_ROOT, false);
//如果不存在则创建一个/LOCKS的持久节点
if (exists == null) {
zooKeeper.create(LOCK_ROOT, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//创建临时有序节点,节点为/LOCKS/LOCK_xxxxxxxx
lockPath = zooKeeper.create(LOCK_ROOT.concat("/").concat(LOCK_NODE_NAME), new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("LOCK NODE ".concat(lockPath).concat(" has been created"));
}
//尝试去获取锁
public void attemptLock() throws KeeperException, InterruptedException {
//获取/LOCKS节点的孩子节点
final List<String> children = zooKeeper.getChildren(LOCK_ROOT, false);
//对children节点进行排序
Collections.sort(children);
//获取当前锁节点排序之后的下标,截取掉/LOCKS/之后的内容
final int index = children.indexOf(lockPath.substring(LOCK_ROOT.length() + 1));
//如果获取到的index为0,也就是第一个元素
if (index == 0) {
System.out.println("获取LOCK成功");
return;
}
//如果不是第一个元素,获取上一个节点的路径
final String s = children.get(index - 1);
//监视它的上一个元素的变化情况,传入watcher对象
final Stat exists = zooKeeper.exists(LOCK_ROOT.concat("/").concat(s), watcher);
//继续去获取锁
if (exists != null) { //如果exists不为null,那么就阻塞,不然就尝试去获取锁
LockSupport.park();
}
attemptLock();
}
public void unlock() {
//删除锁的临时有序节点,并且关闭连接对象
try {
zooKeeper.delete(lockPath, -1);
zooKeeper.close();
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}
参考:
(1)https://www.jianshu.com/p/c68b6b241943
(2)https://www.nightfield.com.cn/index.php/archives/189/