BodySense Redis 第 8 课:Cache Aside 与 Cache Correctness
以 BodySense HealthWorkspace 为候选案例,学习 Cache Aside、失效策略、穿透/击穿/雪崩、stampede、stale fill race 与 Redis failure fallback。
[!info] related notes
- 课程 MOC:BodySense Redis 从零到一课程 MOC
- 前置课程:第 7 课
- 对应知识笔记:缓存策略与一致性、Go 缓存策略、Redis
- 项目:BodySense 性能优化策略
BodySense Redis 第 8 课:Cache Aside 与 Cache Correctness
[!abstract] 学习目标 学会把“加一个 Redis Cache”看成一致性与故障语义设计,而不是只写 GET/SET。
先从零解释:Cache 到底是什么
Cache 不是“另一个更快的数据库”这么简单。
更准确的定义是:
Cache 保存一份可以从别处重新得到的派生副本,用额外空间和一致性复杂度换取更低的读取延迟或更少的下游负载。
这句话有三个关键点。
1. Cache 是副本,不是唯一事实
例如:
PostgreSQL user row
= truth
Redis user cache
= copy
Redis 丢失:
cache miss
→ 回 PostgreSQL
→ 正确性仍然存在
如果 Redis 丢失后整个业务事实也一起丢了,那它就不再只是 Cache。
2. Cache 不是免费性能
你得到:
更少 DB query
更低 read latency
更少重复计算
同时付出:
失效策略
并发一致性
额外内存
故障 fallback
监控
隐私删除
预热 / 冷启动
所以“Redis 很快”不是添加 Cache 的充分理由。
3. 先测 Baseline,再缓存
假设 endpoint p95 = 120ms:
如果数据库只占 8ms,真正慢的是外部 AI 或网络,给数据库结果加 Redis 可能几乎没有用户收益。
因此缓存前先知道:
当前 p50 / p95
SQL query count
DB time
serialization time
payload size
read/write ratio
hot key distribution
Hit、Miss、Fill、Invalidate 四个基础词
Cache Hit
Redis 有
→ 直接返回
Cache Miss
Redis 没有
→ 查询 authority
Cache Fill
查到 authority
→ 写回 Redis
Cache Invalidation
authority 已变化
→ 旧 cache 不能继续被相信
→ 删除/更新旧副本
缓存正确性的大多数问题,其实都围绕:
什么时候这个副本还可信?什么时候必须失效?
Cache Aside:应用自己负责“先查缓存,Miss 再回源”
Cache Aside 也常叫 lazy loading pattern。
核心是:
Cache 不主动拦在数据库前面;Application 自己显式决定先查 Cache,Miss 后再查数据库。
读路径:
Request
↓
Application
↓ Redis GET
├─ hit
│ └─ return cached value
│
└─ miss
↓ query PostgreSQL
↓ build response/projection
↓ SET Redis + TTL
└─ return
写路径常见:
Application
↓
PostgreSQL transaction COMMIT
↓
invalidate related Redis key
为什么通常强调 after commit?
如果数据库还没成功就先把 Cache 更新成新值:
Redis = new
PostgreSQL transaction = rollback
Cache 就会出现一个数据库从未真正接受过的状态。
Cache Aside 和 Read-through 不完全一样
可以先这样区分:
Cache Aside
→ Application 知道 cache miss 后怎么回源
Read-through cache
→ Cache layer / library 本身负责 miss loader
Write-through / Write-behind 又是什么
Write-through:
write cache
→ cache 同步写 backing store
Write-behind:
先写 cache
→ 异步批量落 backing store
它们会改变谁负责 durable write,因此风险和 Cache Aside 不同。
BodySense 目前课程优先讨论 Cache Aside,因为它最容易保持:
PostgreSQL = durable authority
Redis = read optimization
为什么不直接把 Cache 当 Truth
Cache:
Redis unavailable
→ fallback DB
与 Auth authority:
Redis unavailable
→ 不能安全判断授权
→ fail closed
完全不同。
这就是“同样使用 Redis,failure semantics 不能复制粘贴”的典型例子。
BodySense HealthWorkspace 候选
HealthWorkspace 可能聚合:
Profile
latest Consultation
BodyState snapshot
Facts / Observations
latest Diagnosis
Treatment
Training Plan
Outcomes
Trends
Capabilities
...
这类 projection 可能读很多表,因此是缓存候选。
但第一步不是直接缓存,而是 baseline:
SQL query count
p50 / p95
payload size
mutation frequency
concurrent reads
没有证据就加缓存,会增加复杂度而不一定改善 UX。
Cache Invalidation
假设:
workspace:u1
→ cached projection
用户更新 BodyState。
如果只写数据库,不删 cache:
PostgreSQL = new
Redis = old
↓
用户继续看到旧状态
所以必须明确所有会改变 projection 的 mutation:
profile
body state
facts / observations
diagnosis
treatment
training
outcome
consultation state
...
然后:
DB transaction commit
→ invalidate workspace cache
[!warning] 短 TTL 不能替代正确的 invalidation ownership “反正 30 秒就过期”只是第二层保护,不应该掩盖遗漏的写路径。
Cache Penetration:请求的东西本来就不存在
Penetration 这里指请求不断穿透 Cache 去打 authority。
例如攻击者持续请求:
user:-1
user:-2
user:-3
...
每个 ID 在 PostgreSQL 里也不存在:
Redis miss
→ DB miss
→ 什么都不缓存
→ 下次仍 Redis miss + DB miss
于是“Cache 根本挡不住不存在的数据请求”。
常见治理按复杂度从低到高:
参数/权限校验
很多非法 key 根本不应该进入数据库路径。
Negative Cache
missing:user:xyz = true
TTL = 30s
短期记住“确实不存在”。
Bloom Filter
当 keyspace 巨大、无效查询很多时,可以用概率数据结构先判断“很可能不存在”。
但 Bloom Filter 有 false positive 等语义,不能因为听起来高级就默认引入。
Cache Stampede / 热点击穿:同一个 Miss 被并发放大
假设一个热点 Key:
workspace:u1
平时:
1000 requests
→ Redis hit
→ PostgreSQL 几乎没压力
刚好 TTL 到期:
100 concurrent requests
→ 全部同时 miss
→ 100 次 PostgreSQL rebuild
这就是 stampede。
问题不只是“Cache Miss”,而是:
同一个昂贵重建被并发重复做了很多次。
singleflight
在一个进程里可以把相同 key 的并发 rebuild 合并:
100 callers
→ 1 caller rebuilds
→ 99 callers wait/share result
Distributed lock
多实例时也可以围绕 rebuild key 做短锁,但要考虑锁失败、过期和 owner;不能为了缓存随便引入一套复杂分布式锁。
stale-while-revalidate
允许短时间返回旧但可接受的数据:
serve stale
+
background rebuild
这只适用于业务允许 bounded staleness 的数据。
Cache Avalanche / 雪崩:很多 Key 同时失效
Stampede 常讨论一个热点 Key。
Avalanche 讨论:
大量不同 Key
→ 在相近时刻过期/不可用
→ 大量请求同时回源
例如批量预热时统一:
TTL = exactly 3600s
一小时后:
成千上万 Key 同时 expire
→ PostgreSQL load spike
常见手段:
TTL Jitter
base TTL 3600
+ random 0~300
把过期时间打散。
Capacity protection
即使 Cache 全失效,也不能允许无限并发直接把 DB 打死。
Progressive warm-up
按热点/批次预热,而不是瞬间加载全部。
Stale Fill Race
一个很容易忽略的竞态:
A: cache miss → 开始读取旧 DB state
B: mutation → DB commit → invalidate cache
A: 旧查询完成 → SET old value
结果:
B 明明已经 invalidated
但 A 又把旧值填回 cache
这就是 stale fill race。
可能治理:
- versioned key;
- revision/version check;
- after-commit ordering;
- 短 TTL 作为兜底;
- 对特定热点引入 singleflight。
Negative Cache 的边界
缓存“不存在”:
missing:user:xyz = true
TTL = short
可以防 penetration。
但如果对象随后被创建,需要确保:
create success
→ invalidate negative cache
否则产生短期假“不存在”。
Privacy Erasure
BodySense 是健康数据产品。
用户执行 erasure 后:
PostgreSQL data deleted
还必须考虑:
Session
Refresh family
Workspace cache
RAG/user scoped cache
future Redis state
因此 Cache Key Registry 很重要,不能靠全库扫描 userID 猜哪里还有数据。
Redis Down 时 Cache 怎么办
Cache 的典型策略:
Redis error
→ log / metric
→ fallback durable path
→ return correct result
但要防止:
Redis outage
→ every request hits DB
→ DB overload
所以还要有:
- timeout;
- circuit breaker / bounded retry;
- concurrency control;
- DB capacity protection。
Prediction
[!question]
- 为什么 Cache Aside 的 Redis miss 与 Auth Session miss 语义完全不同?
- “TTL 设短一点”为什么不能代替 invalidation audit?
- 热点 Key 一过期,100 个请求同时查 DB,叫什么?
- mutation 已 invalidation,但并发旧读又把旧值写回,叫什么?
- Redis Cache 挂掉时,为什么通常应该 fallback,而认证 Redis 挂掉时却可能 fail closed?
[!success]- Answer / 解析 1. Cache Aside 的 Redis miss 与 Auth Session miss 为什么完全不同?
Cache miss 表示“高速副本不存在”,真正事实仍在 PostgreSQL,可以回源并重新填充;Session miss 则表示在线授权 authority 本身不存在,通常就是当前 Session 已失效。前者是性能事件,后者是业务/安全事实。2. 为什么短 TTL 不能代替 invalidation audit?
TTL 只能限制“错误最多持续多久”,不能保证 mutation 后立即一致,也不能满足隐私擦除、权限变化等需要立刻生效的语义;并发 stale fill 甚至可能在刚 invalidation 后又把旧数据写回。正确做法仍是梳理所有 mutation ownership,在 commit 后明确失效相关 Cache。3. 热点 Key 到期后 100 个请求同时回源叫什么?
Cache stampede / cache breakdown(常称“缓存击穿”)。大量并发请求同时 miss,把原本由一个缓存 Key 承担的流量瞬间压到数据库或下游服务。4. mutation 已 invalidation,但旧读把旧值重新写回叫什么?
Stale fill race。它说明“先删缓存”本身还不保证并发正确性,可能需要 version/revision check、singleflight、短 TTL 等机制。5. Cache Redis 挂掉通常 fallback,而 Auth Redis 挂掉可能 fail closed,为什么?
因为 Cache 的 authoritative source 还在,Redis 只是优化层;Auth Session 的 Redis 状态本身就是在线授权 authority。前者丢失可以回源保持正确,后者不可用时无法证明当前授权是否仍有效,因此不能安全绕过。
Lab / Design Exercise
给 HealthWorkspace 设计六条路径:
1. cache hit
2. cache miss
3. concurrent miss
4. mutation invalidation
5. Redis down fallback
6. privacy erasure purge
每条写清:
- source of truth;
- key;
- TTL;
- failure policy;
- metrics;
- recovery。
L4 Gate
能够回答:
这次缓存究竟优化了哪个用户可感知指标?如果 Redis 完全删除,系统还能保持正确吗?
如果这两个问题答不清,就不该进入生产缓存。