官方插件

缓存

为每个 Plugin 隔离缓存,并组合本地缓存、后端、加载器、失效和请求合并。

@pluxel/plugin-cache 目前只供 Pluxel 工作区使用,尚不是公开安装入口。完整边界见 Package 矩阵。

@pluxel/plugin-cache 会识别当前调用它的 Plugin,并自动隔离普通 key。业务 Plugin 依赖抽象的 Cache,宿主选择内存或 Redis 后端;只有多个 Plugin 明确共享同一份值协议时,才应使用 cache.global。

先选择用法

需求写法
同步计算、进程内热点cache.local
简单异步 cache-aside method@Cached
主动失效、独立策略、多层缓存或统计cache.scope() + getOrLoad()
跨 Plugin 共享完全相同的数据 contractcache.global
长期 freshness、跨实例 claim/lock、事务repository/database

getOrLoad() 的主流程是 local -> backend -> loader -> backend/local write。decorator 只是便利写法,不负责 repository orchestration。

安装 provider

consumer 只依赖 Cache 抽象;host catalog 选择:

  • MemoryCacheBackendPlugin:单进程或本地开发;
  • Redis cache backend adapter:多实例共享加速层,见 Redis。

业务 Plugin 不同时维护独立 Redis client、第二套 namespace 和 cleanup。backend 选择属于 host composition。

默认 CachePlugin 策略是 ttlMs: 300000、maxEntries: 1000、maxInFlight: 256、 readPolicy: 'cache-first'、backendFailure: 'required'。应用将 CachePlugin、所选 backend 和 consumer 加入 catalog,配置记录与启动策略见 添加插件。

MemoryCacheBackendPlugin 默认 maxEntries: 10000,persistence.mode: 'off' 不需要 Persistence。 启用 best-effort 或 durable 时,宿主必须安装 Persistence:前者恢复/保存失败只告警,后者要求 durable、writable 存储且恢复失败时拒绝启动。persistence.flushIntervalMs 默认 1000,用于合并快照写入。 快照只恢复异步 backend,停机时间计入 TTL;可写快照要求 value 能被 Node structured-clone codec 序列化。 这是重启预热,不是数据库 durability。

部署配置入口

@pluxel/plugin-cache 根入口导出 CacheConfig 和 MemoryCacheBackendConfig,分别用于对应 Plugin 的 env/file 绑定。比如 envBinding(CachePlugin, { config: { schema: CacheConfig, mapping: { maxEntries: 'CACHE_MAX_ENTRIES' } } })。完整绑定与来源优先级见 Host 部署绑定。Workbench 不参与配置生效。

Cache 配置提供默认策略;consumer 的 scope() 显式字段覆盖默认值。Memory backend 的持久快照只恢复缓存条目,不恢复旧配置;当前容量、TTL 过期判定和持久化策略仍由本次部署决定。

显式绑定稳定 scope

复杂缓存应在 Plugin generation 启动时创建一次:


@({ : 'Catalog' })
export class  extends  {
	private !: CacheNamespace

	constructor(private readonly : ) {
		super()
	}

	protected override () {
		this. = this..('catalog', {
			: 10 * 60_000,
			: 5_000,
			: 32,
			: 'bypass',
		})
	}

	(: string, : string) {
		return this..([, ], () => this.(, ))
	}

	(: string, : string) {
		return this..([, ])
	}

	private (: string, : string) {
		return (, )
	}
}

同 caller、scope name 和 normalized policy 会复用同一个 bucket;同名 scope 请求不同 policy 会抛出 CachePolicyConflictError。不要让“第一次收到的业务请求”决定长期 scope policy。

handle 绑定 caller 和 provider generation。任一 stop/replacement 后继续调用旧 handle 会抛出 CacheStoppedError。

Key 设计

key 支持 string | number | bigint | boolean、最多 16 项的 primitive tuple 或最多 16 字段的 primitive-value plain record;canonical encoding 最多 1024 UTF-8 bytes。拒绝 nested object、accessor、symbol、非 finite number 和未配对 surrogate:

catalog.getOrLoad(itemId, load)
catalog.getOrLoad([tenantId, provider, itemId], load)
catalog.getOrLoad({ tenantId, provider, itemId }, load)

类型、tuple 位置、record 字段和 -0 都参与 canonical encoding。不要手工拼接可能歧义的复合 key,也不要把 credential/token 放进 key。

key 应包含所有会改变结果的 caller-visible input:tenant、locale、permission view、schema version 等。遗漏隔离维度会比 cache miss 更危险,因为它可能返回另一个安全域的数据。

Miss、负缓存和 single-flight

undefined 始终表示 miss,不能缓存。需要表示“查无结果”时返回 null 或明确的 domain value:

const user = await users.getOrLoad(userId, async () => {
	return (await repository.find(userId)) ?? null
})

同一进程、同 scope、同 canonical key 的 backend read 和 loader 自动合并。后到调用者订阅已有 Promise,不重复消耗 maxInFlight。

这是进程内 single-flight,不是跨进程 distributed lock。多个实例可能同时执行 loader;写入和外部 side effect 必须本身幂等或由 repository/database 协调。

达到 maxInFlight 时新 key 会抛出 CacheBusyError。调用方决定返回 503、绕过缓存或排队;不要在 cache 内建立无界等待队列。

读取策略与取消

readPolicy行为
cache-first默认:local → backend → loader
cache-and-refresh有效 local hit 立即返回,后台通过同一 single-flight 刷新;过期值仍是 miss
remote-first跳过 local read,backend / loader 结果仍写回 local

provider / scope 设置稳定策略,get() / getOrLoad() 可以按次覆盖 readPolicy。 set(key, value, { ttlMs }) 按值覆盖 TTL,0 表示不过期。 subscriber 的 AbortSignal 只取消自己的等待,不取消其他 subscriber 的共享工作。等待取消时原样 reject signal.reason(包括非 Error 值);未指定 reason 时为原生 AbortError。 global 共享值和同进程 single-flight,handle 仍绑定各自 caller;最后一个 owner 停止后清理 local registration,backend 值按 TTL 存续。

Backend failure policy

默认 required 适合把 backend 可用性视为业务前提。数据库是权威来源且能承受 Redis 故障后的降级流量时,在 scope 设置:

backendFailure: 'bypass'

getOrLoad() 的 backend read failure 会继续 loader;loader 成功后的 backend write failure 仍返回结果并发布 local。loader、数据库和外部 API error 不会被吞掉。

显式 get()、set()、delete() 和 clear() 始终严格传播 backend failure,因为调用方明确要求 backend operation。

Local cache

同步、纯计算热点使用 cache.local:

const parsed = this.cache.local.getOrCompute(['policy', policyVersion], () => parsePolicy(source), {
	ttlMs: 60_000,
})

local 数据只存在当前进程,不参与 Redis,也不提供跨 instance coherence。值应该是当前 caller 可安全复用的 immutable snapshot;不要缓存可被调用方继续 mutation 的共享对象。

Decorator

简单 method cache-aside 可以使用 decorator:

import { Cached, Memoized } from '@pluxel/plugin-cache'

@Cached({ ttlMs: 60_000 })
async findUser(id: string): Promise<User | null> {
	return (await this.users.find(id)) ?? null
}

@Memoized({ ttlMs: 5_000 })
featureEnabled(name: string): boolean {
	return this.flags.read(name)
}

constructor 仍需声明 Cache required dependency。@Cached 要求 Promise-returning method;同步方法使用 @Memoized。参数必须能形成稳定 primitive tuple,复杂参数通过 key 显式映射。

需要主动失效、查看 stats、按操作覆盖 read policy 或组合 repository 时,使用显式 scope,避免把 lifecycle 和 policy 藏在 decorator 中。

长期 freshness

getOrLoad(key, () => repository.loadFresh(...)) 只包住短期缓存。长期保存、refreshedAt、 跨实例 refresh claim/CAS/fencing、事务与外部 API 失败后的旧值策略属于 repository/database。 数据库中的 missing/tombstone 可向 Cache 返回 null;进程内 single-flight 不提供跨实例锁。

检查清单

在 测试宿主 中用计数 loader 连续读取同一个 key,两次调用应只加载一次;执行 delete() 后再读,应重新加载。使用不同 tenant 的 tuple key 验证隔离,用 null 验证负缓存。需要 Redis 共享缓存时,再在目标 backend 验证失效;单进程的请求合并不保证跨实例只运行一次 loader。

在缓存之外构造 Result

如果调用方必须区分“账号不存在”,在业务方法返回 Result;Cache 的 miss 仍是 undefined,不改成 Err。 下面的 AccountSource 是测试用内存数据源,实际应用换成权威 repository。CachedAccounts 只缓存普通账号或 明确的负缓存 null,每次读取后才创建 Result。空值会缓存 60 秒;新建账号后调用 invalidate() 撤销负缓存。

import { ,  } from '@pluxel/core'
import { ,  } from '@pluxel/core/better-result'
import { , type CacheNamespace } from '@pluxel/plugin-cache'

export type  = <{ : string; : string }>

export class  extends ('AccountNotFound')<{ : string }> {}

// 示例数据源;实际应用替换为 repository。
@()
export class  extends  {
	async (: string): < | null> {
		return  === 'alice' ? { , : 'Alice' } : null
	}
}

@()
export class  extends  {
	private !: CacheNamespace

	constructor(
		private readonly : ,
		private readonly : ,
	) {
		super()
	}

	protected override (): void {
		this. = this..('accounts', { : 60_000 })
	}

	async (: string): <<, >> {
		// Only data and the deliberate negative-cache sentinel cross the backend.
		const  = await this..(, () => this..())
		return  === null ? .(new ({  })) : .()
	}

	(: string): <boolean> {
		return this..()
	}
}

不要直接在返回 Result 的方法上使用 @Cached,也不要让 getOrLoad() 的 loader 返回 Err:它是普通的 fulfilled value,可能被缓存,经过 Redis / Persistence 序列化后也不保留 Result/Error prototype。 这里 repository 或 backend 的拒绝继续传播,不会被保存成“找不到账号”。 调用方对 find() 的 Err 提示账号缺失,对 Ok 使用 value;网络故障不应导致创建重复账号。

可执行示例与回归测试。

最后更新于

本页目录