Skip to content
bitzorcas
中EN

Concept

缓存

使用 FusionCache、Redis 与 ICacheStore 构建可降级、可观测的多层缓存,并正确处理键、负缓存、区域目录、预热与失效。

Last updated

BitzOrcas 的应用缓存不是一层 IMemoryCache 包装。生产组合根通过 AddBitzOrcasCaching() 注册 FusionCache:进程内缓存是 L1;配置 Redis 后,Redis 同时承担 L2 与多实例失效通知。业务代码通过 ICacheStore 使用它,不直接依赖 FusionCache 或 Redis。

运行结构

Application → ICacheStore → FusionCacheStore → L1 memory
└──────→ L2 Redis + backplane

Application

ICacheStore

FusionCache L1

Redis L2

Backplane 失效广播

  • 没有 Redis 时,应用仍可用 L1 运行,适合本地开发和单实例部署。
  • Redis 短暂故障时,默认把读取错误当作未命中,并允许工厂重新计算;可通过策略收紧行为。
  • Request 作用域只在当前请求内缓存,不写入分布式层。
  • MemoryCacheStore 是单实例回退实现;它不支持跨实例标签失效。

应用端口

ICacheStore 提供读取、读穿、写入、按键删除、按标签删除和批量读写。CacheResult<T> 明确区分三种状态:

状态含义调用方动作
Hit找到非空值直接使用
Negative已确认数据不存在不再查询数据源
Miss没有缓存结论查询数据源或调用 GetOrCreateAsync

负缓存可以挡住“反复查询不存在记录”的穿透流量,但 NegativeTtl 应明显短于正常 TTL。

// ① 先看契约与控制流;校验、取消和类型化错误都要显式保留。
var policy = CachePolicy.Short with
{
// ② Tenant Scope 让统一组件把租户维度纳入 Key,AreaTag 支持成组失效。
Scope = CacheScope.Tenant,
AreaTag = "catalog",
Tags = ["product", $"product:{productId}"]
};
// ③ Factory 只在 Miss 时读取事实来源,并一路传播取消信号。
var product = await cache.GetOrCreateAsync(
key,
policy,
token => repository.GetAsync(productId, token),
cancellationToken);

键与隔离

通过 ICacheKeyBuilder 构造 CacheKey,不要手拼字符串。标准形态包含应用、环境、版本、区域、作用域和业务片段:

{app}:{env}:v{version}:{area}:{scope}:{parts...}

租户数据必须使用租户作用域,并把可信 tenantId 放入键。键的 UTF-8 长度上限为 512 字节;不要放访问令牌、邮箱、手机号等敏感原文。

CacheKeyBuilder 从 ICurrentUserAccessor 注入作用域,而不是接受调用方传入的 tenant/user:

Scope生成段缺少上下文时
Global无额外段正常
Tenanttenant-{TenantId}抛出异常
Usertenant-{TenantId}:user-{UserId}抛出异常
Clienttenant-{TenantId}:client-{ClientId}抛出异常
Requestreq-{UUIDv7}每次构造新键

Request scope 会跳过分布式缓存和 backplane,但仍使用本次构造的进程内键;它不是自动绑定整个 HTTP 请求的共享字典。需要同一请求内复用时,调用方必须保留同一个 CacheKey。

策略怎么选

内置预设覆盖常见场景:VeryShort 适合秒级热点,Short 适合普通读模型,Medium 与 Long 适合低频变更数据;OneTimeTicket、Idempotency 表达的是专门场景,不应随意复用。

CachePolicy 还控制:

  • Ttl 与 NegativeTtl:正常值和不存在结果的寿命。
  • CacheTimeout:缓存操作预算,默认 500 ms,最大 5 秒。
  • JitterRatio:给过期时间加抖动,避免同一时刻集中回源。
  • HideErrors:缓存故障时是否回源;默认开启。
  • AllowStaleOnError 与 StaleTtl:数据源故障时是否接受旧值。
  • AreaTag 与 Tags:批量失效边界。

写操作后的失效

先提交业务事务,再删除受影响的键或标签。标签应围绕业务边界设计,例如 catalog:products,而不是用一个全局标签清空全部缓存。多实例部署中,FusionCache backplane 会把失效广播给其他实例的 L1。

// 业务提交成功后同时失效实体键与集合标签。
await cache.RemoveAsync(productKey, cancellationToken);
await cache.RemoveByTagAsync("catalog:products", cancellationToken);
// 失效异常当前按 best-effort 记录;事件消费者应允许安全重放。

FusionCacheStore 真正支持标签与跨实例广播;MemoryCacheStore.RemoveByTagAsync 当前不提供同等的标签索引语义。单实例测试若只验证 Memory Store,不能证明生产标签失效正确。

失败策略

普通查询通常适合 fail-open:缓存不可用就回源。涉及授权、票据或额度的判断不能套用这一默认值,应使用专门端口或显式的 fail-closed 策略。开启陈旧值回退前,还要确认旧数据不会越过权限、价格或状态边界。

TryGetAsync 默认捕获非取消异常并返回 Miss;只有 FusionCacheStore.FailClosedOnGet=true 才向上抛出。GetOrCreateAsync 则由每个 CachePolicy.HideErrors 决定缓存失败后是否回源。两者不是同一个开关。

AllowStaleOnError 会映射到 FusionCache fail-safe;当前 StaleTtl 被用于 FailSafeThrottleDuration。应以集成测试验证实际陈旧保留窗口,不能仅凭属性名推断 FailSafeMaxDuration 已配置。

健康、指标与故障注入

CacheHealthCheck 先对 L1 执行 set/get/remove。Redis 未注册时报告 Healthy(L1 only);Redis 已注册但 ping 失败时报告 Degraded。该探针验证连接,不验证一次真实跨实例标签广播。

实例 A 缓存值 → 实例 B 更新事实并按标签失效
→ A 的 L1 收到 backplane 通知
→ A 下一次读取回源并得到新值

发布测试还要覆盖 Redis 中断回源风暴、负缓存、标签失效、陈旧数据、批量并发和连接恢复。指标观察 hit/miss/negative/error、factory duration 与 cache latency,并按 area 聚合。

安全与一致性决策

权限、Feature 和关系缓存失效失败可能延长旧授权,因此必须有短 TTL、事件重放与安全测试。一次性票据、幂等占位和额度不要因为 policy 名称看似专用就视为强一致;它们仍需专用原子存储语义。

区域目录、预热与指纹

业务缓存不再只是“各模块私自用 Tag”。Framework 提供统一治理面:

概念作用
CacheAreaKeys对外稳定区域键(HTTP / Host UI 只认这些键)
[BitzCacheArea]生产消费方属性嗅探;AreaKey 必须落在目录内
ICacheAreaCatalog运行时真相源:区域、内部 Tag、是否可预热
ICacheWarmupContributor有限键空间区域的 owner 预热实现
CacheContentFingerprint预热后内容指纹,供 Host 健康展示
Cache:Warmup可选启动预热;默认关闭

默认读路径仍是 Cache-Aside(GetOrCreateAsync / TryGet + Set)。预热是运维增强,不是授权或业务事实源。

当前目录(摘要)

AreaKey可预热说明
settings是设置定义投影
master-data是启用字典组(默认 culture)
translations是启用语言 × office=0
delivery是通知投递渠道合并投影
identity-organization是组织目录;必须配置 TenantIds
identity-users是活跃用户协作资料(每租户有上限);必须 TenantIds
identity-security是锁定/密码策略;必须 TenantIds
workflow否领域口 IWorkflowCache;Host 仅可失效
navigation否菜单树按用户组合,禁止全量预热
authorization否权限/Feature/DataScope/ReBAC 决策,禁止当授权事实预热
chat-presence否在线态实时、键空间无限

多租户预热必须先 ICurrentTenantAccessor.BeginScope,再读 ORM / 写缓存;显式租户键优先 ICacheKeyBuilder.BuildForTenant,避免 Host / Job 依赖 ambient 用户。

启动预热配置

{
"Cache": {
"Warmup": {
"EnabledOnStartup": false,
"AreaKeys": [
"master-data",
"translations",
"settings",
"delivery"
],
"TenantIds": [],
"StartupMode": "FillMissing",
"MaxDegreeOfParallelism": 2,
"AreaTimeout": "00:02:00"
}
}
}
  • EnabledOnStartup 默认 false,避免 SaaS 冷启动打爆数据库。
  • AreaKeys 为空表示所有 SupportsWarmup 区域。
  • 无 TenantIds 时,仅适合平台键区域(settings / master-data / translations / delivery);identity-* 区域会被跳过或拒绝。
  • StartupMode:FillMissing 只补洞;全量重建走 Host full-rebuild(确认令牌 REBUILD)。

完整矩阵、禁止项与验证清单见仓库架构文 docs/architecture/05-cross-cutting/0511-cache-catalog-warmup-fingerprint.md。运维 API 见 Operations 缓存治理。

上线检查

  1. Redis 已配置并通过缓存健康检查;多实例环境不能误用纯内存实现。
  2. 租户键包含可信租户标识,且没有敏感数据。
  3. TTL、负缓存、抖动和回源预算经过压测。
  4. 写路径有对应的键或标签失效。
  5. 监控命中、未命中、负命中、回源、错误和操作耗时。
  6. 新增业务缓存区域已登记 CacheAreaKeys + [BitzCacheArea],有限键空间才注册预热贡献者。
  7. 若开启启动预热:已限定 AreaKeys / TenantIds,并完成 Staging 压测与 Host /operations/cache 验收。

底层类型与注册细节见缓存构建块。

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%