接到一个需求,在现有的功能中启动一个后台线程,来实时监听目标ZooKeeper的某一Node的下各个配置的子节点变化(包括子节点的数据变化)
,并提供外围接口来获取最新的子节点信息。
需要监听的Node结构如下:
+ cfg
- cfg1
- cfg2
- cfg3
Zookeeper的开源客户端有许多如:ZkClient,Curator,我们这里使用ZkClient
,maven依赖如下:
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
</dependency>
归纳起来,需求需要满足如下几点:
- 需要提供外围接口来获取最新的子节点信息
- 需要监听path下各个
子节点个数的变化
(增加、删除)需要监听path下各个子节点个数的变化
(增加、删除) - 需要监听各个
子节点数据的变化
- zkClient的启动线程需要在在后台运行.
Question-1
//定义一个全局的_cache 来缓存数据信息
private final HashMap<String, Object> _cache = new HashMap<>();
/**
* 供外部接口调用,获取缓存信息;
* @return
*/
public HashMap getCache() {
return _cache;
}
Question-2
//监听 ----- 子节点(个数)发生变化
zkClient.subscribeChildChanges(path, (String parentPath, List<String> currentChilds) -> {
//子节点(个数)发生变化: 重新填充缓存
fillCacheMap(parentPath, currentChilds);
});
Question-3
//监听----当前节点 数据变化
zkClient.subscribeDataChanges(fullPath, new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
_cache.put(dataPath, data);
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
_cache.remove(dataPath);
}
});
Question-4
如何让线程在后台一直运行呢,我们能想到的方法有:Thread.sleep(无限大值)
。
这里我们使用CountDownLatch
.
private CountDownLatch shutdownLatch = new CountDownLatch(1);
/**
* 启动....
* @param path
* @param server
*/
public void setUp(String path, String server) {
zkClient = new ZkClient(server, 3000);
//zkClient do sth....
shutdownLatch.await();//阻塞----后台一直运行
}
/**
* 关闭
*/
public void shutDown() {
shutdownLatch.countDown();
}
完整代码
MyZkClientHolder
public class MyZkClientHolder {
private MyZkClientHolder() {
}
private static class MyZkClientHolderFactory {
private static MyZkClientHolder instance = new MyZkClientHolder();
}
public static MyZkClientHolder getInstance() {
return MyZkClientHolderFactory.instance;
}
private CountDownLatch shutdownLatch = new CountDownLatch(1);
//防止重复运行Flag...
private volatile boolean alreadySetUpFlag = false;
private ZkClient zkClient;
private final HashMap<String, Object> _cache = new HashMap<>();
/**
* 启动....
* @param path
* @param server
*/
public void setUp(String path, String server) {
if (alreadySetUpFlag) {
return;
}
zkClient = new ZkClient(server, 3000);
zkClient.setZkSerializer(new MyZkSerializer());
List<String> children = zkClient.getChildren(path);
//初始化缓存
fillCacheMap(path, children);
//监听 ----- 子节点变化
zkClient.subscribeChildChanges(path, (String parentPath, List<String> currentChilds) -> {
//子节点发生变化: 重新填充缓存
fillCacheMap(parentPath, currentChilds);
});
alreadySetUpFlag = true;
try {
shutdownLatch.await();//阻塞----后台一直运行
//运行到此处,说明已经关闭
alreadySetUpFlag = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void fillCacheMap(String parentPath, List<String> children) {
if (!_cache.isEmpty()) {
_cache.clear();
}
children.stream().forEach(child -> {
String fullPath = parentPath + "/" + child;
Object value = zkClient.readData(fullPath);
_cache.put(fullPath, value);
//监听----当前节点 数据变化
zkClient.subscribeDataChanges(fullPath, new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
_cache.put(dataPath, data);
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
_cache.remove(dataPath);
}
});
});
//todo sendto kafka......
}
/**
* 供外部接口调用,获取缓存信息;
*
* @return
*/
public HashMap getCache() {
return _cache;
}
/**
* 关闭
*/
public void shutDown() {
shutdownLatch.countDown();
}
public static void main(String[] args) throws InterruptedException {
String path = "/cfg";
String server = "192.168.129.159:2181";
new Thread(() -> {
MyZkClientHolder.getInstance().setUp(path, server);
}).start();
for (int i = 0; i < 1000; i++) {
System.out.println(MyZkClientHolder.getInstance().getCache());
Thread.sleep(3000);
}
}
}
MyZkSerializer
public class MyZkSerializer implements ZkSerializer {
@Override
public Object deserialize(byte[] bytes) throws ZkMarshallingError {
return new String(bytes, Charsets.UTF_8);
}
@Override
public byte[] serialize(Object obj) throws ZkMarshallingError {
return String.valueOf(obj).getBytes(Charsets.UTF_8);
}
}
SpringBoot 后台启动守护线程
上述代码已经完成客户的需求,并可以通过MyZkClientHolder.main()
调用测试。
但是现在有个新的问题,如何将上述的代码与现有项目(SpringBoot)集成呢?
参照stackoverflow上类似的解决方案:SpringBoot启动后台线程-最佳实践
@Component
class EventSubscriber implements DisposableBean, Runnable {
private Thread thread;
private volatile boolean someCondition;
EventSubscriber(){
this.thread = new Thread(this);
this.thread.start();
}
@Override
public void run(){
while(someCondition){
doStuff();
}
}
@Override
public void destroy(){
someCondition = false;
}
}
结合上述应用,集成后完整代码:
@Component
public class MyZkClientEventSubscriber implements DisposableBean, Runnable {
private String path ;
private String server ;
private Thread thread;
MyZkClientEventSubscriber(){
//todo path 和server 从配置文件中取;
this.path = "/cfg";
this.server = "s159:2181";
this.thread = new Thread(this,"my_zk_client_holder_thread");
this.thread.start();
}
@Override
public void run() {
MyZkClientHolder.getInstance().setUp(this.path,this.server);
}
@Override
public void destroy() throws Exception {
MyZkClientHolder.getInstance().shutDown();
}
}