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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
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 缓存 vs Redis;与 redis-essentials 分工。

25 分钟

应用层缓存对照

这一课你将能做到

  1. 用一张表说清浏览器/CDN HTTP 缓存、Nginx proxy_cache、Redis 应用缓存各自的键、TTL 量级与失效方式
  2. 画出 GET /api/tracks 从浏览器到 MySQL 的逐层决策图,并写出 cache-aside(旁路缓存)的读路径与写后失效代码
  3. 遇到「后台改了课表但页面仍旧」时,按「浏览器 → Nginx → Redis → DB」的顺序定位该失效哪一层,而不是只让用户清浏览器

为什么要学

只学 HTTP 头,会以为「加了 max-age 就完事」;只加 Redis,浏览器还是每次都来打你。三层缓存职责完全不同:HTTP 缓存省的是重复传输,Nginx 挡的是打到应用前的流量,Redis 省的是数据库查询——而 MySQL 永远是权威数据源。

真实面试题「接口响应慢,你有哪些层次的优化手段」「如何保证缓存与数据库一致性」都建立在这张全景图上。本课是 HTTP 缓存课的延伸,也是 Redis/MySQL 学习路线的接口。

先建立图像

层级谁决定典型键TTL 量级失效方式
浏览器 / CDNCache-Control、ETagURL(+ Vary 指定头)秒 ~ 天过期 / 304 校验 / 换 URL
Nginxproxy_cache 配置$request_uri 等几十秒常见TTL 过期 / 主动 purge
RedisJava 业务代码tracks:all 等分钟级写时 DEL / 版本号 bump
MySQL—行数据—权威源,无失效概念

一条 GET 的逐层决策:

GET /api/tracks
  ① 浏览器强缓存未过期? → 不发请求,直接用(disk/memory cache)
  ② 请求发出,CDN / Nginx proxy_cache 命中? → 不打 Java,返回 X-Cache-Status: HIT
  ③ 到了 Spring,Redis 有 tracks:all? → 不打 MySQL
  ④ MySQL 查询 → 回填 Redis → 设置 HTTP 缓存头 → 响应逐层返回

每命中一层,就替下一层省一次工作;每层失效策略独立,所以「改了数据不生效」要按层排查。

跟着做

步骤 1:cache-aside 读路径(真实代码)

@Service
public class TrackService {

    private final StringRedisTemplate redis;
    private final TrackRepository trackRepository;   // → MySQL
    private final ObjectMapper objectMapper;

    public List<TrackDTO> listAll() throws Exception {
        String cached = redis.opsForValue().get("tracks:all");
        if (cached != null) {
            return objectMapper.readValue(cached, new TypeReference<>() {});
        }
        List<TrackDTO> list = trackRepository.findAllDto();     // 权威:查库
        redis.opsForValue().set("tracks:all", objectMapper.writeValueAsString(list),
                Duration.ofMinutes(5));                          // 回填 + TTL 兜底
        return list;
    }
}

要点:先读缓存,未命中才读库并回填。TTL 是最后的兜底防线——就算忘了主动失效,5 分钟后数据也会自然新鲜。

步骤 2:写后失效(比读路径更容易出错)

@Transactional
public void update(Long id, TrackForm form) {
    trackRepository.update(id, form);          // ① 先写权威源 MySQL

    // ② 再删相关缓存键(失效,而非回写——回写易把旧值写回)
    redis.delete("tracks:all");
    redis.delete("tracks:by-slug:" + form.getSlug());

    // ③ HTTP 层配合:公开列表的 max-age 要与业务容忍度匹配
}

只更新 DB 不删 Redis → 用户永远看旧列表,与浏览器无关;这是「加了缓存反而更不一致」的第一大来源。

步骤 3:给接口套上 HTTP 缓存头,形成完整三层

@GetMapping("/api/tracks")
public ResponseEntity<List<TrackDTO>> list() throws Exception {
    return ResponseEntity.ok()
        .cacheControl(CacheControl.maxAge(Duration.ofSeconds(30)).cachePublic())
        .body(trackService.listAll());
}

对照观测:

# 第 1 次:Redis MISS → 打 MySQL;响应带 Cache-Control: max-age=30
curl -i http://localhost:8080/api/tracks

# 第 2 次(30 秒内、Redis 已热):Java 里 Redis 命中——但注意,浏览器强缓存命中时这次请求根本不会发出来
curl -i http://localhost:8080/api/tracks

DevTools 里 Size 列显示 (disk cache) 说明连 8080 都没碰到;X-Cache-Status: HIT(若配了 Nginx proxy_cache)说明流量被 Nginx 挡下;Redis 命中默认不在 HTTP 头暴露,除非你自己加 X-App-Cache 教学头。

步骤 4:Nginx proxy_cache 挡在应用前

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m max_size=100m inactive=60s;

server {
    location /api/tracks {
        proxy_pass http://127.0.0.1:8080;
        proxy_cache api_cache;
        proxy_cache_valid 200 60s;                 # 200 响应缓存 60 秒
        add_header X-Cache-Status $upstream_cache_status;  # HIT / MISS / EXPIRED
    }
}
# 两次连续请求,第二次应显示 X-Cache-Status: HIT
curl -si http://localhost/api/tracks | findstr /i "x-cache cache-control"
curl -si http://localhost/api/tracks | findstr /i "x-cache cache-control"

Nginx 缓存期间 Java 日志安静——这是好事,但排错时要意识到「应用根本没收到请求」。

步骤 5:故障演习——「改了课表前台仍旧」按层定位

现象定位动作
无痕窗口/Disable cache 后变新浏览器强缓存(或 Service Worker)调短 HTML/接口 max-age,静态资源换哈希 URL
无痕仍旧,但 curl 直连 8080 已是新值Nginx proxy_cache 或响应头策略缩短 proxy_cache_valid,或管理端写操作后 purge
curl 直连也旧,但 DB 已新Redis 键没删检查 update 事务是否执行了 redis.delete;TTL 是否过长
三层全旁路仍旧连错库(读写分离延迟)/ 多实例配置漂移看应用实际连接串,核对从库延迟

把这张表存成团队的排错手册,比每次现场回忆快十倍。

步骤 6:写后失效清单模板

为 PUT /admin/tracks/{id} 列出必须失效的键(示例命名,团队要统一规范):

tracks:all                    ← 列表聚合键
tracks:by-slug:{slug}         ← 单个详情键
tracks:{id}:lessons           ← 子资源
tracks:{id}:lessons:lite      ← 子资源瘦身版

再补 HTTP 层的思考题:若曾给列表设过较长 max-age,现在要不要改成「短 TTL + 写时删 Redis」组合?改库 → 删 Redis → 核对 HTTP 头策略,三步写成团队检查表,比再引入第四层缓存更值钱。

步骤 7:给每类数据选层(落到本站)

数据推荐组合
哈希文件名 JS/CSS长 HTTP 强缓存,不带 Redis
公开课表列表HTTP 短缓存 30s + Nginx 60s + Redis 5min(可选叠加)
用户会话Redis(禁 CDN 缓存,HTTP 头 no-store)
个人学习进度HTTP private, no-store + Redis 按用户键缓存

讲清楚要点

  1. 因为权威在数据库,所以一切缓存都是性能副本——一致性靠 TTL 与主动失效保证在「业务可容忍窗口」内,不存在绝对实时;产品讨论的不是「要不要缓存」而是「能容忍多久旧数据」。
  2. 因为 HTTP 缓存的键主要是 URL,所以个性化响应不能配 public——用户 A 的「我的进度」可能被共享缓存发给用户 B;个性化数据 private/no-store + 按用户键的应用缓存。
  3. 因为 Redis 键是业务命名,所以要有命名规范与批量失效策略(tracks:* 前缀扫描或维护 key 清单)——散落的魔法字符串是缓存事故之源。
  4. 因为层数越多推新越难,所以公开列表宜「短 TTL + 写时删」组合,而不是指望一年 max-age 加祈祷;新内容要快,用版本化 URL 立即生效。
  5. 因为 Nginx 命中时应用日志是安静的,所以「没打到应用」可能是缓存正常工作——排错先看 X-Cache-Status,别急着断言「服务挂了」。
  6. 因为 cache-aside 的写路径最容易漏删键,所以代码评审要重点盯 update/delete 方法——「先写库、后删缓存」是默认范式;「写库后回写缓存」在并发下会把旧值写回,新手高危。

你可能会懵的地方

  1. HTTP 缓存和 Redis 是二选一吗? 不是,可叠加且职责不同:浏览器 30s 强缓存省「重复传输」;Redis 5min 省「数据库查询」。同一接口两层都命中时各回各家——前者表现为请求没发出,后者表现为 Java 里没查库。

  2. 为什么加了 Redis,Chrome 还是频繁请求? Redis 不代替浏览器缓存。接口没配 Cache-Control 时,每次刷新都会打到 Nginx/Java(只是 Java 里查 Redis 更快)。想让「浏览器别来」,必须配 HTTP 头。

  3. 缓存穿透/击穿/雪崩要在本课管吗? 点到为止:空结果也短缓存(或布隆过滤器)防穿透;热点 key 加互斥重建防击穿;TTL 加随机抖动防集体过期。细节留 Redis track,但要知道「DB 扛不住」时先想到这三个词。

  4. 「清一下缓存」这句产品需求到底指什么? 必须问清:清 CDN?清 Nginx proxy_cache?删 Redis 键模式?还是发新版本换 URL?四个动作完全不同——把模糊需求翻译成具体层操作,是后端的基本功。

  5. 单机内存缓存(Caffeine)放哪一层? 比 Redis 更快但进程独享。多实例 Spring 下「实例 A 已更新、实例 B 内存仍旧」是真实问题;常见做法是进程内短 TTL(秒级)+ Redis 共享(分钟级)两级,或只用 Redis 换取一致性。

错法对照

错法问题正解
只加 Redis,从不设 HTTP 缓存头浏览器仍每次来打,Redis 只是让 Java 快了一点公开 GET 配短 max-age 或协商,多层各自生效
更新 DB 后不删 Redis用户长期看旧数据,且 TTL 越长越离谱写路径强制 invalidate,评审盯紧 update/delete
用户私有接口配 public, max-age=3600A 用户数据可能被 CDN 发给 B 用户private/no-store,应用层按 userId 键缓存
排障第一步教用户 Ctrl+F5只治浏览器层的个别用户,其它层原样按层定位表逐层排除,服务端主动失效
所有缓存键同一超长 TTL冷热数据一锅炖,推新困难按易变程度分级 TTL,热点短、冷区长

卡点急救

  1. Redis 里全是旧值,不知道该删哪些键:开发期直接 redis-cli --scan --pattern "tracks:*" 列出再批量删;根治是给「每个写接口对应哪些键」建文档或用统一缓存管理类,禁止散落 redis.delete("xxx") 字符串。
  2. Nginx proxy_cache 缓存了不该缓存的接口:proxy_cache_valid 按路径收窄;带 Set-Cookie/Authorization 的请求默认别进缓存(proxy_cache_bypass、proxy_no_cache 控制);上线前 curl 两次看 X-Cache-Status 与响应体是否含用户特征。
  3. 删了 Redis 值,下一秒又变旧:多实例下另一实例的写请求没走失效逻辑,或定时任务在回写旧快照。全局搜所有写该键的代码;失效逻辑收敛到一个组件,所有写路径必须经过它。

面试常问

  • 接口响应慢,可以从哪些层次优化? ① HTTP 层:静态资源长缓存 + API 短缓存/协商,压测确认 TTFB 归属;② 网关层:Nginx proxy_cache 挡热点;③ 应用层:Redis 缓存热数据、接口合并、异步化;④ 数据层:索引、慢 SQL 优化、读写分离;⑤ 协议层:h2/h3、gzip/br。回答时强调「先用 Timing/日志定位瓶颈层,再选手段」。

  • 如何保证缓存与数据库一致性? 常用 cache-aside:读时未命中查库回填;写时「先更新 DB,再删除缓存」。进阶讨论:删除失败用 TTL 兜底或消息队列重试;并发读写下的短暂不一致用「业务可容忍窗口 + 版本号」控制;强一致场景干脆不用缓存或加分布式锁(代价大)。核心表达:追求最终一致 + 窗口可控,不追求绝对实时。

  • 浏览器缓存、Nginx 缓存、Redis 缓存各自的键和失效方式? 浏览器/CDN:键是 URL(+Vary 头),由 Cache-Control/ETag 控制,过期或换 URL 失效。Nginx:键默认 $request_uri,由 proxy_cache_valid 控制,TTL 或 purge 失效。Redis:键是业务字符串,由代码与 TTL 控制,写时 DEL 或过期失效。三者可叠加,MySQL 始终是权威。

自测三问

  1. 三层缓存各用一句话说明「它省下了谁的工作」。
  2. cache-aside 的读路径四步是什么?写成功后至少要做什么?
  3. 后台改了 title 但浏览器仍显示旧名,你的排查顺序与每一步的验证手段是什么?

小结一句话

HTTP 缓存省重复传输,Nginx 挡应用前流量,Redis 减数据库压力——MySQL 永远是权威;写后失效 + 分层排错,比再堆一层缓存更值钱。

延伸

  • MDN:HTTP 缓存(浏览器/共享缓存层)
  • Redis 官方:Caching 指南(应用层思路)
  • 下一课:综合练习——把方法、状态、头、HTTPS、缓存、Cookie、CORS、REST 串成一条可重复的排错流程,交付一张全链路请求记录表。
加载本节练习…
← 上一课HTTP/2 与 HTTP/3下一课 →综合练习