Akka分布式游戏后端开发2 配置数据缓存

83 阅读3分钟

本专栏的项目代码在Github上,如果感兴趣的话,麻烦点点Star,谢谢啦。


在实际的项目中,游戏服务中的很多配置数据都是需要放到配置中心上的,供所有节点访问的,并且有时候还会有配置更新需求,并且在配置更新之后,还需要在游戏服务这边做相应的更新逻辑。

Apache Curator 是一个用于简化与 Apache Zookeeper 交互的高层次 Java 客户端库。它提供了一组高级特性和工具,用于解决 Zookeeper 原生 API 使用复杂、难以管理的问题,例如重试机制、连接管理以及常见的分布式场景(分布式锁、领导选举等)。

在这个项目中,我们使用 Zookeeper 作为配置中心,并且使用 Apache Curator 作为连接 Zookeeper 的客户端。并且使用 CuratorCache 来监听 Zookeeper 中的数据变化。

CuratorCache 简介

CuratorCache 是 Apache Curator 提供的一个高效的、易于使用的缓存工具,用于监听 Zookeeper 中节点的变化并维护一个本地缓存。它可以替代之前的 TreeCacheNodeCachePathChildrenCache,并且性能更高。

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 中的配置,并且处理配置更新的情况。