BitzOrcas 的应用缓存不是一层 IMemoryCache 包装。生产组合根通过 AddBitzOrcasCaching() 注册 FusionCache:进程内缓存是 L1;配置 Redis 后,Redis 同时承担 L2 与多实例失效通知。业务代码通过 ICacheStore 使用它,不直接依赖 FusionCache 或 Redis。
运行结构
Application → ICacheStore → FusionCacheStore → L1 memory └──────→ L2 Redis + 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 | 无额外段 | 正常 |
| Tenant | tenant-{TenantId} | 抛出异常 |
| User | tenant-{TenantId}:user-{UserId} | 抛出异常 |
| Client | tenant-{TenantId}:client-{ClientId} | 抛出异常 |
| Request | req-{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只补洞;全量重建走 Hostfull-rebuild(确认令牌REBUILD)。
完整矩阵、禁止项与验证清单见仓库架构文 docs/architecture/05-cross-cutting/0511-cache-catalog-warmup-fingerprint.md。运维 API 见 Operations 缓存治理。
上线检查
- Redis 已配置并通过缓存健康检查;多实例环境不能误用纯内存实现。
- 租户键包含可信租户标识,且没有敏感数据。
- TTL、负缓存、抖动和回源预算经过压测。
- 写路径有对应的键或标签失效。
- 监控命中、未命中、负命中、回源、错误和操作耗时。
- 新增业务缓存区域已登记
CacheAreaKeys+[BitzCacheArea],有限键空间才注册预热贡献者。 - 若开启启动预热:已限定 AreaKeys / TenantIds,并完成 Staging 压测与 Host
/operations/cache验收。
底层类型与注册细节见缓存构建块。