阅读提要
Cache-Control、ETag、Last-Modified、Vary 四个头各管一段,配错的时候不会报错,只会出现「改了没生效」和「怎么老是旧的」。这篇把决策顺序和常见冲突一次讲清。
缓存出问题,几乎都不是「没配」
线上最常见的两类缓存问题,恰好相反:一类是「后台改了配置,前台还是旧的」,另一类是「用户每次都重新下载,CDN 命中率个位数」。前者是缓存太久,后者是根本没缓存。
而这两个问题的根因往往相同:多个缓存头同时存在,但没人知道谁说了算。
浏览器不傻,它会按固定顺序判断。理解这个顺序,80% 的困惑就消失了。
决策顺序:从强到弱

大致遵循这样一条链:
Cache-Control: no-store—— 直接不存,跳过后面的所有判断;Cache-Control: no-cache—— 可以存,但每次用之前必须回源验证;Cache-Control: max-age=N—— 存 N 秒,期间不请求;- 没有
Cache-Control时才看Expires(HTTP/1.0 的遗留字段); - 需要验证时,先看
ETag(If-None-Match),再看Last-Modified(If-Modified-Since)。
所以「我明明加了 max-age=60,为什么还是旧的」这类问题,答案通常是同一个响应里还有一个更强的头——比如网关自动补了 no-cache,或者反向代理把 Cache-Control 覆盖了。
检查方法很朴素:curl -I 看源站返回什么,再看 CDN 返回什么。两层不一致时,问题在中间层,不在你的代码里。
ETag 和 Last-Modified 的分工
两者都能做协商缓存,区别在精度与语义。
Last-Modified 只有秒级精度。如果两个版本的修改发生在同一秒内,浏览器会认为「没变」,拿到旧内容。文件系统时间戳还可能因为时区、touch、容器重建而漂移。
ETag 是内容指纹,精度够,但要注意强弱之分:
- 强校验:
"abc123",要求字节级完全一致; - 弱校验:
W/"abc123",语义等价即可,常用于压缩后内容。
如果响应经过 gzip 或 CDN 动态压缩,强 ETag 很容易失效——压缩后的字节每次可能不同。这时用弱 ETag,或者干脆让边缘层重新生成。
一个实用做法:静态资源用内容哈希做文件名,配一年强缓存;HTML 用 no-cache + ETag。前者永久命中,后者每次回源但只传几个字节的 304。
# 带哈希的静态资源
Cache-Control: public, max-age=31536000, immutable
# HTML 文档
Cache-Control: no-cache
ETag: "d3f1..."
注意 immutable 这个扩展值:它告诉浏览器「这个 URL 的内容永远不会变,别再发验证请求」。只在文件名带内容哈希时才敢用,否则就是给自己挖坑。
Vary 是最容易被忽略的一个
Vary 决定缓存键里包含哪些请求头。最常见的场景是语言和编码:
Vary: Accept-Encoding
漏了它,压缩版和非压缩版会互相污染,出现「某些用户拿到乱码」。
更危险的是把 Vary: User-Agent 用在大流量站点上:UA 组合几乎无限,缓存会被打成一万个碎片,命中率归零。要区分移动端和桌面端,用 Vary 不如用不同的 URL。
CDN 是独立的一层,别指望它读你的心思
源站设了 max-age=0,很多人以为 CDN 就不会缓存。实际上多数 CDN 有自己的「忽略源站缓存头」开关,默认可能按文件类型给一个 TTL。
真正控制 CDN 的是它自己的配置,或者 s-maxage(只对共享缓存生效)和 CDN-Cache-Control 这类专用头:
Cache-Control: public, max-age=60, s-maxage=600
这行的意思是:浏览器存 60 秒,CDN 存 600 秒。发布新内容时如果 CDN 还在 600 秒里,就会出现「我这边刷新了,别人还是旧的」——这不是 bug,是你自己配的。
要主动失效,只能调 CDN 的刷新接口。所以凡是「发布后必须立刻可见」的页面,s-maxage 就别设太长。
一个可以直接抄的配置
# HTML:每次验证,命中 304 只传头部
location ~* \.html$ {
add_header Cache-Control "no-cache";
etag on;
}
# 带哈希的静态资源:一年强缓存
location ~* \.(js|css|woff2|png|jpg|webp)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# 接口:默认不缓存,需要缓存的接口单独加头
location /api/ {
add_header Cache-Control "no-store";
}
这三条覆盖了绝大多数站点。剩下的问题基本都是中间层覆盖,用 curl -I 逐层对比就能定位。
排查清单
- 后端真的返回了预期的头吗?(
curl -I,别只看浏览器面板) - 反代、CDN 有没有改写或补头?
ETag是强还是弱,是否和压缩冲突?- 有没有漏
Vary: Accept-Encoding? - 发布后要立刻生效的页面,是否被
s-maxage拖住?
缓存不出错的时候没人注意,出一次就是「线上改了没生效」这种最难解释的故障。把这五个问题固化进上线检查,比事后排查便宜得多。
你认为这篇文章怎么样?
还没有表情反馈

一起聊聊
一个想法、一点补充,或一句简单的问候。
评论与回复
友善交流,彼此尊重第一句话,留给你。
读完有什么感受?欢迎成为第一个分享的人。