缓存不是一个单一开关。一个响应既有一个由缓存作出的新鲜度决策,也有与源站之间的校验对话。把这两种想法混在一起,会得到陈旧内容或多余请求。
先选变更模型
对于发布之后可能变化的 HTML 文档,Cache-Control: no-cache 通常比 no-store 更接近正确起点。no-cache 允许存储,但要求缓存复用前先校验;no-store 则要求缓存完全不存储。当你想要条件请求与廉价的 304 Not Modified 时,这个区别很关键。
对类似 app.4f2c.js 这种带指纹的资源,URL 在字节变化时也随之改变,此时一个较长的新鲜度生命周期是合理的:
Cache-Control: public, max-age=31536000, immutable
但对一个内容可能被原地替换的稳定 URL,该策略不安全。如果 URL 稳定,应优先使用较短的生命周期,并搭配实体标签(ETag)或有意义的 Last-Modified 值。
除了新鲜期,还要决定谁可以存储响应。如果共享缓存不能保存面向特定用户的内容,应使用不带参数的 private 指令;浏览器的私有缓存仍可存储。单独使用 no-cache 不提供这项限制。上面的 public 示例适用于可以在用户之间共享的资源,不是个性化响应的默认策略。
同时检查两条路径
发一次普通请求,再发一次条件请求。各服务器的具体标志可能不同,但你想要的证据是稳定的:
curl -sS -D - -o /dev/null https://www.example.test/
curl -sS -D - -o /dev/null \
-H 'If-None-Match: "copy-a-real-etag-from-the-first-response"' \
https://www.example.test/
不要在 runbook 里粘贴编造的验证器。从第一次响应里复制该值,或使用测试夹具。对这里的条件 GET,If-None-Match 匹配时可以通过弱比较返回 304:它允许语义等价,并不一定保证字节完全相同。验证器变化时通常返回新的 200 响应。保留 ETag 的双引号与可能存在的 W/ 前缀。
让中间层可见
Age、Vary 与 cache-control 指令说明了为什么两个客户端可能得到不同结果。如果响应随 Accept-Encoding、语言或其他请求字段变化,该维度应进入 Vary;否则共享缓存可能复用错误的表示。
有意义的发布检查不是"扫描器找到了缓存头",而是"该头与替换策略匹配,且条件请求的行为符合文档说明"。
immutable 只在声明的新鲜期内生效,并非永久禁止重校验。版本化 URL 的内容应保持不变,只要旧 HTML 仍可能引用旧资源,就应让它们继续可访问。今天修改响应头不会追溯缩短既有缓存响应的寿命;缓存策略出错时,可能需要新 URL 或缓存系统明确支持的清除操作。
来源
- RFC 9111 — HTTP Caching,第 4–5 节,已核验 2026-10-15。
- RFC 9110 — HTTP Semantics,第 8 与 13 节,已核验 2026-10-15。
- MDN:
Cache-Control,指令语义与默认值,已核验 2026-10-15。 - RFC 8246 — 不可变响应,immutable 仅在新鲜期内生效,已核验 2026-10-15。