阅读提要

Cache-Control、ETag、Last-Modified、Vary 四个头各管一段,配错的时候不会报错,只会出现「改了没生效」和「怎么老是旧的」。这篇把决策顺序和常见冲突一次讲清。

开始阅读

字号

缓存出问题,几乎都不是「没配」

线上最常见的两类缓存问题,恰好相反:一类是「后台改了配置,前台还是旧的」,另一类是「用户每次都重新下载,CDN 命中率个位数」。前者是缓存太久,后者是根本没缓存。

而这两个问题的根因往往相同:多个缓存头同时存在,但没人知道谁说了算。

浏览器不傻,它会按固定顺序判断。理解这个顺序,80% 的困惑就消失了。

决策顺序:从强到弱

缓存决策顺序

大致遵循这样一条链:

  1. Cache-Control: no-store —— 直接不存,跳过后面的所有判断;
  2. Cache-Control: no-cache —— 可以存,但每次用之前必须回源验证;
  3. Cache-Control: max-age=N —— 存 N 秒,期间不请求;
  4. 没有 Cache-Control 时才看 Expires(HTTP/1.0 的遗留字段);
  5. 需要验证时,先看 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 逐层对比就能定位。

排查清单

  1. 后端真的返回了预期的头吗?(curl -I,别只看浏览器面板)
  2. 反代、CDN 有没有改写或补头?
  3. ETag 是强还是弱,是否和压缩冲突?
  4. 有没有漏 Vary: Accept-Encoding?
  5. 发布后要立刻生效的页面,是否被 s-maxage 拖住?

缓存不出错的时候没人注意,出一次就是「线上改了没生效」这种最难解释的故障。把这五个问题固化进上线检查,比事后排查便宜得多。

你认为这篇文章怎么样?

还没有表情反馈

分享文章