← [ BACK_TO_NOTES ]

缓存策略与一致性

从 source of truth 出发组织 Cache-aside、TTL、失效、并发回源、穿透、击穿和雪崩,解释缓存如何用可控的不一致窗口换取延迟和容量。

#synthesis #growing #backend

[!info] related notes

缓存策略与一致性

[!abstract] 核心结论 缓存保存的是为延迟和容量服务的派生副本。设计的第一问题不是“缓存多久”,而是明确 Source of Truth、允许读旧多久、写入与失效失败时如何收敛,以及缓存不可用时怎样保护事实层。

基本角色

Source of Truth
→ owns business facts and constraints

Cache
→ derived copy for faster reads / reduced load

除非系统明确按 Cache-primary 模型设计,否则缓存丢失后应该可以从事实层重建。

Cache-aside

读路径

GET key
├─ hit  → return cached value
└─ miss → load source of truth
           → populate cache
           → return

写路径

常见做法:

update database transaction
→ after commit, invalidate cache

无并发旧回填且读取源足够新时,下一次 miss 会重新加载;实际还需考虑下面的竞争与数据库副本延迟。

[!warning] 不要在事务提交前更新缓存 如果数据库最终回滚,其他请求可能已经读到从未真正提交的值。

为什么常“删缓存”而不是“直接更新缓存”

直接更新需要同时维护两份事实,并可能出现并发覆盖:

T1 reads old value
T2 updates DB to v2 and cache to v2
T1 later updates cache with stale v1

删除缓存让下一次读回到事实层重新构建,逻辑更简单。但删除也不是天然无竞争,仍需分析故障窗口。

Cache-aside 的两个典型竞争

先删 Cache,再写 DB

T1 delete cache
T2 cache miss → reads old DB → writes old cache
T1 commits new DB

结果:Cache 长时间保留旧值。因此通常不优先先删后写。

先写 DB,再删 Cache

T1 cache miss → reads old DB
T2 commits new DB → deletes cache
T1 writes old value into cache

该窗口通常较窄但仍存在。可根据一致性要求采用:

  • 短 TTL;
  • Versioned Value / Compare Version;
  • 延迟再次失效(不是通用万能方案);
  • CDC / Event-driven Invalidation;
  • 协调写入与回填的版本/锁协议;仅改名为 Write-through 或 Read-through 不会自动消除竞争;
  • 对关键读绕过缓存或等待版本。

TTL 是一致性预算

TTL 约束单次缓存条目的有效期,并影响:

  • 该条目被自然淘汰出读路径的时机;
  • 清理策略下的 Memory 占用(惰性过期不等于释放内存);
  • 回源频率;
  • 热点失效风险;
  • 故障后自动收敛速度。
short TTL → fresher, more backend load
long TTL  → fewer misses, longer stale window

TTL 从回填起算,而源数据可能已经落后;并发旧回填、多级缓存与 stale-while-revalidate 都可能延长从事实更新到读新的时间。要证明最大旧数据窗口,需一起约束源版本、回填、失效与过期后返回旧值的策略。

不要为所有 Key 使用同一 TTL。按数据变化率、读取频率、重建成本和业务风险分层,并加入 Jitter 避免同时过期。

三类高频故障

故障根因常见治理
穿透查询不存在对象,缓存无法形成有效命中参数校验、Negative Cache、Bloom Filter
击穿 / Stampede热点 Key 失效时大量请求并发回源Singleflight、Mutex、逻辑过期、预热
雪崩大量 Key / 节点同时失效TTL Jitter、多级缓存、限流、降级、容量保护

三者根因不同,不要只回答“加锁”。

Negative Cache 与 Bloom Filter

Negative Cache

对确定不存在的结果使用短 TTL:

user:999 → NOT_FOUND (30s)

必须避免把临时查询失败缓存成“永久不存在”。

Bloom Filter

对正确维护的已插入集合,标准 Bloom Filter 的否定结果表示「一定不在该集合」,肯定结果只表示「可能存在」。它有假阳性,没有假阴性;肯定结果仍需查事实层。若过滤器尚未同步新数据,否定结果不能代表数据库当前不存在;必须处理更新顺序与重建窗口。

Stale-while-revalidate / 逻辑过期

cache entry contains value + logical expiry
→ expired reader may receive stale value
→ one worker refreshes in background

适合可接受短时旧数据、但不能让热点请求同时回源的场景。代价是必须明确最大 Staleness、Refresh Lock、失败重试和过期值是否还能继续服务。

多级缓存

L1 process cache
→ L2 Redis
→ Database

优点:降低网络延迟和 Redis 压力。

代价:

  • 每个实例都有旧副本;
  • 失效广播和版本更复杂;
  • 发布后 Schema / Serialization 兼容;
  • L1 容量和 GC 压力;
  • 排障时难判断命中哪一层。

常用策略:L1 短 TTL、L2 稍长 TTL,加版本或 Invalidation Event。

Event / CDC 驱动失效

DB commit
→ Outbox / CDC event
→ invalidate or update cache

可以缩短旧值窗口并跨实例同步,但消息会延迟或重复,因此:

  • Invalidation 必须幂等;
  • Event 带 Entity Version;
  • Cache Value 不能被旧事件覆盖;
  • TTL 可让旧条目自然失效,但仍需确保回源最终变新、没有无限旧回填;它单独不证明端到端收敛时限。

缓存故障时的选择

Fail Open

缓存错误后直读数据库。适合低流量、数据库有余量的场景,但必须限流和合并请求,避免 Cache Outage 变成 DB Outage。

Fail Closed / Degrade

拒绝、返回旧值或关闭非关键功能是不同降级策略。数据库无法承受全量回源时可按业务选择;涉及权限、余额等安全或强一致判断时,返回旧值未必安全,应按该接口的失败合同决定。

策略应按接口定义,不能统一“Redis 挂了就查 DB”。

观测重点

hit / miss rate by cache layer
load / fill / invalidate latency
backend QPS caused by miss
stale read / version conflict
hot key / big key
expired / evicted keys
singleflight shared calls
refresh success / age of cached value
cache error and fallback rate

命中率高不等于缓存健康:可能命中大量过期或错误数据。

设计检查表

  • Source of Truth 和缓存可重建性明确。
  • 写入、事务提交与 Invalidation 顺序明确。
  • 结合源数据、回填、TTL 与过期后读旧策略,验证业务 Staleness Budget,并按需要加入 Jitter。
  • 穿透、击穿、雪崩分别有治理策略。
  • 多级缓存有版本、失效和容量边界。
  • 缓存故障不会无保护地打垮数据库。
  • Invalidation Event 幂等并拒绝旧版本。
  • 指标包含 Freshness 和 Backend Load,而不只看 Hit Rate。
创建于 2026/9/4 更新于 2026/10/6
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本