缓存不是一个单一开关。一个响应既有一个由缓存作出的新鲜度决策,也有与源站之间的校验对话。把这两种想法混在一起,会得到陈旧内容或多余请求。

存储与复用矩阵之后,分别说明稳定 URL 与带指纹 URL 的策略。条件 GET 在 If-None-Match 中发送真实 ETag:弱匹配可返回 304,验证器变化时通常返回新的 200。语义等价不保证字节一致。Age、Vary、仅在新鲜期内生效的 immutable 与既有缓存寿命,共同约束复用。
存储、新鲜度与校验是不同问题。根据 URL 的变更方式选择策略,完整保留验证器,并检查共享缓存的变体。

先选变更模型

对于发布之后可能变化的 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 或缓存系统明确支持的清除操作。

来源