六个指令与它们实际的意思

HTTP 缓存的规范是 RFC 9111,里面每个指令的语意都有明确定义。问题在于其中几个的名字与直觉不符,而多数人是从别人的配置文件复制过来、没查过规范。

最关键的区分是存储与使用:缓存可以存储一份响应,但在使用它之前可能需要先验证。no-cache 管的是后者,no-store 管的是前者——名字容易混淆,行为完全不同。

指令 实际语意 常见误解
no-cache 可以存储,但每次使用前必须向源站验证 被当成「不要缓存」——它不是
no-store 完全不得存储,包含标头在内 这才是真正的「不要缓存」
max-age=N N 秒内视为新鲜,对所有缓存生效 以为它也管 CDN 就够了
s-maxage=N 只对共享缓存生效,并覆盖 max-age 与 Expires 不知道它会盖掉 max-age
must-revalidate 过期后未经验证不得供应,离线时须回错误 以为过期后还能先挡一下
private 只允许浏览器存储,共享缓存不得存储 与 no-store 混用,结果过度保守
六个常用的 Cache-Control 指令与它们在 RFC 9111 中的实际语意。

下表列出六个最常用的指令。第一行就是那个被误解得最严重的。

这个误解造成的三种实际后果

名称误导不只是学术问题,以下三种都会实际发生:

  1. 用 no-cache 保护敏感数据是无效的 想让账单页面、个人资料不被留下痕迹,设了 no-cache 就以为安全了。但依规范,缓存仍然可以把整份响应写进磁盘,只是下次使用前要验证。共用电脑上的浏览器缓存、企业的代理服务器,都可能留着那份内容。要真正不留存,必须用 no-store。
  2. 一律用 no-store 会赔掉性能 知道 no-store 比较保险之后,有些人干脆全站都设。结果是每一次请求都要完整回源,CDN 形同虚设,静态资源也不再被浏览器重用。正确做法是分清楚哪些响应真的含个人数据——通常只有登录后的动态页面——其余照常缓存。
  3. 不知道 s-maxage 导致 CDN 与浏览器绑在一起 max-age 同时作用于浏览器与 CDN,所以想让 CDN 缓存久一点、浏览器短一点,用单一指令做不到。s-maxage 就是为此存在:它只对共享缓存生效,而且会覆盖 max-age。设定 max-age=60, s-maxage=86400 的意思是浏览器一分钟、CDN 一天——这是 CDN 调优最基本也最常被忽略的工具。

Vary:决定缓存键的那个标头

缓存默认以网址作为键。但如果同一个网址会依请求标头返回不同内容(例如依 Accept-Encoding 给压缩或未压缩版本),缓存必须知道这件事,否则会把压缩版回给不支持的客户端。Vary 就是用来声明「这些请求标头会影响响应内容」。

依规范,缓存在使用已存储的响应之前,Vary 列出的每一个请求标头都必须与当初那次请求相符,否则不得直接使用。所以 Vary 列得越多,缓存命中率越低——每一个被列入的标头都会让缓存键再细分一次。

如果您的 CDN 命中率低得不合理,Vary 是第一个该检查的地方。这个问题不会产生任何错误信息,只会安静地让每一次请求都回源。

过期之后:ETag 与条件式请求

缓存过期不代表内容变了。这时候缓存可以发出条件式请求:带上先前拿到的 ETag(内容指纹)或 Last-Modified(修改时间),问源站「还是这一份吗?」。如果没变,源站回 304 Not Modified,只有标头没有内容。

这就是 no-cache 真正的用途——它强制每次都走这个验证流程,但保留了 304 的快速路径。对于「必须是最新、但通常不会变」的内容(例如 HTML 首页),no-cache 加上 ETag 的组合,比起 no-store 每次完整传输有效率得多,同时仍保证用户不会看到旧内容。

三个层次的配置

实务上可以依内容性质分成三类处理:

一、带版本或指纹的静态资源

文件名含哈希的 JS、CSS、图片,内容永远不会变(变了就是新文件名)。这类可以设很长的 max-age 并加上 immutable,让浏览器连验证都省下。这是性能收益最大、风险最低的一类。

二、会变动但不敏感的内容

HTML 页面、API 的公开响应。适合 no-cache 或短的 max-age 搭配 ETag——确保拿到最新版本,但不变时只付出一次 304 的成本。若想让 CDN 挡掉大部分流量,再用 s-maxage 拉长边缘的缓存时间。

三、含个人数据的响应

登录后的页面、账务信息、任何带身份的内容。这类要用 no-store,而不是 no-cache。如果只是不想让 CDN 存、但浏览器可以存,那是 private 的用途。三者不要混用。

排查缓存问题时该先确认的三件事

命中率不如预期或内容没更新时,这三项最常是原因:

  1. 确认 Vary 列了什么 用浏览器开发者工具或 curl 看实际响应标头。若列了 User-Agent 或 Cookie 之类取值极多的标头,缓存几乎不可能命中,而这不会有任何错误提示。
  2. 确认 s-maxage 与 max-age 的关系 如果 CDN 的缓存时间跟你设的 max-age 不一致,先看有没有 s-maxage——它会覆盖 max-age,而且只影响共享缓存。两者并存时很容易误判。
  3. 确认你看的是哪一层缓存 浏览器、CDN 边缘节点、反向代理各有各的存储。响应中的 Age 标头会告诉你这份内容在缓存里待了多久——用它判断内容是从哪一层来的,比猜测可靠。

小结

no-cache 允许存储、要求验证;no-store 才是禁止存储。这个命名造成的误解,一边让敏感数据在以为受保护的情况下被写入磁盘,一边让过度保守的配置牺牲掉整个缓存层的价值。两种错误的代价不同,但根源是同一个。

而真正决定 CDN 效果的往往不是这些指令本身,是 Vary 与 s-maxage 这两个较少被谈到的部分。如果您的跨境流量需要靠 CDN 挡下大部分回源,这两项的配置比节点数量更有影响。

参考资料