← [ BACK_TO_NOTES ]

BodySense Redis 第 8 课:Cache Aside 与 Cache Correctness

以 BodySense HealthWorkspace 为候选案例,学习 Cache Aside、失效策略、穿透/击穿/雪崩、stampede、stale fill race 与 Redis failure fallback。

#course #growing #backend #redis #bodysense

[!info] related notes

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]

  1. 为什么 Cache Aside 的 Redis miss 与 Auth Session miss 语义完全不同?
  2. “TTL 设短一点”为什么不能代替 invalidation audit?
  3. 热点 Key 一过期,100 个请求同时查 DB,叫什么?
  4. mutation 已 invalidation,但并发旧读又把旧值写回,叫什么?
  5. 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 完全删除,系统还能保持正确吗?

如果这两个问题答不清,就不该进入生产缓存。

创建于 2026/9/18 更新于 2026/9/18
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
关闭浮层 / 取消命令;输入框内清空文本