分布式环境下,多个服务节点需要相互同步配置,一个服务配置修改完成,需要其他服务对修改的配置可见,并及时的同步修改的数据;
zookeeper 就是提供一系列原语和功能,基于这些原语和功能 我们可以实现分布式配置注册 ;
原语:get ,set ,create,delete
功能:watch ,客户端的回调。。
1. ZKTest
主类
测试启动类
package com.msb.zookeeper.configuration;
import org.apache.zookeeper.ZooKeeper;
import java.util.concurrent.CountDownLatch;
/**
* 测试类
*/
public class ZKTest {
/**
* path
*/
public static String CHILD_WORK_NODE = "/name";
/**
* 分布式配置的配置数据 : path 的 data 数据 ;
*/
public static String config ;
/**
* 获取 ZooKeeper 连接
*/
public static ZooKeeper zkClient = ZKUtils.zkClient;
/**
* 用于阻塞
*/
public static CountDownLatch cdl = new CountDownLatch(1);
/**
* 同步配置
*/
public static void syncConf() {
//存在吗
zkClient.exists(CHILD_WORK_NODE, ZNodeWatcher.watchInstance, ZNodeStatCallBack.statCB, "syncConf aWait");
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//............程序获取分布式配置 并设置回调同步......
syncConf();
//模拟其他线程 获取配置 干事情
new Thread(()->{
while (true) {
System.out.println(Thread.currentThread().getName()+" ::"+config);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
2.ZKUtils
获取连接类
package com.msb.zookeeper.configuration;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.util.concurrent.CountDownLatch;
/**
* @Author sff
* @Description 连接工具 做zk连接 释放连接等
* @Date 19:40 2022/4/17
**/
public class ZKUtils {
/**
* 设置连接和
*/
public static String CONNECTIONURL = "192.168.141.131:2181,192.168.141.132:2181,192.168.141.133:2181,192.168.141.134:2181/iconfig" ;
/**
* 连接的过期时间
*/
public static Integer SESSION_TIMEOUT = 4000;
/**
* 外部使用
**/
public static ZooKeeper zkClient = getConnection();
/**
* 初始化 获取连接
* @return
*/
public static ZooKeeper getConnection() {
ZooKeeper zkClient = null;
try {
CountDownLatch downLatch = new CountDownLatch(1);
zkClient = new ZooKeeper( CONNECTIONURL, SESSION_TIMEOUT, new Watcher() {
@Override
public void process(WatchedEvent event) {
Event.KeeperState state = event.getState();
switch (state) {
case Disconnected:
break;
case SyncConnected:
downLatch.countDown();
break;
case AuthFailed:
break;
case Expired:
break;
}
}
});
downLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
return zkClient;
}
}
3.ZNodeWatcher
节点的watch
package com.msb.zookeeper.configuration;
import lombok.SneakyThrows;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.util.concurrent.CountDownLatch;
import static com.msb.zookeeper.configuration.ZKTest.CHILD_WORK_NODE;
public class ZNodeWatcher implements Watcher {
public static final ZNodeWatcher watchInstance = new ZNodeWatcher();
public static final ZooKeeper zkClient = ZKUtils.zkClient;
@SneakyThrows
@Override
public void process(WatchedEvent event) {
switch (event.getType()) {
case None:
break;
case NodeCreated:
System.out.println("节点被创建 注册 watch 和数据改变回调");
//节点被创建 注册 watch 和数据改变回调
zkClient.getData(CHILD_WORK_NODE, watchInstance,
ZNodeDataCallBack.dataCB, "NodeCreated");
break;
case NodeDeleted:
//节点被删除 继续阻塞
ZKTest.config = null;
ZKTest.cdl = new CountDownLatch(1);
System.out.println("NodeDeleted :" + Thread.currentThread().getName());
//为什么不能放这里 如果放在这里 就是 这个是异步的太快了节点,或者的是板顶在后面
new Thread(() -> {
System.out.println("NEW NodeDeleted :" + Thread.currentThread().getPriority());
ZKTest.syncConf();
}).start();
break;
case NodeDataChanged:
//节点被创建 注册 watch 和 数据改变回调
zkClient.getData(CHILD_WORK_NODE, watchInstance,
ZNodeDataCallBack.dataCB, "NodeDataChanged");
break;
case NodeChildrenChanged:
break;
}
}
}
4.ZNodeStatCallBack
节点状态改变回调
/**
* 节点数据改变的 节点存在 添加watch 并 数据改变回调
*/
public class ZNodeStatCallBack implements AsyncCallback.StatCallback {
public static ZNodeStatCallBack statCB = new ZNodeStatCallBack();
@Override
public void processResult(int rc, String path, Object ctx, Stat stat) {
//节点存在 添加watch 和 节点 数据改变回调
//节点不存在 什么都不做
if (stat != null) {
ZKUtils.zkClient.getData(ZKTest.CHILD_WORK_NODE, ZNodeWatcher.watchInstance,
ZNodeDataCallBack.dataCB, "ZNodeStatCallBack");
}
}
}
5.ZNodeDataCallBack
节点数据改变回调
/**
* 节点数据改变的 回调方法 ==》 数据存在就 设置本地配置 并解除阻塞
*/
public class ZNodeDataCallBack implements AsyncCallback.DataCallback {
public static ZNodeDataCallBack dataCB = new ZNodeDataCallBack();
@Override
public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
if(data != null ){
ZKTest.config = new String(data);
ZKTest.cdl.countDown();
}
}
}
getData
获取数据再次添加此watch 并且添加一个 数据存在回调 有数据 修,改数据 更新本地配置文件 解除缓存NodeDeleted
中删除本地配置并且 syncConf
,到此,只是学习ZK 的API 的一部分,还有很多小问题,不如在 1.4 中的其他场景,在删除path 触发的watch NodeDeleted
,重新 syncConf
必须在新线程里面(上文代码);
也会在分布式的场景中,多个服务节点同时修改同一个资源;为了避免多个节点并发修改的问题,可以采用分布式锁的方案;节点在获取资源的同时,先去获取一把锁,这个锁只能在同一时间被一个节点获取,其他节点一直处于阻塞的状态,直到前一个节点修改资源完成,并释放了锁之后,再去获得锁;使得并发的工作在分布式锁的干预下变成一个线性的过程;
锁的基本要求
watch 虽好,但是多个节点watch 一个path ,可能会有压力;解决方案:临时节点+ sequence (并发创建);序列小的在前,添加watch,第二个
1.ZKLock
public class ZKLock {
@SneakyThrows
public static void main(String[] args) {
//模拟 多服务节点 获取锁
//重入特性重新设置
ZKUtils.zkClient.setData("/", "".getBytes(), -1);
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
WatchCallBack watchCallBack = new WatchCallBack();
String threadName = Thread.currentThread().getName();
watchCallBack.setThreadName(threadName);
watchCallBack.tryLock();
//干活
// do something
//释放锁
watchCallBack.unLock();
}
}.start();
}
}
}
2.WatchAndCallBack
@Data
public class WatchCallBack implements Watcher, AsyncCallback.StringCallback ,AsyncCallback.Children2Callback {
//创建该对象的线程
String threadName;
//创建的并发节点的路径
String pathName;
//加锁阻塞 直到最后加上锁才释放
CountDownLatch cc = new CountDownLatch(1);
@SneakyThrows
public void tryLock(){
try {
byte[] data = ZKUtils.zkClient.getData("/", false, new Stat());
if(new String(data).equals(threadName)){
//可以重入
}else{
ZKUtils.zkClient.create("/lock",threadName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL,
/*** StringCallback **/
this,"aaa");
cc.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* StringCallback 对象创建后回调
*/
@Override
public void processResult(int rc, String path, Object ctx, String name) {
//获取到创建后的节点名称 给 pathName
if(name != null ){
pathName = name ;
System.out.println("init info:"+ threadName+ " to: "+ pathName);
//给该节点的父节点添加 一个 getChildren 回调
ZKUtils.zkClient.getChildren("/",false,
/***Children2Callback ***/
this ,"bbb");
}
}
/**
* Children2Callback
*/
@Override
public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {
//一定能看到自己前边的。。
System.out.println(threadName+" try get lock ");
for (String child : children) {
System.out.println(child);
}
Collections.sort(children);
int i = children.indexOf(pathName.substring(1));
//是不是第一个
if(i == 0){
//yes
System.out.println(threadName +" is first lock,get lock ,起飞 ");
System.out.println(pathName);
try {
ZKUtils.zkClient.setData("/",threadName.getBytes(),-1);
cc.countDown();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println(threadName +" not first lock ,add watch to "+children.get(i-1));
ZKUtils.zkClient.exists("/" + children.get(i - 1),
/*** watch */
this, new StatCallback() {
@Override
public void processResult(int rc, String path, Object ctx, Stat stat) {
//stat 为空 提前释放了 这个时候也可以
if(stat == null){
cc.countDown();
}
}
}, "ccc");
}
}
@Override
public void process(WatchedEvent event) {
switch (event.getType()) {
case None:
break;
case NodeCreated:
break;
case NodeDeleted:
// 1. 释放锁 通知后面的第一个拿到锁
// 2. 不是第一个 和前一个关联
/****Children2Callback ***/
ZKUtils.zkClient.getChildren("/",false,this ,"sdf");
break;
case NodeDataChanged:
break;
case NodeChildrenChanged:
break;
}
}
public void unLock(){
try {
System.out.println(threadName + " unload ,deleted path:" + pathName);
ZKUtils.zkClient.delete(pathName,-1);
} catch ( Exception e) {
e.printStackTrace();
}
}
}
NodeDelete
事件 ,继续阻塞其中一个服务节点在获取到所有节点,并且是在判断自己节点是不是第一个节点的时候,上一个节点就已经把自己释放;会就导致后面无法继续执行!!!
前一个节点获取到释放过快(业务逻辑时间间隙),后面节点在判断的时候较慢 (判断时间)(加watch 的时候);
处理:1. 业务逻辑时间> 判断时间; 2. 给前一个exits 做判断;
//stat 为空 提前释放了 这个时候也可以
if(stat == null){
cc.countDown();
}
判断节点路径,是不是所有节点的第一个,是的话,就获取到锁,解除阻塞
NodeDelete
事件 ,继续阻塞其中一个服务节点在获取到所有节点,并且是在判断自己节点是不是第一个节点的时候,上一个节点就已经把自己释放;会就导致后面无法继续执行!!!
前一个节点获取到释放过快(业务逻辑时间间隙),后面节点在判断的时候较慢 (判断时间)(加watch 的时候);
处理:1. 业务逻辑时间> 判断时间; 2. 给前一个exits 做判断;
//stat 为空 提前释放了 这个时候也可以
if(stat == null){
cc.countDown();
}