CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
HTTP 协议与缓存/课时/HTTP 缓存
RawBlame
课时目录12 节

HTTP 协议与缓存

  1. 01HTTP 是什么
  2. 02方法与语义
  3. 03状态码
  4. 04请求与响应头
  5. 05HTTPS 与 TLS
  6. 06HTTP 缓存
  7. 07Cookie 与会话
  8. 08CORS
  9. 09REST 与资源设计
  10. 10HTTP/2 与 HTTP/3
  11. 11应用层缓存对照
  12. 12综合练习

HTTP 缓存

Cache-Control、ETag、304、CDN。

35 分钟

HTTP 缓存

这一课你将能做到

  1. 区分强缓存(max-age 未过期直接用本地副本,不发请求)与协商缓存(带条件头问服务器「变了没」→ 常返回 304),并各自画出完整时序
  2. 读懂 Cache-Control 六个常用指令与 ETag / If-None-Match、Last-Modified / If-Modified-Since 两对搭档
  3. 在 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=6060 秒内可当新鲜,直接用本地
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 三态对比

  1. 普通刷新:看哪些请求是 (disk cache) / (memory cache),哪些是 304
  2. 勾选 Disable cache 再刷新:全部真实下载,对比总耗时
  3. 取消勾选,再普通刷新:恢复缓存

注意:Disable cache 只在 DevTools 打开时生效,是用来调试的,不代表用户真实行为——别用它评估线上性能。

步骤 5:观察本站静态资源的「哈希文件名 + 长缓存」

  1. 打开 /tracks → Network → 找 /_next/static/... 的 JS/CSS
  2. 看 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/CSSpublic, max-age=31536000, immutable靠换 URL,秒级
HTML 文档no-cache(或极短 max-age)每次校验,准
公开 GET JSONpublic, max-age=30, must-revalidate最迟 30 秒
登录态/个人数据private, no-store永远新鲜

问自己:管理端改了 title,30 秒内用户可能看到旧值——能接受吗?不能就调短 max-age 或改用纯协商。

讲清楚要点

  1. 因为强缓存不问源站,所以 HTML 配长缓存必然导致「发了版用户仍旧壳」——HTML 宜 no-cache(每次校验),静态资源靠哈希文件名配长缓存。
  2. 因为 no-cache ≠ 禁止缓存,所以敏感数据必须用 no-store——这两个词的命名是 HTTP 历史上最反直觉的设计,面试爱考。
  3. 因为 ETag 是内容指纹,所以内容变了 ETag 必须变——后端改了返回内容却复用旧 ETag(比如手写死),客户端会一直 304 出旧数据,且极难排查。
  4. 因为共享缓存(CDN/代理)不分用户,所以带鉴权的个性化响应默认不许 public——A 用户的响应被 CDN 缓存后可能发给 B 用户,真实事故。
  5. 因为 304 省的是 body 不是往返,所以热点 API 要进一步减压得靠 CDN/Nginx/Redis 分层——HTTP 缓存只是第一层(应用缓存课对照)。
  6. 因为缓存是分层的,所以「清了 CDN」清不掉浏览器里的强缓存——要立等可见只能换 URL(版本参数/哈希),或一开始就把 TTL 定短。

你可能会懵的地方

  1. from disk cache 和 304 有什么区别? 前者是强缓存命中:浏览器直接用本地副本,网络里根本没有这个请求(Network 显示灰色)。后者真的发了请求,服务器说「你没落下什么,继续用」。前者更快,零流量零往返。

  2. Last-Modified 和 ETag 用哪个? ETag 更精确(内容哈希级);Last-Modified 是秒级时间戳、实现简单但有坑(文件内容没变但 mtime 变了、秒内多次修改检测不到)。可同时下发,优先级 If-None-Match 高于 If-Modified-Since。

  3. POST 能被缓存吗? 默认不。HTTP 缓存的主战场是 GET/HEAD 这类安全方法。这也是「查询别乱改成 POST」的又一理由(方法课呼应)。

  4. Vary 头是什么? 告诉共享缓存:「同一 URL 的响应还会随某个请求头变化」。典型 Vary: Accept-Encoding(gzip 与 br 的副本分开存)。乱加 Vary: User-Agent 会让缓存命中率暴跌;Vary: * 等于禁缓存。

  5. Service Worker 缓存和这节课讲的是一回事吗? 不是。Service Worker 是 JS 层的可编程缓存,优先级在 HTTP 缓存之上,能拦截改写请求。本课聚焦协议层;排查「明明 no-store 还能离线打开」时要想到 SW。

错法对照

错法问题正解
全站一刀切 Cache-Control: no-cache静态资源每次回源,带宽与 TTFB 全浪费按资源类型分级:静态长缓存、HTML/API 短或协商
用户私有 JSON 配 public, max-age=86400CDN 可能把你 A 用户的数据发给 B 用户private 或 no-store
改了返回内容但 ETag 写死不变客户端永远 304,线上数据「卡死」ETag 必须由内容派生;变更内容即变更指纹
「缓存坏了」就教用户 Ctrl+F5用户不是工程师,问题还会复发工程上用哈希文件名与正确头解决,强刷只当诊断手段
把 Redis 命中当成 HTTP 304两层机制完全不同,指标混着看会误判HTTP 层看 304/size,应用层看 Redis 命中率(应用缓存课)

卡点急救

  1. 发了新版本用户还看旧页面:按层排查——HTML 的 Cache-Control 是否配了长 max-age(改成 no-cache);Service Worker 是否缓存了旧壳;CDN 是否需要 purge。验证用无痕窗口 + curl -I 看源站真实响应头。
  2. 浏览器死活不缓存某静态资源:检查响应头有没有 Set-Cookie(带 Set-Cookie 的响应多数缓存层会拒存)、是否 no-store、URL 是否带变化参数导致每次都「未命中」。curl -I 逐项核对。
  3. 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 抖动;集群下建议用内容哈希统一生成,或干脆用「哈希文件名 + 长强缓存」替代协商。

自测三问

  1. 用三步分别描述强缓存与协商缓存的完整流程。
  2. no-cache 与 no-store 区别?登录后的用户信息接口该用哪个?
  3. 如何在 DevTools 证明某个 JS 命中了磁盘缓存?为什么这个证据不能用来评估所有用户的线上体验?

小结一句话

Cache-Control 定能不能存、存多久;ETag 负责「变了没」的校验——GET 只读资源是 HTTP 缓存主战场,no-cache 可存要验、no-store 彻底别存。

延伸

  • MDN:HTTP 缓存
  • MDN:Cache-Control
  • 下一课:Cookie 与会话——HTTP 缓存管「数据的新鲜」,Cookie 管「你是谁」:Set-Cookie 四大属性与 CSRF 的关系。
加载本节练习…
← 上一课HTTPS 与 TLS下一课 →Cookie 与会话