HTTP缓存机制详解强制缓存与协商缓存工作原理
HTTP协强制缓存与协商缓存详解
聊到Web性能优化,缓存是绕不开的核心话题。但很多开发者一听到“强制缓存”、“协商缓存”这些术语就头疼,觉得是面试八股文,离实际开发很远。今天,我们就来彻底拆解一下HTTP缓存的两层架构,看看它们到底是怎么工作的,以及在实际项目中,尤其是Spring Boot生态下,我们到底该怎么用。
一、先搞懂:HTTP 缓存的两层架构
HTTP缓存机制其实是一个清晰的递进流程,分为强制缓存和协商缓存两层。简单来说,浏览器发起请求时,会先问自己:“本地缓存还能用吗?” 如果能用(强制缓存命中),就直接从本地读取,连请求都不发。如果不能用(强制缓存未命中),它才会去问服务器:“我手上的这个版本,你还认吗?” 这就是协商缓存。
1.1 强制缓存:浏览器自己说了算
强制缓存的核心逻辑非常霸道:只要浏览器判断缓存没过期,就直接使用本地资源,完全不会向服务器发送任何请求。这就像是给浏览器发了一张“通行证”,在有效期内,它自己说了算。
这张“通行证”主要通过两个响应头来签发:
Cache-Control: max-age=86400:这是相对时间,从服务器响应返回的那一刻开始计时,86400秒(也就是1天)内有效。Expires: Wed, 28 May 2026 10:00:00 GMT:这是绝对时间,告诉浏览器在这个时间点之前,缓存都是有效的。
这里有个关键区别:Cache-Control的max-age不依赖客户端的本地时间,它是通过计算“服务器响应时间 + max-age”来得到过期时间的,因此稳定性远高于依赖客户端时钟的Expires。当两者同时存在时,Cache-Control的优先级更高。
当强制缓存命中时,你在浏览器开发者工具的网络面板里,会看到状态码显示为200 OK (from disk cache)或200 OK (from memory cache),这表示资源根本没经过网络。
1.2 协商缓存:服务器最终说了算
强制缓存虽好,但有个致命问题:一旦“通行证”过期,不管服务器上的资源有没有真正更新,浏览器都得重新下载完整的资源。这显然不够聪明。
协商缓存就是为了解决这个问题而生的。它的核心是“协商”:当强制缓存失效后,浏览器会带着一些“凭证”去问服务器,服务器根据这些凭证判断资源是否变化,再决定是让浏览器继续用旧缓存,还是返回新资源。
二、协商缓存核心原理详解
协商缓存有两种独立的实现机制,它们可以单独使用,也可以协同工作。
2.1 基于时间的实现:Last-Modified / If-Modified-Since
这是最古老也最简单的一种方式,基于资源的最后修改时间。
流程是这样的:
- 首次请求:服务器在返回资源时,会在响应头里加上
Last-Modified: Wed, 27 May 2026 10:00:00 GMT,告诉浏览器这个文件最后是什么时候被改动的。 - 再次请求:当强制缓存过期后,浏览器再次请求这个资源时,会在请求头里带上
If-Modified-Since: Wed, 27 May 2026 10:00:00 GMT,把上次拿到的时间戳还给服务器。 - 服务器判断:服务器对比当前资源的最后修改时间和请求头里的时间戳。
- 如果两者相等,说明资源没变,服务器就返回一个轻量的
304 Not Modified响应,告诉浏览器:“接着用你缓存里的吧。” - 如果不相等,说明资源更新了,服务器就返回
200 OK和全新的资源内容,同时更新Last-Modified头。
- 如果两者相等,说明资源没变,服务器就返回一个轻量的
但这个机制有几个明显的缺点:精度只有秒级,一秒内的多次修改无法识别;有时候文件内容没变,只是被重新保存了一下,修改时间变了,会导致不必要的重新下载;另外,有些服务器可能无法精确获取文件的修改时间。
2.2 基于唯一标识的实现:ETag / If-None-Match
为了解决Last-Modified的不足,HTTP/1.1引入了ETag(实体标签)机制。它不关心时间,只关心内容。
它的工作流程和基于时间的类似,但凭证变了:
- 首次请求:服务器根据资源内容(比如计算一个MD5哈希值)生成一个唯一标识,通过响应头
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"发给浏览器。 - 再次请求:浏览器在请求头里带上
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"。 - 服务器判断:服务器重新计算当前资源的ETag,和请求头里的值对比。
- 一致则返回
304。 - 不一致则返回
200和新资源。
- 一致则返回
ETag还分两种类型,适用于不同场景:
| 类型 | 格式 | 对比精度 | 适用场景 |
|---|---|---|---|
| 强 ETag | ETag: "abc123" |
字节级精确对比 | 绝大多数场景 |
| 弱 ETag | ETag: W/"abc123" |
语义级对比(允许注释、空格等微小差异) | 对性能要求极高的场景 |
2.3 优先级与对比
如果服务器同时提供了ETag和Last-Modified,那么ETag拥有绝对的优先权。服务器会先检查If-None-Match(ETag),只有在ETag验证通过或者不存在时,才会去检查If-Modified-Since(Last-Modified)。
2.4 你不知道的 304 响应细节
很多人以为304就是个状态码,其实它背后有几个非常重要的特性,正是这些特性让它能节省带宽:
- 只返头,不返体:304响应只包含响应头,没有响应体。这就是为什么一个304响应可能只有几百字节,却能省下几十KB的资源下载。
- 会更新缓存过期时间:如果304的响应头里包含了新的
Cache-Control或Expires,浏览器会用这些新值来更新本地缓存的过期时间。 - 不更新验证标识:304响应不会更新资源的
ETag或Last-Modified值,这些标识只有在返回200 OK的新响应时才会被更新。
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
想要更进一步,开启强制缓存来获得极致性能?只需要在application.yml里加几行配置:
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 进阶:自定义静态资源缓存规则
一刀切的缓存策略并不合理。HTML文件、带哈希的JS文件、接口文档,它们的更新频率天差地别。我们可以通过实现WebMvcConfigurer接口来为不同资源定制策略:
@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):设置超长强制缓存(如1年)。因为文件名哈希变了就是新文件,旧缓存自然失效。 - HTML入口文件:设置
no-cache,每次都走协商缓存。确保用户总能拿到最新的页面骨架。 - 不常变的资源(如图片、文档):设置一个合理的强制缓存时间(如几小时或几天)。
4.4 高级:为动态接口生成自定义 ETag
缓存不只是静态资源的专利。对于那些数据变化不频繁的动态接口(比如查询字典、配置项),同样可以利用协商缓存大幅提升性能,降低数据库压力。
Spring Boot提供了ShallowEtagHeaderFilter,可以自动为响应内容生成ETag:
@Configuration
public class ETagConfig {
/**
* 自动为所有响应生成浅ETag
* 基于响应内容的MD5哈希值
*/
@Bean
public FilterRegistrationBean shallowEtagHeaderFilter() {
FilterRegistrationBean filterBean = new FilterRegistrationBean<>();
filterBean.setFilter(new ShallowEtagHeaderFilter());
// 只对指定接口生效,避免不必要的计算开销
filterBean.addUrlPatterns("/api/dict/*", "/api/config/*");
filterBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filterBean;
}
}
配置后效果立竿见影:当请求/api/dict/list时,服务器会计算响应体的ETag。下次请求如果数据没变,ETag匹配,服务器直接返回304,省去了业务逻辑执行和完整数据传输的开销。对于数据量大、调用频繁的只读接口,性能提升非常明显。
4.5 实际开发场景案例
光说不练假把式,我们来看几个真实场景。
场景 1:前端工程化项目的缓存策略
这是现代互联网公司的标准做法:
- 前端使用Webpack/Vite等工具打包,为每个JS/CSS文件生成基于内容的哈希文件名(如
main.a1b2c3.js)。 - 这些带哈希的资源,在Nginx或Spring Boot中配置1年的强制缓存。
- HTML文件不设强制缓存,使用
no-cache。 - 当代码更新时,只有变动的文件哈希会改变,用户只需拉取变化的文件,其余资源均从本地缓存读取。更新体验丝滑,带宽节省巨大。
场景 2:字典接口的缓存优化
系统里的字典数据(省份城市、订单状态、性别等)几乎不变,但每个页面都可能调用。给这类接口加上ETag和短暂的强制缓存(比如1小时),QPS提升十倍以上不是梦,还能极大减轻数据库压力。
场景 3:CDN 配合的缓存策略
当引入CDN后,缓存流程变成了三级:浏览器 -> CDN -> 源站。
- 用户请求先到CDN节点。
- CDN判断自己缓存是否过期(依据就是源站返回的
Cache-Control和Expires)。 - 如果未过期,CDN直接返回资源给用户(命中)。
- 如果过期,CDN会带着
If-None-Match和If-Modified-Since回源站询问。 - 源站返回304,CDN刷新自己缓存的过期时间,然后返回缓存给用户。
- 源站返回200和新资源,CDN更新缓存并返回给用户。
合理配置缓存头,能保证CDN高效命中,显著降低源站压力,加速全球访问。
五、灵魂拷问:协商缓存真的有人用吗?
这可能是很多后端开发者,尤其是在传统企业或内部系统工作的朋友,心中最大的疑问。
5.1 为什么感觉用不上?
这通常是场景差异导致的错觉:
- 用户量与性能压力不同:一个几百人用的内部OA系统,服务器资源充裕,不用缓存也跑得飞快。但面对千万日活的C端应用,节省1KB的流量、减少10ms的延迟,乘以用户数就是巨大的成本和体验收益。
- 技术栈与迭代速度不同:很多老项目采用JSP、Thymeleaf等服务端渲染,页面动态生成,静态资源少且更新慢。而现代前后端分离项目,前端资源独立部署,每周甚至每天迭代,缓存策略直接关乎用户体验和发布效率。
- 逻辑被基础设施封装了:你没手动写过ETag的逻辑,不代表它不存在。Tomcat、Nginx、Spring Boot在默认情况下就已经为静态资源处理了这些。CDN的核心工作就是缓存。它们都在底层默默工作,开发者无需感知。
5.2 哪些地方在大规模使用?
事实上,协商缓存无处不在:
- 所有大型网站的前端资源:淘宝、京东、抖音的JS、CSS、图片,绝大部分请求都通过304状态码命中缓存。
- CDN与对象存储:阿里云OSS、腾讯云COS等服务,默认就会为你存储的文件生成ETag和Last-Modified。
- 公共API服务:GitHub API、各种地图API、天气API等,广泛使用ETag来避免重复传输相同数据,节省双方资源。
5.3 后端开发到底需要做什么?
对于大多数后端开发者而言,你需要做的不是从零实现缓存逻辑,而是:
- 正确配置:根据资源类型,在Nginx、Spring Boot或网关中配置正确的
Cache-Control等头部。 - 按需开启:对合适的动态接口(如字典、配置)启用ETag过滤。
- 会排查问题:当用户反馈“看到的是旧页面”时,能打开开发者工具,看懂网络请求的缓存状态,知道问题是出在强制缓存时间太长,还是协商缓存失效。
六、缓存最佳实践与避坑指南
- 优先使用强制缓存:对于能确定版本的文件(如带哈希的资源),用
max-age,性能最好。 - 入口文件慎用强制缓存:HTML文件应使用
no-cache或较短的max-age,确保用户能获取到最新版本。 - 不要缓存POST请求:POST通常是非幂等的写操作,缓存它们会导致数据不一致。
- 更新即改名:更新静态资源时,最好改变其文件名(通过哈希),而不是依赖浏览器协商缓存更新。这是最彻底的缓存失效策略。
- 善用CDN:结合CDN时,理解
public、private、s-maxage等指令的含义。 - 线上问题,先看缓存:遇到样式没更新、数据不对的问题,首先排查的应该是缓存配置,十有八九是这里出的错。
总结
说到底,协商缓存不是什么高深莫测的黑科技,也不是只存在于面试题里的概念。它是HTTP协议的基础组成部分,是构建高效、可扩展Web应用的基石之一。从浏览器到CDN,从静态资源到动态接口,缓存的思维无处不在。
理解强制缓存与协商缓存的原理,能让你在性能优化时有的放矢,在排查问题时快速定位。记住那句话:在计算机科学中,缓存是最难的问题之一,但一旦用好,它带来的收益也是最高的之一。
相关攻略
近期Cursor更新后,开发者普遍反映其AI编程能力下降,表现为规划缺失、代码质量粗糙、Token消耗激增及自定义功能失效。问题根源在于官方为提升速度与控制成本,削弱了深度推理与规划能力。通过调整设置可部分恢复原有性能,包括强制开启完整规划、锁定强模型、关闭自动裁剪及优化使用习惯。
使用智谱清影生成视频时,为精准控制画面光照,需提供具体描述而非抽象词汇。应详细说明光源位置、光线特征及环境交互,如太阳角度、色温或大气效果。还可借助ControlNet光照图控制明暗结构,或在提示词中指定色温、光强等参数,以实现理想的光影效果。
在“动荡时空V”活动期间,获取坐骑“维拉诺兹之裔”所需完成的周常任务周数由五周减至四周。玩家需通过时光漫游地下城累计获得四次“时空掌握”状态即可达成成就。活动持续五周,此调整降低了获取门槛,为玩家提供了更灵活的完成空间。
现货白银价格日内跌幅达4 3%,报每盎司73 63美元。市场情绪转向谨慎,可能受技术卖压及多重因素影响,包括美元走势、实际利率与工业需求等。短期波动考验投资者判断,需保持冷静并关注核心逻辑变化。
IOI工作室新作《007:初露锋芒》已开放预购与抢先体验。游戏引入特殊互动机制:任务中女性NPC会主动接近邦德制造干扰,玩家需选择应对方式。该设计旨在融合角色魅力、心理博弈与任务节奏,强化沉浸感。游戏版本正持续优化,机制也将进一步完善。
热门专题
热门推荐
Quriosity产品介绍:一站式知识探索与终身学习平台 在信息过载的当下,如何高效、系统地获取高质量知识,并保持持续学习的动力,是许多人面临的挑战。Quriosity 正是为解决这一需求而生的综合性知识探索与学习资源平台。其核心目标在于帮助用户打破学科壁垒,构建系统化的知识体系,从而实现认知水平的
ClaudeCode采用三层异步记忆架构解决AI信息遗忘问题:短期会话记忆维护结构化笔记;中期记忆在每轮对话后持久化关键信息至磁盘;长期记忆跨会话整合与修剪记忆。通过后台分叉代理异步操作,系统实现记忆按需加载与动态优化,使AI成为具备持续记忆能力的协作搭档。
驾驭工程通过优化AI运行环境来提升效能,其核心包括构建动态知识库、实施架构约束和管理技术债务。这一理念将焦点从优化输入转向优化系统环境,强调环境设计是提升工程效能的新杠杆。智能体的失败常揭示环境缺陷,通过持续改进环境可实现更可靠、高效的AI应用。
对于开发者而言,选择GitHub Copilot还是Tabnine,关键在于理解其底层架构如何影响编码效率、数据安全与响应性能。本文将从通信路径、本地推理、云端调用、上下文理解及私有化部署五大核心维度,为你提供一份详尽的实测对比指南,助你做出精准决策。 一、通信路径与推理位置深度解析 GitHub
南京举行纪念中国航天事业创建70周年主题活动,通过史料、展览和讲座回顾钱学森光辉一生与中国航天奋斗史诗。钱永刚教授作讲座并赠书,现场设六大主题展及互动体验,展现钱学森卓越贡献与航天精神。





