【无标题】

HTTP协强制缓存与协商缓存详解

本文首发于个人技术博客,转载请注明出处

你是否遇到过这些问题:

  • 前端更新了代码,用户刷新浏览器还是看到旧版本?
  • 明明服务器资源没变,每次刷新还是要重新下载几十 MB 的 JS?
  • F5 刷新和 Ctrl+F5 强制刷新到底有什么区别?

这些问题的答案,都藏在 HTTP 缓存机制里。而协商缓存作为 HTTP 缓存体系中最灵活、最常用的一环,既是面试高频考点,也是解决线上性能问题的关键。

一、先搞懂:HTTP 缓存的两层架构

HTTP 缓存分为强制缓存协商缓存两层,它们是递进关系:先判断强制缓存是否命中,命中则直接使用本地缓存;未命中才会进入协商缓存流程。

1.1 强制缓存:浏览器自己说了算

强制缓存的核心是:只要浏览器判断缓存没过期,就直接使用本地资源,完全不发请求到服务器

它通过两个响应头实现:

  • Cache-Control: max-age=86400:相对时间,从响应返回时刻算起,86400 秒(1 天)内有效
  • Expires: Wed, 28 May 2026 10:00:00 GMT:绝对时间,在这个时间点之前有效

核心区别Cache-Control不依赖客户端本地时间,通过计算「服务器响应时间 + max-age」得到过期时间,稳定性远高于Expires。同时存在时,Cache-Control优先级更高。

状态码200 OK (from disk cache)200 OK (from memory cache)

1.2 协商缓存:服务器最终说了算

强制缓存有一个致命问题:过期后不管资源有没有变化,都要重新下载完整资源

协商缓存就是为了解决这个问题:

强制缓存过期后,浏览器不直接下载资源,而是携带缓存标识发请求给服务器。

服务器判断资源是否更新:

  • 没更新:返回304 Not Modified,告诉浏览器继续用本地缓存
  • 更新了:返回200 OK和完整的新资源

这就是 "协商" 的含义:由服务器最终决定是否使用缓存

二、协商缓存核心原理详解

协商缓存有两种独立的实现方式,可以单独使用,也可以同时使用。

2.1 基于时间的实现:Last-Modified / If-Modified-Since

这是最古老、最简单的协商缓存机制:

  1. 第一次请求资源时,服务器在响应头中返回:

    http 复制代码
    Last-Modified: Wed, 27 May 2026 10:00:00 GMT

    表示这个资源的最后修改时间。

  2. 强制缓存过期后,浏览器再次请求时,会在请求头中携带:

    http 复制代码
    If-Modified-Since: Wed, 27 May 2026 10:00:00 GMT

    把上次拿到的最后修改时间发给服务器。

  3. 服务器对比资源当前的最后修改时间和请求头中的值:

    • 如果相等:资源没更新,返回304 Not Modified
    • 如果不等:资源更新了,返回200 OK和新资源,同时更新Last-Modified

缺点

  • 精度只有秒级,1 秒内多次修改无法识别
  • 文件内容没变但修改时间变了(比如重新保存),会导致误判
  • 部分服务器无法准确获取文件修改时间

2.2 基于唯一标识的实现:ETag / If-None-Match

为了解决Last-Modified的缺点,HTTP/1.1 引入了ETag机制:

  1. 第一次请求资源时,服务器根据资源内容计算出一个唯一哈希值,在响应头中返回:

    http 复制代码
    ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
  2. 强制缓存过期后,浏览器再次请求时,会在请求头中携带:

    http 复制代码
    If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
  3. 服务器重新计算当前资源的 ETag,和请求头中的值对比:

    • 如果相等:返回304 Not Modified
    • 如果不等:返回200 OK和新资源,同时更新ETag

ETag 的两种类型

类型 格式 对比精度 适用场景
强 ETag ETag: "abc123" 字节级精确对比 绝大多数场景
弱 ETag ETag: W/"abc123" 语义级对比(允许注释、空格等微小差异) 对性能要求极高的场景

2.3 优先级与对比

当响应头中同时存在ETagLast-Modified时:

  • ETag优先级绝对高于Last-Modified
  • 服务器会先判断 ETag 是否变化,只有 ETag 不变时,才会去判断 Last-Modified

2.4 你不知道的 304 响应细节

很多人以为 304 只是一个状态码,其实它有几个非常重要的特性:

  1. 304 响应只返回响应头,不返回响应体:这是它节省带宽的核心原因,一个 304 响应通常只有几百字节
  2. 304 响应会更新本地缓存的过期时间 :如果服务器返回了新的Cache-ControlExpires,浏览器会用新值更新缓存
  3. 304 响应不会更新 ETag 和 Last-Modified:只有返回 200 时才会更新这两个标识

2.5 不同刷新行为对缓存的影响(面试必问)

这是最容易被忽略但最实用的知识点:

操作 强制缓存 协商缓存 行为说明
地址栏回车 / 链接跳转 ✅ 生效 ❌ 不触发 优先使用强制缓存,过期才走协商
F5 刷新 / 点击刷新按钮 ❌ 失效 ✅ 生效 跳过强制缓存,直接发起协商请求
Ctrl+F5 强制刷新 ❌ 失效 ❌ 失效 完全跳过所有缓存,请求头不带任何缓存标识,强制服务器返回完整资源

三、一张图看懂完整 HTTP 缓存流程

下面是从浏览器发起请求到最终渲染的完整缓存流程图,涵盖了所有分支情况:

四、Spring Boot 缓存实战:零代码到自定义

很多人以为协商缓存需要写大量代码,其实 Spring Boot 已经为我们做了几乎所有的工作。

4.1 Spring Boot 默认缓存机制

Spring Boot 默认就为所有静态资源(JS、CSS、图片、字体等)开启了协商缓存:

  • 自动生成Last-Modified:基于文件的最后修改时间
  • 自动生成ETag:基于文件内容的 MD5 哈希值
  • 默认没有开启强制缓存,也就是每次请求都会走协商缓存

这意味着:你什么都不用做,Spring Boot 项目已经在使用协商缓存了

4.2 基础配置:application.yml

我们只需要通过简单的配置,就能开启强制缓存,进一步提升性能:

yaml 复制代码
spring:
  resources:
    cache:
      # 开启缓存
      cachecontrol:
        # 静态资源强制缓存1年(31536000秒)
        max-age: 31536000
        # 允许CDN和浏览器缓存
        cache-public: true
        # 资源过期后必须和服务器协商
        must-revalidate: true
      # 开启ETag生成(默认开启)
      use-etag: true
      # 开启Last-Modified生成(默认开启)
      use-last-modified: true

配置说明

  • max-age: 31536000:设置 1 年的强制缓存,这是静态资源的标准配置
  • cache-public: true:允许中间代理(如 CDN)缓存资源
  • must-revalidate: true:强制缓存过期后必须和服务器协商,不能使用过期缓存

4.3 进阶:自定义静态资源缓存规则

不同类型的静态资源,缓存策略应该不同。我们可以通过WebMvcConfigurer自定义规则:

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 1. 打包后的前端资源(带哈希值):强制缓存1年
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/")
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)
                        .cachePublic()
                        .mustRevalidate());

        // 2. HTML文件:不使用强制缓存,每次都走协商缓存
        registry.addResourceHandler("/*.html")
                .addResourceLocations("classpath:/static/")
                .setCacheControl(CacheControl.noCache());

        // 3. 接口文档:缓存1小时
        registry.addResourceHandler("/doc.html")
                .addResourceLocations("classpath:/META-INF/resources/")
                .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)
                        .cachePublic());
    }
}

最佳实践

  • 带哈希值的静态资源(如app.abc123.js):设置超长强制缓存,更新时改文件名即可
  • HTML 文件:设置no-cache,每次都走协商缓存,保证用户能看到最新版本
  • 不经常变化的资源(如接口文档、图片):设置适中的强制缓存时间

4.4 高级:为动态接口生成自定义 ETag

对于不经常变化的动态接口,我们也可以手动实现协商缓存,大幅提升接口性能。

Spring Boot 提供了ETagResponseFilterShallowEtagHeaderFilter,可以自动为响应生成 ETag:

java 复制代码
@Configuration
public class ETagConfig {

    /**
     * 自动为所有响应生成浅ETag
     * 基于响应内容的MD5哈希值
     */
    @Bean
    public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
        FilterRegistrationBean<ShallowEtagHeaderFilter> filterBean = new FilterRegistrationBean<>();
        filterBean.setFilter(new ShallowEtagHeaderFilter());
        // 只对指定接口生效
        filterBean.addUrlPatterns("/api/dict/*", "/api/config/*");
        filterBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return filterBean;
    }
}

效果

  • 访问/api/dict/list接口时,服务器会自动生成 ETag
  • 再次请求时,如果接口返回的数据没变,服务器会直接返回 304
  • 对于数据量大、变化频率低的接口,性能提升非常明显

4.5 实际开发场景案例

场景 1:前端工程化项目的缓存策略

这是目前互联网公司最常用的方案:

  1. 前端打包时,给所有 JS、CSS、图片文件名加上内容哈希值
  2. 这些带哈希值的资源,设置 1 年强制缓存
  3. HTML 文件不设置强制缓存,每次都走协商缓存
  4. 更新时,只有修改过的文件哈希值会变,用户只会下载变化的资源
场景 2:字典接口的缓存优化

系统中的字典接口(如性别、地区、状态)通常变化频率极低,但调用量极大。

  • 配置ShallowEtagHeaderFilter为字典接口生成 ETag
  • 设置Cache-Control: max-age=3600,强制缓存 1 小时
  • 这样既保证了数据的实时性,又能将接口 QPS 提升 10 倍以上
场景 3:CDN 配合的缓存策略

当使用 CDN 时,缓存流程变成:

  1. 用户请求 CDN 节点
  2. CDN 节点判断自己的缓存是否过期
  3. 未过期:直接返回给用户
  4. 过期:CDN 节点携带 ETag 和 Last-Modified 回源到服务器
  5. 服务器返回 304:CDN 更新自己的缓存过期时间,返回给用户
  6. 服务器返回 200:CDN 更新缓存,返回新资源给用户

五、灵魂拷问:协商缓存真的有人用吗?

这一开始也是是我最最易或的问题,也是很多人对缓存最大的误解。

5.1 为什么我在国企三年没用到?

这完全是场景差异导致的,和技术本身的实用性无关:

  1. 用户量和性能要求不同:国企内部系统通常只有几十到几百个用户,服务器压力极小,即使所有资源都不缓存也能跑通。而互联网公司的系统要面对几百万甚至几千万用户,每节省 1KB 带宽、每减少 100ms 延迟,都能带来巨大的成本节约。
  2. 技术栈和迭代频率不同:很多国企老项目还是 JSP、Thymeleaf 这种服务端渲染技术,静态资源极少,且迭代周期极长,几个月甚至几年才更新一次。而互联网公司的前端项目每周甚至每天都要更新,缓存策略直接影响用户体验。
  3. 缓存逻辑被自动处理了:没写过不代表它不存在。Nginx、Tomcat、Spring Boot 默认就会为静态资源生成 ETag 和 Last-Modified,CDN 的核心就是缓存。这些逻辑都在底层默默工作,后端开发完全感知不到。

5.2 哪些地方在大规模使用协商缓存?

  • 所有互联网公司的前端项目:淘宝、京东、抖音这些网站,90% 以上的静态资源都在使用协商缓存
  • 所有 CDN 和对象存储服务:阿里云 OSS、腾讯云 COS 默认就会为所有文件生成 ETag 和 Last-Modified
  • 公共 API 和开放平台:GitHub API、高德地图 API 等都大量使用协商缓存来节省服务器资源和 API 调用次数

5.3 后端开发到底需要做什么?

绝大多数情况下,你不需要手动实现协商缓存的逻辑,框架和中间件已经帮你做好了。你只需要:

  1. 正确配置Cache-Control头,明确不同资源的缓存策略
  2. 对于数据量大、变化频率低的接口,开启 ETag 生成
  3. 排查线上缓存问题时,能看懂缓存头,知道为什么用户看到的是旧资源

六、缓存最佳实践与避坑指南

  1. 优先使用强制缓存:强制缓存不需要发请求,性能远高于协商缓存
  2. 带哈希值的资源用超长强制缓存:更新时改文件名即可,永远不会有缓存问题
  3. HTML 文件永远用 no-cache:保证用户能看到最新版本
  4. 不要给 POST 接口设置缓存:POST 接口通常是写操作,缓存会导致数据不一致
  5. 更新资源时一定要改文件名:不要依赖协商缓存来更新资源,用户可能会看到旧版本
  6. 线上问题排查时,先看缓存头:90% 的缓存问题都是因为缓存头配置错误导致的

总结

协商缓存不是什么高大上的新技术,也不是只存在于面试中的八股文。它是 HTTP 协议的基础组件,是现代 Web 架构中无处不在的性能优化手段。

缓存确实在互联网的每一个角落默默工作着。理解它的原理,能让你在遇到缓存问题时不再抓瞎,也能让你设计出性能更好的系统。

最后记住一句话:缓存是计算机科学中最难的问题之一,也是收益最高的问题之一

相关推荐
喵手4 个月前
Python爬虫实战:HTTP缓存系统深度实战 — ETag、Last-Modified与requests-cache完全指南(附SQLite持久化存储)!
爬虫·python·爬虫实战·http缓存·etag·零基础python爬虫教学·requests-cache
捧 花4 个月前
HTTP的补充
http·cookie·session·http缓存·工作流程
Light605 个月前
告别缓存浪费:No-Vary-Search,为你的网站性能注入“AI级”智能
缓存·性能优化·cdn·web性能·http缓存·no-vary-search·url参数
fighting ~2 年前
浏览器如何进行静态资源缓存?—— 强缓存 & 协商缓存
缓存·http缓存