HTTP 缓存与 CDN 加速:把“能不能复用”写进响应头
HTTP 缓存与 CDN 加速的核心,是把“响应能否复用、由谁复用、复用多久、过期后如何验证”写成可判定的响应头,再由 CDN 在共享缓存层执行。文章区分浏览器私有缓存与 CDN 共享缓存的职责,说明 Cache-Control 各指令、ETag 与 Vary 的组合边界,并给出 CDN 侧缓存键、查询参数与清除三个配置点,附一段示意响应头与验证方法。
- 浏览器缓存与 CDN 缓存是两层:max-age 面向前者,s-maxage 面向后者,写混会把私有内容放进共享缓存。
- no-cache 不等于 no-store:前者要求每次使用前验证,后者才是不落盘。
- 缓存键设计决定命中率,也决定误命中风险;带用户身份的响应默认不应进入共享缓存。
HTTP 缓存与 CDN 加速要回答的其实只有三个问题:这份响应能不能被复用、能被谁复用多久、过期之后靠什么判断还能不能用。答案写在响应头里,CDN 只是把这些头执行到共享缓存这一层。头写错了,CDN 不会纠正,只会把错误更快地放大给更多用户。本文只讨论这一层通用机制;涉及具体 CDN 厂商的配置项名称与调试字段,需以其官方文档为准,本文未做联网核验。
两层缓存,各自看不同的指令#
同一份响应会经过两类缓存:浏览器里的私有缓存,和 CDN、反向代理这类共享缓存。HTTP 缓存规范(RFC 9111,具体编号与章节以 IETF 发布原文为准)为两者准备了不同指令:max-age 约束私有缓存,s-maxage 只约束共享缓存并会覆盖 max-age。因此常见的“浏览器缓存 1 分钟、CDN 缓存 10 分钟”并不需要两份配置,一条头就能表达。
与此配套的是可见性指令:public 允许共享缓存存储,private 表示只允许浏览器存。默认行为在这点上容易出错——带 Authorization 或 Set-Cookie 的响应,是否可进入共享缓存取决于具体缓存实现与指令组合,不能依赖“大概不会缓存”的假设。
三个最容易写反的指令#
- no-cache 与 no-store:
no-cache是“可以存,但每次使用前必须向源站验证”;no-store才是“不要存”。把no-cache当成“不缓存”,会得到一份仍然占用缓存空间、且每次都要回源的响应。 - ETag / Last-Modified:它们是验证器。
no-cache配合 ETag,命中时源站可以返回 304,省掉响应体;没有验证器的no-cache,等于每次都完整回源。 - Vary:告诉缓存“这份响应随哪个请求头变化”。压缩编码、语言、甚至
Accept差异都会造成同一 URL 对应多份内容。漏写Vary的后果不是命中率下降,而是把 A 版本的响应发给 B 场景。
CDN 侧真正要决策的三个点#
缓存键
缓存键决定什么算“同一份内容”。URL 全量入键最安全,命中率最低;剥掉无关查询参数能提高命中率,但需要确认被剥掉的参数确实不影响响应。按用户、按设备、按灰度分组的内容,通常应通过 private 或独立路径处理,而不是靠缓存键硬扛。
查询参数与路径
静态资源的版本化命名(把版本写进文件名或路径)比依赖清除接口更可控:新版本是新 URL,没有旧缓存需要等待失效。反之,就地覆盖同一 URL 的内容,就必须规划清除流程与生效时间的预期。
清除与回源保护
清除操作是异步生效的,不同厂商的生效范围与延迟需要实测确认——本文未执行任何清除操作,不给出具体数值。发布后需要立即生效的内容,应优先考虑改 URL,而不是把正确性押在清除速度上。
一段示意响应头#
下面的头仅用于说明指令组合关系,未在任何真实服务上执行;其中 "demo-v1" 是占位值,不是真实 ETag。
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=30
ETag: "demo-v1"
Vary: Accept-Encoding
读法:浏览器最多用 60 秒;CDN 最多用 600 秒;过期后一段时间内可先返回旧内容同时后台刷新(stale-while-revalidate 属于扩展指令,是否被某家 CDN 支持需查其文档,此处待核实)。对包含用户隐私的响应,则改用一个不含 public 的组合,例如 Cache-Control: no-store。
怎么验证写对了#
先看头,再看命中。curl -I 能读到响应里的 Cache-Control、ETag 与 Age;Age 大于 0 说明这份响应来自缓存而非刚生成。至于“是否命中 CDN”,多数厂商会附加自定义调试头,字段名各不相同,需要以对应平台文档为准,本文不列举具体字段名。更稳妥的验证方式是对同一 URL 连续请求两次并对比 Age 的变化,而不是依赖某个头名的存在。
边界:哪些内容不该走这条路#
按登录态变化的响应、一次性令牌、支付结果页这类内容,问题不在“缓存多久”,而在“是否允许共享”。这类接口应显式声明不可存储,并在服务端确认响应里没有意外的 Set-Cookie。反过来,长期不变的静态资源适合用较长的 max-age 加版本化 URL,把失效问题转成发布问题。判断标准始终是:这份内容如果被另一个用户看到,会不会造成错误或泄露。