有个同事遇到过一个怪 bug:用户反馈「手机端看到了电脑版页面」。排查半天,发现是 CDN 缓存串了——CDN 缓存了 PC 端的响应,手机端请求同一个 URL 时,CDN 直接返回了 PC 版。

这个问题的根源是:同一个 URL,服务器可能根据请求头返回不同内容(手机返回手机版、电脑返回 PC 版、不同语言返回不同语言版本)。缓存如果只按 URL 存,就会把 PC 版错给手机用户。Vary 头就是解决这个问题的。

缓存键:缓存按什么区分

先理解「缓存键」的概念。缓存存一个响应时,要有个 key,下次用同样的 key 能取出来。最朴素的缓存键就是 URL——/index.html 对应一个缓存项。

但 URL 相同不代表内容相同。同一个 /index.html,服务器可能根据 User-Agent 返回 PC 版或手机版。如果缓存只按 URL 存,PC 版和手机版会互相覆盖,谁先到缓存谁占住,后来的人拿到错的版本。

Vary 让缓存键不只看 URL

Vary 头的作用就是扩展缓存键。响应头 Vary: User-Agent 告诉缓存:「这个响应的内容依赖 User-Agent,缓存它时要把 User-Agent 也算进缓存键」。这样 PC 和手机的响应虽然 URL 相同,但因为 User-Agent 不同,缓存键不同,各存各的,不会串。

Vary 怎么工作

Vary 的值是一组请求头的名字,表示「这些请求头的值会影响响应内容」。缓存计算缓存键时,把这些头的值拼进去。

Vary 的工作机制:按请求头分桶缓存

一个典型的场景:

响应头: Vary: Accept-Encoding, User-Agent

这告诉缓存:这个响应同时依赖 Accept-Encoding(压缩方式)和 User-Agent(设备类型)。缓存键就变成 URL + Accept-Encoding值 + User-Agent值 的组合。

  • 手机 + gzip → 缓存项 A
  • 手机 + br → 缓存项 B
  • PC + gzip → 缓存项 C
  • PC + br → 缓存项 D

四个组合各存各的,互不干扰。

最常见的 Vary 用法

实战中最常见的 Vary 配置:

  • Vary: Accept-Encoding:服务器根据客户端支持的压缩算法返回 gzip 或 br 版本。这是几乎每个站点都该配的——不配会导致「支持 br 的客户端拿到 gzip 版本」或更糟的串内容。
  • Vary: User-Agent:响应内容随设备变化(响应式或独立移动版)。但 User-Agent 的值非常多样,可能导致缓存命中率下降(每个不同的 UA 都是一个独立缓存项),所以有的服务会做 UA 归一化(把「Chrome 120」和「Chrome 121」归成一类)。
  • Vary: Accept-Language:响应内容随语言变化(i18n 国际化)。
  • Vary: Origin:CORS 场景,响应的 Access-Control-Allow-Origin 随请求的 Origin 变化。

漏配 Vary 的坑

漏配 Vary 是真实的高频 bug,开头那个手机看到 PC 版就是典型。再举一个:服务端返回 JSON,支持 gzip 和 br 两种压缩,但没配 Vary: Accept-Encoding。结果 CDN 缓存了 gzip 版,一个只支持 br 的老客户端请求时,CDN 返回了 gzip 版给它——客户端解压失败,接口报错。

这种 bug 的特点是很隐蔽:本地测试没问题(本地不经过 CDN),一到生产环境偶发,排查困难。根因都是缓存键没包含影响内容的维度

配 Vary 的原则很简单:响应内容依赖哪个请求头,Vary 就必须列哪个。漏配就串内容,多配(列了不影响的头)只会降低缓存命中率,不会出错。所以有疑问时宁可多配。

Vary: * 和反向情况

有两个特殊情况:

  • Vary: *:表示「这个响应每次都可能不同,别缓存」。等于禁用缓存,比 no-cache 更彻底。极少用。
  • 响应内容其实不随任何头变化:这时候不用配 Vary(或者不配也行)。但实践中很多服务器为了安全会统一配 Vary: Accept-Encoding,因为压缩几乎总是会影响内容。

取舍与边界

这个串内容的 bug 有个特别隐蔽的特征——本地永远复现不了:

漏配 Vary 的完整故障链路:从 CDN 缓存到手机看到 PC 版

Vary 有几个值得注意的边界:

  • Vary 列的头越多,缓存命中率越低。因为每个组合都是一个独立缓存项,组合爆炸会让缓存变得稀疏。所以 Vary 要精确——只列真正影响内容的头。
  • HTTP/2 里 Vary 依然有效,但头部压缩(HPACK)让多个请求头的存储更高效,缓解了 Vary 的开销。
  • CDN 的 Vary 处理能力不同。有些 CDN 只认有限的几个 Vary 头(如只认 Accept-Encoding),自定义 Vary 可能被忽略。配 Vary 前要确认你的 CDN 支持哪些。

收束:Vary 是缓存正确性的补丁

强缓存和协商缓存解决「能不能用、要不要重传」,Vary 解决「缓存的是不是我要的那个版本」。三者合起来,才是完整的缓存正确性保障。漏了 Vary,缓存得越快,错得越离谱——手机用户飞快地看到 PC 版页面,比慢一点但正确更糟。

下一篇讲缓存层级——从浏览器到 CDN 到源站,缓存分布在不同位置,每一层都有自己的判断逻辑。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。

我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。

如果你想继续跟完这套「图解 HTTP」,欢迎关注公众号 「十三Tech」。后续会按 URL 与报文、连接与传输、缓存与协商、安全与边界、HTTP/2 与 HTTP/3 这条线更新。

十三Tech公众号二维码