NocoBase CacheManager 缓存管理器源码级解析:从实例创建到 Store 注册
【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
导读
CacheManager是 NocoBase 的核心缓存管理模块,基于node-cache-manager生态封装,负责统一创建、注册与回收各类缓存。本文以 CacheManager 官方 API 文档 为主线,结合 packages/core/cache/src/cache-manager.ts 的完整实现与测试用例,深入讲解CacheManagerOptions配置、registerStore扩展注册、createCache实例创建、getCache/flushAll/close生命周期管理,并说明 NocoBase 服务端如何将它接入应用启动流程。读完本文,你将能独立在 NocoBase 插件中注册 Redis/Memory 缓存、自定义 Store 类型,并理解缓存命名空间与 key 前缀的隔离机制。
概览:CacheManager 的职责与内置 Store
CacheManager为 NocoBase 提供统一的 Cache 模块管理能力,其底层依赖node-cache-manager的caching(store, config)工厂方法。内置的 Cache 类型有两种:
- memory:由
node-cache-manager默认提供的 lru-cache; - redis:由
cache-manager-redis-yet的redisStore支持。
更多类型(如自研的中间件缓存)可以通过registerStore()API 扩展注册。从 packages/core/cache/src/index.ts 可以看出,@nocobase/cache包同时导出CacheManager、Cache、布隆过滤器(bloom-filter)与计数器(counter)四组能力,本文聚焦前两者。
三个核心概念
文档中明确了三个容易混淆的概念,理解它们有助于看懂后续所有 API:
| 概念 | 说明 |
|---|---|
| Store | 定义一种缓存方式,包含创建缓存的工厂方法和其他相关配置。每种缓存方式都有唯一标识,在注册时提供。内置标识即memory和redis |
| Store 工厂方法 | 由node-cache-manager及相关扩展包提供,即caching()方法的第一个参数,如'memory'、redisStore |
| Cache | NocoBase 封装的类,提供使用缓存的方法。实际读写操作的是Cache实例,每个实例有唯一标识,可作为区分不同模块的命名空间 |
在源码中,这三层结构对应CacheManager.storeTypes(注册的 Store 类型)、CacheManager.stores(已创建的底层 store 实例)与CacheManager.caches(已创建的Cache实例),三个 Map 分别在 cache-manager.ts 中声明。
构造函数:合并默认配置并注册内置 Store
签名与类型
constructor(options?: CacheManagerOptions)export type CacheManagerOptions = Partial<{ defaultStore: string; stores: { [storeType: string]: StoreOptions; }; prefix: string; // 源码中额外支持,见下文 }>; type StoreOptions = { store?: 'memory' | FactoryStore<Store, any>; close?: (store: Store) => Promise<void>; // global config [key: string]: any; };注意:官方文档的类型定义未包含
prefix,但实际源码 cache-manager.ts 中的CacheManagerOptions还支持全局prefix,会作为所有 Cache 实例 key 前缀的默认值,下文createCache一节会说明其合并规则。
参数详细说明
CacheManagerOptions
| 属性 | 类型 | 描述 |
|---|---|---|
defaultStore | string | 默认 Cache 类型的唯一标识,createCache省略store时生效 |
stores | Record<string, StoreOptions> | 注册 Cache 类型,key 为类型唯一标识,值为包含注册方法与全局配置的对象 |
prefix | string | (源码支持)全局默认 key 前缀,可选 |
StoreOptions
| 属性 | 类型 | 描述 |
|---|---|---|
store | memory|FactoryStore<Store, any> | store 工厂方法,对应caching第一个参数 |
close | (store: Store) => Promise<void> | 可选。Redis 等需要建立连接的中间件需提供关闭连接的回调,入参为工厂方法返回的对象 |
[key: string] | any | 其他 store 全局配置,对应caching第二个参数 |
默认 options 与深合并行为
源码构造函数中的默认配置如下(与文档一致):
import { redisStore, RedisStore } from 'cache-manager-redis-yet'; const defaultOptions: CacheManagerOptions = { defaultStore: 'memory', stores: { memory: { store: 'memory', // 全局配置 max: 2000, }, redis: { store: redisStore, close: async (redis: RedisStore) => { if (!redis.client?.isOpen) { return; } await redis.client.quit(); }, }, }, };与文档示例相比,源码 cache-manager.ts 在close中增加了redis.client?.isOpen守卫:仅当连接处于打开状态时才调用quit(),避免对已关闭的连接重复调用导致异常。
options与默认值通过deepmerge进行深合并(见 cache-manager.ts),因此默认配置已有的内容可以缺省。例如只覆盖 Redis 的连接地址:
const cacheManager = new CacheManager({ stores: { redis: { // redisStore 已在默认 options 中提供,只需覆盖配置 url: 'redis://localhost:6379', }, }, });构造完成后,代码会遍历stores,将每个 store 拆分为store工厂方法与其余全局配置,依次调用registerStore()完成注册(cache-manager.ts)。这与测试 cache-manager.test.ts 中手动registerStore({ name: 'memory', store: 'memory' })后再创建缓存的行为完全一致。
registerStore:扩展注册自定义缓存方式
用法示例
import { redisStore, RedisStore } from 'cache-manager-redis-yet'; cacheManager.registerStore({ // store 唯一标识 name: 'redis', // 创建 store 的工厂方法 store: redisStore, // 关闭 store 连接 close: async (redis: RedisStore) => { await redis.client.quit(); }, // 全局配置 url: 'xxx', });签名与实现
registerStore(options: { name: string } & StoreOptions)源码实现非常简洁(cache-manager.ts):
registerStore(options: { name: string } & StoreOptions) { const { name, ...rest } = options; this.storeTypes.set(name, rest); }它将name拆出作为 Map 的 key,其余内容(工厂方法、close 回调、全局配置)整体存入storeTypes。注册本身不会建立任何连接,真正的连接在createCache首次创建时才会建立——这是按需初始化的设计,避免应用启动时无谓地创建连接。
createCache:创建 Cache 实例
用法示例
await cacheManager.createCache({ name: 'default', // cache 唯一标识 store: 'memory', // store 唯一标识 prefix: 'mycache', // 自动给缓存 key 加上 'mycache:' 前缀,可选 // 其他 store 配置,会和 store 全局配置合并 max: 2000, });签名
createCache(options: { name: string; prefix?: string; store?: string; [key: string]: any }): Promise<Cache>options 详细说明
| 属性 | 类型 | 描述 |
|---|---|---|
name | string | cache 唯一标识 |
store | string | store 唯一标识 |
prefix | string | 可选,缓存 key 前缀 |
[key: string] | any | 其他 store 相关的自定义配置项 |
源码中的两条关键分支
阅读 cache-manager.ts 的实现,可以归纳出createCache的完整决策逻辑:
async createCache(options: { name: string; prefix?: string; store?: string; [key: string]: any }) { const { name, store = this.defaultStore, ...config } = options; let { prefix } = options; prefix = this.prefix ? (prefix ? `${this.prefix}:${prefix}` : this.prefix) : prefix; if (!lodash.isEmpty(config) || store === 'memory') { const newStore = await this.createStore({ name, storeType: store, ...config }); return this.newCache({ name, prefix, store: newStore }); } const s = this.stores.get(store); if (!s) { const defaultStore = await this.createStore({ name: store, storeType: store }); return this.newCache({ name, prefix, store: defaultStore }); } return this.newCache({ name, prefix, store: s.store }); }可以提炼出以下几点对使用者至关重要的行为:
store省略时使用defaultStore:此时缓存方式会跟随系统默认缓存方式改变而改变,例如应用从 memory 切换为 redis 后,省略store的缓存自动改用 redis,实现"配置即切换"。共享默认缓存空间:没有自定义配置时(且 store 非 memory),会复用该 store 类型下已创建的共享实例,即文档所说"返回由全局配置创建、当前缓存方式共享的默认缓存空间"。因此推荐加上
prefix避免不同模块之间的 key 冲突:// 使用默认缓存,使用全局配置 await cacheManager.createCache({ name: 'default', prefix: 'mycache' });memory 特殊处理:由于内存缓存本身独立于进程,条件
store === 'memory'会让每次调用都为该 Cache 实例单独创建底层 store,确保不同命名空间的 memory 缓存互不干扰。自定义配置触发独立实例:只要传入了额外配置(如
max、ttl),即使 store 相同也会走createStore创建独立实例,并将自定义配置与全局配置合并后传给caching()。这对应文档中createCache示例里max: 2000的语义。未注册的 store 类型会报错:
createStore内部若在storeTypes中找不到对应类型,会抛出Create cache failed, store type [...] is unavailable or not registered(cache-manager.ts),提示先注册。
prefix 的全局与局部合并
当构造 CacheManager 时传入了全局prefix,且createCache也传了prefix,源码会将两者拼接为全局prefix:局部prefix;只传一个时则使用该值。NocoBase 服务端正是利用这一点,在应用层面统一注入命名空间(见下文"服务端集成")。
返回的 Cache 实例
createCache返回的是@nocobase/cache封装后的Cache类(参考 Cache 文档),其内部持有底层node-cache-manager的 store,并自动为每个 key 套用prefix:key的命名空间(见 cache.ts)。Cache类提供的方法包括:
- 基础读写:
get()、set(key, value, ttl)、del()、reset()、wrap(); - 批量操作:
mset()、mget()、mdel(); - 命名空间查询:
keys(pattern)、ttl(key); - NocoBase 扩展:
wrapWithCondition()(按条件决定是否使用缓存)、setValueInObject()/getValueInObject()/delValueInObject()(操作对象型缓存的单个字段)。
getCache:按名称获取缓存
cacheManager.getCache('default');签名:getCache(name: string): Cache
源码中该方法是同步的,直接从未创建的缓存 Map 中查询(cache-manager.ts):
getCache(name: string): Cache { const cache = this.caches.get(name); if (!cache) { throw new Error(`Get cache failed, ${name} is not found`); } return cache; }因此必须先createCache再getCache,否则会抛出Get cache failed, xxx is not found。在实际使用中,getCache常用于跨模块共享:某个模块负责创建并注册缓存,其他模块通过名称获取同一实例,从而以name为命名空间实现缓存隔离与复用。
flushAll 与 close:生命周期管理
flushAll:重置所有缓存
await cacheManager.flushAll();源码实现遍历所有已创建的Cache实例并逐个调用reset(),通过Promise.all并行执行(cache-manager.ts):
async flushAll() { const promises = []; for (const cache of this.caches.values()) { promises.push(cache.reset()); } await Promise.all(promises); }注意它只清空caches中已创建的缓存实例,不会关闭连接,适合在数据变更后整体刷新缓存内容的场景。
close:关闭所有缓存中间件连接
await cacheManager.close();源码遍历stores中每个已创建 store 的close回调并执行(cache-manager.ts):
async close() { const promises = []; for (const s of this.stores.values()) { const { close, store } = s; close && promises.push(close(store.store)); } await Promise.all(promises); }只有注册 Store 时提供了close回调的才会被调用(如 Redis 的redis.client.quit()),memory 缓存无需关闭。测试 cache-manager.test.ts 验证了该行为:注册时传入closemock,调用cacheManager.close()后断言其被调用。该方法是应用优雅退出时释放连接的关键入口。
服务端集成:CacheManager 在 NocoBase 应用中的接入方式
CacheManager 并不仅仅是独立工具类,它已深度接入 NocoBase 服务端应用生命周期。核心代码在 packages/core/server/src/cache/index.ts:
export const createCacheManager = async (app: Application, options: CacheManagerOptions) => { const cacheManager = new CacheManager(options); const defaultCache = await cacheManager.createCache({ name: app.name }); app.cache = defaultCache; app.context.cache = defaultCache; return cacheManager; };而 packages/core/server/src/application.ts 中应用加载时创建 CacheManager 的方式如下:
async createCacheManager() { this._cacheManager = await createCacheManager(this, { prefix: this.name, // 以应用名作为全局 key 前缀 ...this.options.cacheManager, }); return this._cacheManager; }从中可以提炼出以下事实:
- 应用名即命名空间:NocoBase 自动以
app.name作为 CacheManager 的全局prefix,因此默认缓存的 key 天然带有应用级隔离;开发者还可以通过options.cacheManager(即CacheManagerOptions)覆盖defaultStore、stores等配置。 - 默认缓存实例直挂应用:创建后名为
app.name的默认缓存会被赋给app.cache与app.context.cache,业务代码中可以直接通过ctx.cache读写默认缓存,无需自行管理 CacheManager。 - 生命周期联动:应用
load()时会先close()旧 CacheManager 再重建(application.ts),应用销毁时也会调用cacheManager.close()释放连接(application.ts),并通过this.context.cacheManager = this._cacheManager暴露给请求上下文(application.ts)。
进阶:源码中的实验性扩展(BloomFilter 与 Counter)
在 cache-manager.ts 中,CacheManager还提供了两个标注@experimental的方法,展示了 Cache 基座之上的扩展能力:
createBloomFilter(options?):基于缓存创建布隆过滤器,memorystore 返回MemoryBloomFilter,redisstore 返回RedisBloomFilter,其他 store 抛出BloomFilter store [...] is not supported。实现上会尝试getCache('bloom-filter')复用,不存在则自动创建。createCounter(options, lockManager?):创建计数器,memory返回MemoryCounter,redis返回RedisCounter,其他 store 则要求传入LockManager并返回LockCounter,否则报错。
这两个 API 说明 CacheManager 不只是简单的 key-value 管理器,而是 NocoBase 缓存能力(包括分布式锁、计数、去重等场景)的统一入口,相关实现位于 packages/core/cache/src/bloom-filter 与 packages/core/cache/src/counter。
总结
围绕 CacheManager 文档,本文梳理了 NocoBase 缓存管理器的完整使用链路:
- 构造:通过
CacheManagerOptions声明defaultStore与stores,与内置默认配置深合并; - 注册:
registerStore()以name + StoreOptions扩展任意缓存方式; - 创建:
createCache()依据store/自定义配置决定复用共享实例还是新建实例,并用prefix隔离 key; - 使用:
getCache()按名称获取Cache实例,执行get/set/del/wrap等读写; - 回收:
flushAll()并行重置所有缓存,close()关闭所有中间件连接。
配合 源码实现、测试用例 与 服务端接入代码,开发者既可以作为使用者快速接入 Redis,也可以作为扩展者注册自定义 Store,甚至基于createBloomFilter/createCounter构建更复杂的分布式能力。
【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考