HTTP 缓存
这一课你将能做到
- 区分强缓存(max-age 未过期直接用本地副本,不发请求)与协商缓存(带条件头问服务器「变了没」→ 常返回 304),并各自画出完整时序
- 读懂
Cache-Control 六个常用指令与 ETag / If-None-Match、Last-Modified / If-Modified-Since 两对搭档
- 在 DevTools 用 Size 列(disk/memory cache)、304 与 Disable cache 开关,解释「两次刷新为什么快慢不同」,并为静态资源与 API 各写一条可上线的缓存策略
为什么要学
每次打开 /tracks 都把 JS/CSS 完整下载一遍,又慢又烧带宽。HTTP 缓存让浏览器、CDN、Nginx 在规则允许时直接复用上一次的响应——这是 Web 性能优化的第一杠杆,不花一分钱架构改造。
但规则写错就是事故:登录后的个人数据被 public 缓存,A 用户看到 B 用户的数据;HTML 配了长缓存,发了版用户永远看旧页面。面试里「强缓存和协商缓存的区别」「no-cache 和 no-store 的区别」是高频题,答案全在这一课。
先建立图像
两条缓存路线的时序图:
强缓存命中(零请求):
浏览器:max-age 未过期 → 直接用本地副本
Network 面板 Size 列显示 (disk cache) / (memory cache)
协商缓存(一次往返,省 body):
① 首次 GET → 200 + ETag: "v1" + body
② 再次 GET + If-None-Match: "v1"
③a 内容没变 → 304 Not Modified,无 body(省带宽,仍耗一次往返)
③b 内容变了 → 200 + 新 ETag + 新 body
Cache-Control 常用指令速查:
| 指令 | 人话 |
|---|
max-age=60 | 60 秒内可当新鲜,直接用本地 |
public | 共享缓存(CDN/代理)也可以存 |
private | 只允许浏览器存(用户私有数据) |
no-store | 别存任何副本(敏感数据) |
no-cache | 可以存,但用前必须向服务器校验(名字反直觉!) |
must-revalidate | 过期后必须回源验证,不许用过期副本 |
口诀:公开课表可短缓存;登录态/个性化数据 private 或 no-store。
跟着做
步骤 1:给 Spring Boot 接口配上缓存头
写一个演示接口,体验三种策略:
@RestController
@RequestMapping("/api/cache-demo")
public class CacheDemoController {
// 策略 1:公开数据,短强缓存
@GetMapping("/public-data")
public ResponseEntity<String> publicData() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofSeconds(30)).cachePublic())
.body("public content " + LocalDate.now());
}
// 策略 2:私有数据,禁止一切缓存
@GetMapping("/private-data")
public ResponseEntity<String> privateData() {
return ResponseEntity.ok()
.cacheControl(CacheControl.noStore())
.body("private content for user-A");
}
// 策略 3:协商缓存,用 ETag 问「变了没」
@GetMapping("/negotiated")
public ResponseEntity<String> negotiated(HttpServletRequest request) {
String content = "stable content v1";
String etag = "\"" + Integer.toHexString(content.hashCode()) + "\"";
return ResponseEntity.ok()
.cacheControl(CacheControl.noCache()) // 可存但必须校验
.eTag(etag)
.body(content);
}
}
步骤 2:curl 验证强缓存
curl -i http://localhost:8080/api/cache-demo/public-data
# 观察响应头:Cache-Control: max-age=30, public
浏览器里验证:访问该接口的页面,普通刷新两次,Network 里 Size 列出现 (disk cache)——请求根本没发出去,这就是强缓存。30 秒后刷新,才会真正打源站。
步骤 3:curl 手搓协商缓存
# 第一次:拿 ETag(输出里抄下来,含双引号)
curl -si http://localhost:8080/api/cache-demo/negotiated | findstr /i "etag cache"
# 第二次:把 ETag 填进 If-None-Match
curl -i -H 'If-None-Match: "abc123"' http://localhost:8080/api/cache-demo/negotiated
# 内容没变 → 304,无 body;改成别的值 → 200 + 新内容
生产上 Spring 的 ShallowEtagHeaderFilter、或 Nginx 对静态文件的 ETag 都走这套协议。
步骤 4:DevTools 三态对比
- 普通刷新:看哪些请求是
(disk cache) / (memory cache),哪些是 304
- 勾选 Disable cache 再刷新:全部真实下载,对比总耗时
- 取消勾选,再普通刷新:恢复缓存
注意:Disable cache 只在 DevTools 打开时生效,是用来调试的,不代表用户真实行为——别用它评估线上性能。
步骤 5:观察本站静态资源的「哈希文件名 + 长缓存」
- 打开
/tracks → Network → 找 /_next/static/... 的 JS/CSS
- 看 Response Headers:
cache-control: public, max-age=31536000, immutable
为什么敢缓存一年?因为文件名带内容哈希:内容一改文件名就变,URL 变了缓存自然失效。「长缓存」安全的前提是「URL 可变」——这是前端工程化与 HTTP 缓存的合谋,也是最值得抄走的模式。
步骤 6:Nginx 侧加缓存头(静态资源示例)
location /assets/ {
expires 30d; # 生成 Cache-Control: max-age=2592000
add_header Cache-Control "public, immutable";
}
location /api/ {
proxy_pass http://127.0.0.1:8080;
add_header Cache-Control "no-store" always; # API 默认不缓存,要缓存的接口单独配
}
nginx -t && nginx -s reload 后 curl -I 验证头是否生效。注意 add_header 在同一层级出现多条时会覆盖,需合并写。
步骤 7:为「后台改了课表但用户看旧的」设计预案
| URL 类型 | 建议头 | 更新及时性 |
|---|
| 哈希文件名 JS/CSS | public, max-age=31536000, immutable | 靠换 URL,秒级 |
| HTML 文档 | no-cache(或极短 max-age) | 每次校验,准 |
| 公开 GET JSON | public, max-age=30, must-revalidate | 最迟 30 秒 |
| 登录态/个人数据 | private, no-store | 永远新鲜 |
问自己:管理端改了 title,30 秒内用户可能看到旧值——能接受吗?不能就调短 max-age 或改用纯协商。
讲清楚要点
- 因为强缓存不问源站,所以 HTML 配长缓存必然导致「发了版用户仍旧壳」——HTML 宜
no-cache(每次校验),静态资源靠哈希文件名配长缓存。
- 因为
no-cache ≠ 禁止缓存,所以敏感数据必须用 no-store——这两个词的命名是 HTTP 历史上最反直觉的设计,面试爱考。
- 因为 ETag 是内容指纹,所以内容变了 ETag 必须变——后端改了返回内容却复用旧 ETag(比如手写死),客户端会一直 304 出旧数据,且极难排查。
- 因为共享缓存(CDN/代理)不分用户,所以带鉴权的个性化响应默认不许
public——A 用户的响应被 CDN 缓存后可能发给 B 用户,真实事故。
- 因为 304 省的是 body 不是往返,所以热点 API 要进一步减压得靠 CDN/Nginx/Redis 分层——HTTP 缓存只是第一层(应用缓存课对照)。
- 因为缓存是分层的,所以「清了 CDN」清不掉浏览器里的强缓存——要立等可见只能换 URL(版本参数/哈希),或一开始就把 TTL 定短。
你可能会懵的地方
-
from disk cache 和 304 有什么区别?
前者是强缓存命中:浏览器直接用本地副本,网络里根本没有这个请求(Network 显示灰色)。后者真的发了请求,服务器说「你没落下什么,继续用」。前者更快,零流量零往返。
-
Last-Modified 和 ETag 用哪个?
ETag 更精确(内容哈希级);Last-Modified 是秒级时间戳、实现简单但有坑(文件内容没变但 mtime 变了、秒内多次修改检测不到)。可同时下发,优先级 If-None-Match 高于 If-Modified-Since。
-
POST 能被缓存吗?
默认不。HTTP 缓存的主战场是 GET/HEAD 这类安全方法。这也是「查询别乱改成 POST」的又一理由(方法课呼应)。
-
Vary 头是什么?
告诉共享缓存:「同一 URL 的响应还会随某个请求头变化」。典型 Vary: Accept-Encoding(gzip 与 br 的副本分开存)。乱加 Vary: User-Agent 会让缓存命中率暴跌;Vary: * 等于禁缓存。
-
Service Worker 缓存和这节课讲的是一回事吗?
不是。Service Worker 是 JS 层的可编程缓存,优先级在 HTTP 缓存之上,能拦截改写请求。本课聚焦协议层;排查「明明 no-store 还能离线打开」时要想到 SW。
错法对照
| 错法 | 问题 | 正解 |
|---|
全站一刀切 Cache-Control: no-cache | 静态资源每次回源,带宽与 TTFB 全浪费 | 按资源类型分级:静态长缓存、HTML/API 短或协商 |
用户私有 JSON 配 public, max-age=86400 | CDN 可能把你 A 用户的数据发给 B 用户 | private 或 no-store |
| 改了返回内容但 ETag 写死不变 | 客户端永远 304,线上数据「卡死」 | ETag 必须由内容派生;变更内容即变更指纹 |
| 「缓存坏了」就教用户 Ctrl+F5 | 用户不是工程师,问题还会复发 | 工程上用哈希文件名与正确头解决,强刷只当诊断手段 |
| 把 Redis 命中当成 HTTP 304 | 两层机制完全不同,指标混着看会误判 | HTTP 层看 304/size,应用层看 Redis 命中率(应用缓存课) |
卡点急救
- 发了新版本用户还看旧页面:按层排查——HTML 的 Cache-Control 是否配了长 max-age(改成
no-cache);Service Worker 是否缓存了旧壳;CDN 是否需要 purge。验证用无痕窗口 + curl -I 看源站真实响应头。
- 浏览器死活不缓存某静态资源:检查响应头有没有
Set-Cookie(带 Set-Cookie 的响应多数缓存层会拒存)、是否 no-store、URL 是否带变化参数导致每次都「未命中」。curl -I 逐项核对。
- 304 突然变多,源站 CPU 反而升高:协商缓存每次都要回源算哈希/查库。大流量下把纯协商改成「短强缓存 + 内容哈希 URL」,或把校验逻辑下沉到 Nginx 静态文件/CDN。
面试常问
-
强缓存和协商缓存的区别?完整流程?
强缓存:Cache-Control: max-age 未过期时浏览器直接用本地副本,不发请求(Network 显示 disk/memory cache)。协商缓存:过期或 no-cache 时带 If-None-Match(配 ETag)或 If-Modified-Since(配 Last-Modified)问服务器,未变返回 304 无 body,变了返回 200 新内容。流程是先强后协商:强缓存优先,过期后才走协商。
-
no-cache 和 no-store 的区别?
no-cache:可以缓存,但使用前必须向服务器验证新鲜度(配合 ETag 走 304)。no-store:任何缓存层都不许存副本,用于敏感数据。HTML 文档常用 no-cache 保证壳及时更新;用户数据用 no-store。
-
ETag 是怎么生成的?有什么坑?
可由内容哈希、版本号、mtime 生成。坑:Nginx 默认 ETag 基于 mtime+size,多机部署且文件同步有差异时会 304/200 抖动;集群下建议用内容哈希统一生成,或干脆用「哈希文件名 + 长强缓存」替代协商。
自测三问
- 用三步分别描述强缓存与协商缓存的完整流程。
no-cache 与 no-store 区别?登录后的用户信息接口该用哪个?
- 如何在 DevTools 证明某个 JS 命中了磁盘缓存?为什么这个证据不能用来评估所有用户的线上体验?
小结一句话
Cache-Control 定能不能存、存多久;ETag 负责「变了没」的校验——GET 只读资源是 HTTP 缓存主战场,no-cache 可存要验、no-store 彻底别存。
延伸