2703 字
约 9 分钟
21
S02-B11 · HTTP 缓存入门:强缓存、协商缓存与失效

页面改完、发布成功、浏览器里看到的还是旧版本;换个浏览器或开无痕就正常。这不是发布没生效,而是请求在到达源站之前就被某一层缓存应答了。HTTP 缓存是协议层的能力,它的规则写在响应头里,谁能存、能存多久、过期后怎么验证,全部由头决定。

这篇按"改了为什么看不到新版"拆成四步:缓存在哪几层 → 凭什么不请求 → 过期后怎么验证 → 怎么让它立刻失效。


1. 四个位置,四套开关

位置 谁在存 生效范围 关掉的方式
浏览器私有缓存 每个用户的浏览器 单个用户 用户清缓存;页面头改为 no-store
共享代理 公司/运营商代理 该代理后所有用户 Cache-Control: private 禁止共享缓存存储
CDN 边缘节点 CDN 厂商 一个大区域 源站头 + 控制台缓存规则 + purge
服务端应用缓存 你的应用/网关 全站 应用配置,与 HTTP 头无关

三层 HTTP 缓存遵循同一套语义,但配置入口不同:源站的头是"建议",CDN 的缓存规则通常可以覆盖它。排查时必须先确认响应到底由谁返回——看 Server、Via、X-Cache、CF-Cache-Status 一类头。


2. 强缓存:不请求就回答

Cache-Control: public, max-age=31536000, immutable
Expires: Thu, 31 Dec 2026 23:59:59 GMT
指令 含义 常见误用
max-age=N 从响应生成起 N 秒内可直接复用 与 Expires 同时写且互相矛盾
s-maxage=N 只对共享缓存生效,覆盖 max-age 以为它也管浏览器
immutable 有效期内连"重新验证"都省掉 用在会变的 URL 上
public 允许共享缓存存储 与 private 混写
private 只允许浏览器存 给 CDN 用,导致 CDN 不缓存

Expires 是绝对时间,依赖客户端时钟;机器时间不准就会提前过期或过期后仍命中。Cache-Control 存在时Expires 被忽略,所以现代站点只写前者。immutable 的意义在刷新行为:普通 max-age 下按 F5 会发一次验证请求,带 immutable 的响应在有效期内连验证都不发。


3. 协商缓存:过期后的一次问价

强缓存过期不等于必须重传。客户端带上验证信息问一句"我这份还能用吗",服务端答 304 就继续用本地副本。

组合 请求头 响应头 比较方式
ETag If-None-Match: "abc123" ETag: "abc123" 精确匹配(W/ 前缀为弱比较)
Last-Modified If-Modified-Since: <date> Last-Modified: <date> 时间不晚于该值即命中
两者都有 两个头同时发 服务端以 ETag 优先 按 RFC 9111

304 的语义是"用你的副本",它不带响应体,只回少量头(ETag、Cache-Control、Date)。所以 304 省的是传输字节,不是一次往返——RTT 依然要付。Last-Modified 精度只到秒,同一秒内改两次会误判未变;ETag 由服务端生成,内容或元数据变化就该变。Nginx 默认对静态文件生成 ETag(etag on),格式按版本核对。

location ~* \.(js|css|png|woff2)$ {
    etag on;
    add_header Cache-Control "public, max-age=31536000, immutable";
}
location = /index.html {
    etag on;
    add_header Cache-Control "no-cache";
}

4. no-cache 与 no-store:一字之差

指令 能否存储 使用前是否必须验证 典型用途
no-cache 能存 必须,每次带 If-None-Match 回源验证 HTML、API 的 GET
no-store 不能存(含磁盘) 不适用 含敏感信息的响应
must-revalidate 能存 过期后禁止用 stale,必须验证 库存、价格等强一致数据
stale-while-revalidate=60 能存 60 秒内先用旧值,同时后台刷新 首页列表、非关键内容
max-age=0 能存 等价于立即过期,仍需验证 兼容老客户端的写法

把 no-store 写成 no-cache,是"退出登录后按后退还能看到个人信息"的常见来源:no-cache 允许副本留在磁盘上,只是用前要验证,而某些场景(离线、代理)连验证都省了。

stale-while-revalidate 是"以旧换快"的折中:用户先看到略旧的内容,后台再刷新。它只影响"过期后的一小段时间",不能替代正确的内容版本管理。


5. Vary:被忽略的缓存键

Vary 声明"这份响应随哪些请求头变化",缓存会把这些头的值并入缓存键。忘写 Vary: Accept-Encoding 的后果是:CDN 把 gzip 版本存下来,再发给一个不声明支持压缩的客户端。

Vary: Accept-Encoding, Accept-Language
写法 效果 风险
Vary: Accept-Encoding 压缩与非压缩分开存 命中率略降
Vary: Accept-Language 多语言版本分开存 变体数量乘以语言数
Vary: Cookie 几乎每个用户一个变体 CDN 命中率接近 0,等于没缓存
不写 Vary 只按 URL 缓存 返回错误变体(压缩/语言/设备)

Vary: * 是"永不命中"的写法。判断是否需要 Vary 的方法:响应内容是否依赖某个请求头;若依赖,就必须写,否则跨用户串内容。


6. 指纹文件名与 HTML:两种缓存策略

长缓存的正确用法不是"给所有资源设一年",而是内容哈希进文件名:

<script src="/assets/app.9f2c1a.js"></script>
<link rel="stylesheet" href="/assets/main.4b7e0d.css">

文件名一变,URL 就变,旧缓存自然不会再被请求;新文件名第一次访问必定回源。于是 app.9f2c1a.js 可以设 max-age=31536000, immutable。

资源 建议策略 原因
带哈希的 JS/CSS/字体 max-age=31536000, immutable URL 与内容绑定,可永久缓存
index.html no-cache + ETag URL 固定但内容每次发布都变
API GET no-cache 或短 max-age 数据可能随时变
用户私有接口 private, no-store 不允许任何中间层留存

HTML 长缓存等于发布通道被堵死:用户不会去取新的 HTML,也就永远拿不到新的哈希文件名。这是"改了没生效"的头号原因,且清缓存能"修好",容易误判为偶发。


7. CDN:缓存键、Age 与 purge

CDN 的缓存键默认是 URL(含查询串),可配置忽略指定参数。purge 的成本按粒度上升:

方式 影响面 回源压力
按 URL 精确 purge 单个文件 小
按目录/前缀 purge 一批文件 中
全站 purge 全部 大,流量瞬间打回源站

判断边缘副本有多旧,看 Age 与 Date:

Date: Fri, 09 Nov 2026 12:00:00 GMT
Age: 3500
Cache-Control: public, max-age=3600

Age 表示这份副本已在缓存里放了 3500 秒,max-age 是 3600,只剩 100 秒新鲜。Age 接近 max-age 时,边缘会回源验证;如果源站返回 304,CDN 续期并继续服务。Age 为 0 或缺失,说明这次可能没命中缓存。


8. 强制刷新的三种方式

操作 实际发出的请求 原理
Ctrl-F5 / Cmd-Shift-R 带 Cache-Control: no-cache(部分浏览器还带 Pragma) 跳过强缓存,强制回源验证
DevTools 勾 Disable cache 同上,且持续生效 只对当前开着的 DevTools 有效
URL 加 ?v=2 全新 URL 改变缓存键,旧副本仍在但不再被请求

Ctrl-F5 不会清掉 CDN 的副本,它只影响你这台浏览器;CDN 上是旧内容时,强刷多少次都是旧的。反过来,清浏览器缓存也不影响 CDN。排障时要分清"哪一层的副本"。


9. 最小示例:看头、看 304、看哈希

# 1) 第一次请求:看缓存相关响应头
curl -sI https://example.com/assets/app.9f2c1a.js | grep -iE 'cache-control|etag|age|date'

# 2) 带上 ETag 再请求:期望 304,且无响应体
curl -sI -H 'If-None-Match: "9f2c1a"' https://example.com/assets/app.9f2c1a.js

# 3) 改内容后重新构建,文件名变 9f2c1b,再请求:期望 200 且是新内容
curl -sI https://example.com/assets/app.9f2c1b.js

三步分别验证:强缓存头是否配对、协商缓存是否真的返回 304、指纹是否随内容变。若第 2 步返回 200 而不是 304,检查服务端 ETag 是否每次请求都变(比如基于 mtime 且被回源改写),这类"永远不命中"比不缓存更浪费带宽。


10. 排错

现象 原因 处理
发布后仍看到旧页面 HTML 被长缓存 给 HTML 设 no-cache
强刷能好、普通刷新不行 强缓存 max-age 过长 缩短或改指纹命名
换浏览器正常 本机浏览器缓存 清缓存并核对响应头
CDN 上仍是旧文件 未 purge 或缓存键含被忽略参数 精确 purge 对应 URL
压缩版本串到不支持客户端 缺 Vary: Accept-Encoding 补 Vary
每次请求都 200,从不 304 ETag 每次变化 / 未开 etag 修正 ETag 生成依据
登录后按后退仍见隐私页 该页用了 no-cache 而非 no-store 改 no-store

11. 边界

场景 更合适的做法 理由
秒级更新的数据 短 max-age + 前端轮询/推送 HTTP 缓存粒度是秒级
需要按用户区分的内容 private + 应用层缓存 共享缓存会把 A 的内容给 B
大文件分发 CDN + 分片/断点续传 缓存只解决重复请求
需要"立刻全量失效" 版本化 URL 或灰度发布 purge 有延迟与成本

HTTP 缓存解决"同一 URL 的重复请求",不解决"内容该不该更新"。真正的发布控制靠文件名版本化,缓存头只是配套。


12. 自测

  1. 响应同时有 Cache-Control: max-age=60 和已过期的 Expires,浏览器听谁的?
  2. 304 省掉的是什么,没省掉的是什么?
  3. no-cache 与 no-store 差在哪一步?
  4. 为什么带哈希的静态资源可以设一年,而 HTML 不行?
  5. Vary: Accept-Encoding 漏写会导致什么现象?
  6. Ctrl-F5 能清掉 CDN 上的旧副本吗?
S02-B11 · HTTP 缓存入门:强缓存、协商缓存与失效
https://www.clxhxhhr.top/posts/6145/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。