SpringBoot-ZkClient-守护线程

本文介绍了一个ZooKeeper实时监听器的设计与实现,通过ZkClient监听特定Node的子节点及其数据变化,同时提供了外围接口获取最新信息。文章详细展示了如何使用CountDownLatch确保后台线程持续运行,并通过SpringBoot集成该监听器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

接到一个需求,在现有的功能中启动一个后台线程,来实时监听目标ZooKeeper的某一Node的下各个配置的子节点变化(包括子节点的数据变化),并提供外围接口来获取最新的子节点信息。


需要监听的Node结构如下:

+ cfg
	- cfg1
	- cfg2
	- cfg3

Zookeeper的开源客户端有许多如:ZkClient,Curator,我们这里使用ZkClient,maven依赖如下:

<dependency>
	<groupId>com.101tec</groupId>
 	<artifactId>zkclient</artifactId>
 </dependency>

归纳起来,需求需要满足如下几点:

  1. 需要提供外围接口来获取最新的子节点信息
  2. 需要监听path下各个子节点个数的变化(增加、删除)需要监听path下各个子节点个数的变化(增加、删除)
  3. 需要监听各个子节点数据的变化
  4. 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();
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值