缓存策略深度解析
缓存不是“开关”,而是一套分层一致性工程。
做得好,缓存能同时提升:
- TTFB / LCP(更快)
- 稳定性(抗突发流量、抗抖动)
- 成本(更少的源站请求)
做得不好,缓存会制造更可怕的问题:
- 用户看到过期数据,且你无法复现
- A 用户的内容被 B 用户读到(严重安全事故)
- 缓存命中率很低,反而更慢、更贵
这篇文章的目标是:让你用一套统一的心智模型,设计并落地“可控、可观测、可回滚”的缓存系统。并以 Next.js App Router 为例,把框架缓存语义纳入整套设计。
1. 先建立缓存的“分层模型”
真实系统的缓存几乎总是分层的:
- 浏览器缓存(Browser Cache)
- CDN/边缘缓存(Edge Cache)
- 应用侧缓存(App Cache:SSR/RSC payload、fetch cache)
- 数据侧缓存(Redis/DB buffer)
你需要明确:
- 每一层缓存什么?(HTML / JSON / 静态资源 / RSC payload)
- 每一层的 TTL 多久?
- 写操作发生后,哪几层需要失效?
1.1 缓存的两类问题:性能与一致性
- 性能问题:命中率、压缩、复用、减少源站
- 一致性问题:失效、隔离、权限、版本
很多团队“缓存做不好”,本质是只解决了性能,忽略了一致性。
2. HTTP 缓存基础:Cache-Control 的正确用法
HTTP 缓存是地基。你至少要掌握这些指令:
public/privatemax-age(强缓存 TTL)s-maxage(CDN 侧 TTL)stale-while-revalidate(过期可先用旧的,再后台更新)no-store(不要存储)
2.1 一个常用的 CDN 缓存头模板
对“公共页面(无用户态)”:
Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=86400
含义:
- 浏览器不强缓存(max-age=0)
- CDN 缓存 10 分钟
- CDN 过期后仍可先用旧内容 1 天,同时异步更新
2.2 用户态页面:默认 private 或 no-store
一旦响应与用户身份有关:
- 默认:
Cache-Control: private, no-store - 必须缓存时:缓存 key 必须包含用户维度(例如 userId/session)且只在应用侧缓存
结论:宁可慢一点,也不要把用户态响应交给 CDN 缓存。
3. CDN 缓存:命中率与 Key 设计
CDN 缓存的核心是:
- 你希望命中(hit),就要让相同内容的请求尽可能“看起来一样”
- 你希望隔离(safe),就要让不同内容的请求“必须不一样”
这两个目标天然冲突。
3.1 Cache Key 里哪些维度必须考虑?
最常见维度:
hostpathquery(是否纳入 key?)accept-encoding(gzip/br)accept-language(多语言站点)device(移动/桌面,如果你做了 UA vary)
反例:把“跟内容无关的 query”放进 key(例如 utm_*),会把命中率直接打碎。
3.2 解决 UTM 污染:规范化 URL
建议:
- 进入应用时把
utm_*归一化并 301 到干净 URL - 或在 CDN 层将
utm_*从 cache key 里剔除
4. 应用侧缓存:Next.js App Router 的缓存语义
App Router 的“缓存”不是一个地方,而是一套语义:
- fetch 结果缓存
- RSC payload 缓存
- route segment 缓存
- revalidate 机制(path/tag)
4.1 你必须区分:静态、动态、按需再验证
在 App Router 体系里,一个页面/段可以是:
- 静态(长 TTL)
- 动态(每次都算)
- ISR/按需再验证(写操作后失效)
不要把“页面慢”简单归因给 SSR;很多时候是缓存策略没设计好。
4.2 两个最重要的失效 API:revalidatePath / revalidateTag
revalidatePath('/xxx'):按路由失效(粗粒度)revalidateTag('tag'):按数据域失效(细粒度)
经验法则:
- 数据只影响单页:用 path
- 数据影响多页(列表、详情、侧栏统计):用 tag
5. 写操作后的失效策略:从“拍脑袋”到“可证明”
写操作发生后,你要回答:
- 哪些页面会受影响?
- 哪些数据域会受影响?
- 这些受影响对象是否有统一 tag?
5.1 建议采用“域驱动 tag”
例子:电商商品
product:{id}product:listinventory:{id}
当你更新库存时:
- 失效
inventory:{id} - 可能也失效
product:{id}(如果详情页展示库存)
这样你可以把失效做成一套规则,而不是散落在业务代码里。
5.2 不要用“全站失效”当默认
revalidatePath('/')、清 CDN 全站缓存,短期看省事,长期一定出问题:
- 命中率暴跌
- 源站被打穿
- 回归定位困难
6. 缓存与权限:最危险的组合
6.1 三条硬规则
- 用户态响应不要进 CDN
- 缓存 key 必须包含权限维度(userId/role)
- SSR 页面要明确区分“公共数据”与“私有数据”
6.2 常见事故模式
- 详情页同时展示公共内容 + 用户私有信息(例如是否收藏)
- 你把整页缓存了
- 结果:用户私有信息被缓存并泄漏
正确做法:
- 公共内容可缓存
- 私有信息走客户端请求或单独的私有 API(不缓存)
7. 可观测性:让缓存变成“可量化系统”
你至少需要这些指标:
- CDN 命中率(hit/miss)
- 源站 QPS
- TTFB 分布(p50/p95)
- revalidate 调用量与耗时
- 失效失败率
日志建议:
- 给每个请求注入
cacheStatus(HIT/MISS/BYPASS) - 记录
cacheKey的关键维度(注意脱敏)
8. 一份可直接执行的缓存策略模板
8.1 公共内容页(文章、落地页)
- CDN:
s-maxage=600, stale-while-revalidate=86400 - App:允许 fetch cache,tag 以
content:*组织 - 写操作:发布文章时
revalidateTag('content:article')
8.2 列表 + 详情(强一致性要求不高)
- 详情:tag
entity:{id} - 列表:tag
entity:list - 更新:失效
entity:{id}+entity:list
8.3 强一致性(库存、余额)
- 默认不缓存
- 或只做极短 TTL(1~3s)
- 必须有幂等与容错
9. 检查清单(上线前必过)
- 哪些路由是 public/private?是否明确
- CDN cache key 是否会被无关 query 污染
- 用户态响应是否有 no-store
- 写操作是否有明确的 tag/path 失效策略
- 是否有命中率与回源监控
- 回滚策略是什么(关缓存、降级)
总结
缓存策略的本质是:
- 把“快”变成可控的工程系统
- 把“一致性”变成可证明的规则
- 把“失效”变成可观测、可回滚的流程


