由于zk的一些特性:速度快、高可用、临时节点、序列节点、watcher等
所以可用zk来实现分布式的配置管理(如注册中心) 或者 分布式锁
这里简单用java + zk来实现
准备
idea新建springboot项目,主要是POM要引入Zookeeper的包,注意包的版本必须和zk服务器的版本一致!
分布式配置
如图,分布式节点连接zk集群,watch其中的节点数据(配置),则在节点数据被修改时,会向每个session发送callback,因此分布式节点能够实时更新到配置的信息
代码
ZKUtil用于获取zk客户端
import java.util.concurrent.CountDownLatch;
/**
* @author MasterYee
* @Description:
* @date: 2020/4/24
*/
public class ZKUtils {
public static ZooKeeper getZK() {
ZooKeeper zk;
try {
CountDownLatch cd = new CountDownLatch(1);
DefaultWatcher watcher = new DefaultWatcher();
// connectString是所有集群的地址,如果在后面加了/nodename,就表示这个连接以当前节点为根目录,否则就是/为根目录
// timeout是session断开后维持多久 (session挂则临时节点挂)
// watcher监控了当前整个session
zk = new ZooKeeper("192.168.153.110:2181,192.168.153.111:2181,192.168.153.112:2181,192.168.153.113:2181/locktest",
3000, watcher);
watcher.setCd(cd);
cd.await(); // 阻塞住,知道连接成功后释放门栓
} catch (Exception e) {
e.printStackTrace();
return null;
}
return zk;
}
}
DefaultWatcher 创建连接时的watch
package com.example.zkdemo;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import java.util.concurrent.CountDownLatch;
/**
* @author MasterYee
* @Description:
* @date: 2020/4/24
*/
public class DefaultWatcher implements Watcher {
private CountDownLatch cd;
@Override
public void process(WatchedEvent event) {
System.out.println("---------------------watching event----------");
System.out.println("event : " + event.toString());
// 由于是异步执行的,当连接成功后,触发事件异步调用process,但此时程序已经跑完了!所以用一个门栓来控制
Event.KeeperState state = event.getState();
switch (state) {
case Unknown:
break;
case Disconnected:
break;
case NoSyncConnected:
break;
case SyncConnected:
cd.countDown(); // 门栓-- 放行
System.out.println("connected....");
break;
case AuthFailed:
break;
case ConnectedReadOnly:
break;
case SaslAuthenticated:
break;
case Expired:
break;
case Closed:
break;
}
}
public CountDownLatch getCd() {
return cd;
}
public void setCd(CountDownLatch cd) {
this.cd = cd;
}
}
注意上面用了门栓,这是因为程序运行太快了,运行完主线程就退出了,建立了连接之后的callback都没有调用到程序就跑完了
Test类
package com.example.zkdemo;
import org.apache.zookeeper.ZooKeeper;
import java.util.concurrent.CountDownLatch;
/**
* @author MasterYee
* @Description: zookeeper 做配置管理
* @date: 2020/4/24
*/
public class TestConfig {
static ZooKeeper zk;
public static void main(String[] args) {
zk = ZKUtils.getZK();
getConf();
while (true);
}
/**
* 先判断是否存在 会返回一个callback
* 在callback里异步判断是否存在 存在则get
* get同样会返回一个callback 在这个callback里将data赋值
*/
public static void getConf() {
WatchCallback watchCallback = new WatchCallback();
watchCallback.setZk(zk);
CountDownLatch cd = new CountDownLatch(1);
watchCallback.setCd(cd);
watchCallback.setPath("/AppConf");
watchCallback.await();
}
}
这里有一个WatchCallback,这是因为reactor模型异步调用太深了,所以用一个WatchCallback来封装,它既是watcher又是datacallback又是statcallback
WatchCallback
package com.example.zkdemo;
import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import java.util.concurrent.CountDownLatch;
/**
* @author MasterYee
* @Description:
* @date: 2020/4/24
*/
public class WatchCallback implements Watcher, AsyncCallback.StatCallback, AsyncCallback.DataCallback {
private ZooKeeper zk;
private CountDownLatch cd;
private String path;
private String retData;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public CountDownLatch getCd() {
return cd;
}
public void setCd(CountDownLatch cd) {
this.cd = cd;
}
public ZooKeeper getZk() {
return zk;
}
public void setZk(ZooKeeper zk) {
this.zk = zk;
}
// DataCallback的回调 (get时)
@Override
public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
if (data != null) {
// get的回调有数据
retData = new String (data);
}
}
// StatCallback的回调 (exits时)
@Override
public void processResult(int rc, String path, Object ctx, Stat stat) {
if(stat != null) {
// 有数据时 get数据
zk.getData(path,this,this,"ABC");
}else {
System.out.println("没有数据: " + path);
}
}
// 监听到watch事件
@Override
public void process(WatchedEvent event) {
Event.EventType type = event.getType();
switch (type) {
case None:
break;
case NodeCreated:
System.out.println("node created");
// 监听到node添加时 获取配置
zk.getData(path,this,this,"ABC");
break;
case NodeDeleted:
// 监听到node删除时
System.out.println("node deleted!");
this.retData = null;
break;
case NodeDataChanged:
System.out.println("node changed");
// 监听到node改变时 重新获取配置
zk.getData(path,this,this,"ABC");
break;
case NodeChildrenChanged:
break;
case DataWatchRemoved:
break;
case ChildWatchRemoved:
break;
case PersistentWatchRemoved:
break;
}
}
/**
* 阻塞地去获取配置
* 先判断是否存在 会返回一个callback
* 在callback里异步判断是否存在 存在则get
* get同样会返回一个callback 在这个callback里将data赋值
*/
public void await() {
zk.exists(path,this,this,"ABC");
try {
while (true) {
Thread.sleep(1000);
System.out.println("获得配置: " + retData);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public String getRetData() {
return retData;
}
public void setRetData(String retData) {
this.retData = retData;
}
}
create节点或者修改或者删除节点后,客户端都会收到回调,收到信息。
分布式锁
设计思路:
首先肯定是设置临时节点,这样在程序出错session断开时不会产生死锁
有且仅有一个人能拿到锁,且在运行完后需要释放锁
可以是多个线程exists锁,如果存在则阻塞等待,不存在则创建并获得锁,当一个线程释放后,争抢锁
如何监听到锁释放?1:程序主动轮询,轮询去访问锁是否释放 2:watch+callback,zk释放锁时回调程序。这里肯定是2,因为1会有延迟+轮询压力
所以思路可以是临时节点+watch,但一个锁释放后,其它所有节点都将收到callback,如果节点过多,这样也会造成通信的压力
最终思路 sequence+watch+临时节点,每个节点获取锁时先创建序列节点,然后watch前一个序列节点,通过getchildren获取所有序列节点并排序,如果当前自己排在第一位则获取锁;释放锁时删除这个序列节点,则下一个节点callback,下一个节点获取到锁。
TestLock
package com.example.zkdemo.lock;
import com.example.zkdemo.ZKUtils;
import org.apache.zookeeper.ZooKeeper;
/**
* @author MasterYee
* @Description:
* @date: 2020/4/24
*/
public class TestLock {
public static void main(String[] args) {
// 起10个线程 每个线程尝试去拿锁
for (int i = 0; i < 10 ; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
ZKLock lock = new ZKLock();
ZooKeeper zk = ZKUtils.getZK();
lock.setZk(zk);
lock.setThreadName(Thread.currentThread().getName());
lock.tryLock();
System.out.println("线程获得锁: " + Thread.currentThread().getName());
Thread.sleep(100);
lock.unLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
ZKLock
package com.example.zkdemo.lock;
import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* @author MasterYee
* @Description:
* @date: 2020/4/24
*/
public class ZKLock implements AsyncCallback.Create2Callback, AsyncCallback.Children2Callback , Watcher, AsyncCallback.StatCallback {
private CountDownLatch cd = new CountDownLatch(1);
private ZooKeeper zk;
private String threadName;
private String pathName;
public void tryLock () {
try {
System.out.println("threadName尝试获取锁.. " + threadName);
// 临时+序列节点
zk.create("/lock",threadName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL,this,"ABC");
cd.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void unLock () {
try {
zk.delete(pathName,-1);
System.out.println(threadName + " over work....");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
// 创建完的回调
// 返回的name是创建的序列临时节点的名称
@Override
public void processResult(int rc, String path, Object ctx, String name, Stat stat) {
// 创建成功后获取所有的子节点
System.out.println("线程create node : " + threadName + " " + name);
pathName = name;
zk.getChildren("/",false,this,"ABC");
}
// getChildre时的回调
@Override
public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {
// 对children排序 找到序列最小的
Collections.sort(children);
int i = children.indexOf(pathName.substring(1));
if(i == 0) {
// 是第一个线程
cd.countDown();
} else {
// 不是第一个节点 就watch上一个
zk.exists("/" + children.get(i-1),this,this,"ABC");
}
}
// watch的监听方法
@Override
public void process(WatchedEvent event) {
Event.EventType type = event.getType();
switch (type) {
case None:
break;
case NodeCreated:
break;
case NodeDeleted:
// 上一个节点被删除了,这个节点收到了回调事件
// 尝试拿锁
zk.getChildren("/",false,this,"ABC");
break;
case NodeDataChanged:
break;
case NodeChildrenChanged:
break;
case DataWatchRemoved:
break;
case ChildWatchRemoved:
break;
case PersistentWatchRemoved:
break;
}
}
// statCallback的回调
@Override
public void processResult(int rc, String path, Object ctx, Stat stat) {
}
public String getPathName() {
return pathName;
}
public void setPathName(String pathName) {
this.pathName = pathName;
}
public ZooKeeper getZk() {
return zk;
}
public void setZk(ZooKeeper zk) {
this.zk = zk;
}
public void setCd(CountDownLatch cd) {
this.cd = cd;
}
public CountDownLatch getCd() {
return cd;
}
public String getThreadName() {
return threadName;
}
public void setThreadName(String threadName) {
this.threadName = threadName;
}
}
可以看到,每个线程拿锁时,会往zk里设置一个临时序列节点(以lock开头、值为线程名(可不要)),创建完节点后,当前线程的门栓拴住、阻塞,创建的回调方法去获取所有的children,获取所有孩子的回调方法中又对孩子进行了排序、当当前节点是最小的节点值时门栓放开,线程继续,最后释放锁(删除节点);当当前节点不是最小节点时,watch前一个节点,直到上一个节点删除,会重新获取一次所有孩子(也就是重新尝试拿锁)。