本专栏的项目代码在Github上,如果感兴趣的话,麻烦点点Star,谢谢啦。
在实际的项目中,游戏服务中的很多配置数据都是需要放到配置中心上的,供所有节点访问的,并且有时候还会有配置更新需求,并且在配置更新之后,还需要在游戏服务这边做相应的更新逻辑。
Apache Curator 是一个用于简化与 Apache Zookeeper 交互的高层次 Java 客户端库。它提供了一组高级特性和工具,用于解决 Zookeeper 原生 API 使用复杂、难以管理的问题,例如重试机制、连接管理以及常见的分布式场景(分布式锁、领导选举等)。
在这个项目中,我们使用 Zookeeper 作为配置中心,并且使用 Apache Curator 作为连接 Zookeeper 的客户端。并且使用 CuratorCache 来监听 Zookeeper 中的数据变化。
CuratorCache 简介
CuratorCache
是 Apache Curator 提供的一个高效的、易于使用的缓存工具,用于监听 Zookeeper 中节点的变化并维护一个本地缓存。它可以替代之前的 TreeCache
、NodeCache
和 PathChildrenCache
,并且性能更高。
CuratorCache 的使用方式
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.CuratorCache;
import org.apache.curator.framework.recipes.cache.CuratorCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class CuratorCacheExample {
public static void main(String[] args) throws Exception {
// 1. 创建 CuratorFramework 客户端
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("localhost:2181") // Zookeeper 地址
.retryPolicy(new ExponentialBackoffRetry(1000, 3)) // 重试策略
.build();
client.start();
// 2. 监听的路径(支持递归监听)
String path = "/example";
// 3. 创建 CuratorCache
CuratorCache cache = CuratorCache.build(client, path);
// 4. 添加监听器
CuratorCacheListener listener = CuratorCacheListener.builder()
.forCreatesAndChanges((nodePath, nodeData) -> {
System.out.println("Node created or updated: " + nodePath);
System.out.println("Data: " + (nodeData == null ? "null" : new String(nodeData.getData())));
})
.forDeletes((nodePath, nodeData) -> {
System.out.println("Node deleted: " + nodePath);
})
.build();
cache.listenable().addListener(listener);
// 5. 开始监听
cache.start();
System.out.println("CuratorCache is listening on path: " + path);
// 模拟阻塞,实际项目中可以让程序保持运行
Thread.sleep(Long.MAX_VALUE);
// 6. 关闭资源
cache.close();
client.close();
}
}
封装 CuratorCache
由于我们的配置数据都是以 Json 的形式存在,并且只关心更新操作,所以可以基于 CuratorCache 再次封装一下,不用写那么多代码。
class ConfigCache<C>(
private val zookeeper: AsyncCuratorFramework,
val path: String,
private val clazz: KClass<C>,
onUpdated: ((C) -> Unit)? = null
) : AutoCloseable where C : Any {
val logger = logger()
@Volatile
var config: C
private set
private val cache: CuratorCache
init {
val zookeeper = zookeeper.unwrap()
val bytes = zookeeper.data.forPath(path)
config = Json.fromBytes(bytes, clazz)
cache = CuratorCache.build(zookeeper, path, CuratorCache.Options.SINGLE_NODE_CACHE)
cache.listenable().addListener { type, _, data ->
when (type) {
CuratorCacheListener.Type.NODE_CREATED -> {
logger.info("Node {} created:{}", clazz, data.path)
config = Json.fromBytes(data.data, clazz)
onUpdated?.invoke(config)
}
CuratorCacheListener.Type.NODE_CHANGED -> {
logger.info("Node {} changed:{}", clazz, data.path)
config = Json.fromBytes(data.data, clazz)
onUpdated?.invoke(config)
}
CuratorCacheListener.Type.NODE_DELETED -> {
logger.warn("Node {} deleted:{}", clazz, data.path)
}
null -> Unit
}
}
cache.start()
}
suspend fun update(newConfig: C) {
val bytes = Json.toBytes(newConfig, clazz)
zookeeper.setData().forPath(path, bytes).await()
}
override fun close() {
cache.close()
}
}
这个是监听单个节点的代码,对于某个节点下的直接子节点是相同类型的数据这种情况,我们还可以写一个 ConfigChildrenCache
,这个时候就不能用 CuratorCache.Options.SINGLE_NODE_CACHE
这种模式了,我们需要监听的路径是这些变化节点路径的父路径,并且只匹配直接子路径变化这种情况。
class ConfigChildrenCache<C>(
zookeeper: AsyncCuratorFramework,
val path: String,
private val clazz: KClass<C>,
onChanged: ((Map<String, C>) -> Unit)? = null
) : AutoCloseable, Map<String, C> where C : Any {
private val configsByName: MutableMap<String, C> = mutableMapOf()
private val cache: CuratorCache
init {
val client = zookeeper.unwrap()
client.children.forPath(path).forEach { childPath ->
val bytes = client.data.forPath("$path/$childPath")
val child = Json.fromBytes(bytes, clazz)
configsByName[childPath] = child
}
cache = CuratorCache.build(client, path)
val regex = Regex("^$path/([^/]+)$")
cache.listenable().addListener { type, _, data ->
val matchResult = regex.matchEntire(data.path)
if (matchResult != null) {
val childPath = matchResult.groupValues[1]
when (type) {
CuratorCacheListener.Type.NODE_CREATED,
CuratorCacheListener.Type.NODE_CHANGED -> {
val child = Json.fromBytes(data.data, clazz)
configsByName[childPath] = child
onChanged?.invoke(configsByName)
}
CuratorCacheListener.Type.NODE_DELETED -> {
configsByName.remove(childPath)
onChanged?.invoke(configsByName)
}
null -> Unit
}
}
}
cache.start()
}
override fun close() {
cache.close()
}
override val size: Int
get() = configsByName.size
override val entries: Set<Map.Entry<String, C>>
get() = configsByName.entries
override val keys: Set<String>
get() = configsByName.keys
override val values: Collection<C>
get() = configsByName.values
override fun containsKey(key: String): Boolean {
return configsByName.containsKey(key)
}
override fun containsValue(value: C): Boolean {
return configsByName.containsValue(value)
}
override fun get(key: String): C? {
return configsByName[key]
}
override fun isEmpty(): Boolean {
return configsByName.isEmpty()
}
}
代码比较浅显易懂,这里就不多介绍了。
结尾
通过 ConfigCache
,我们能更加容易的在项目中获取到 Zookeeper 中的配置,并且处理配置更新的情况。