同一个订单,为什么有三种访问结果?
订单 O204 创建后,客户端打开详情。第一次下载了 JSON;过一会儿再次打开,没有向服务器发送这份资源的请求;再过一会儿,发了请求,却只收到 304 和一些字段。
这不是三套接口。HTTP 缓存先判断有没有可用的已存响应,再判断能否直接复用;需要验证时,才询问服务器已有版本是否仍适用。 缓存减少的是重复获取成本,不能保证看到业务的实时状态。
本章用设定的 HTTP/1.1 报文和时间线推导,不声称运行了订单服务或抓到了真实浏览器流量。为了聚焦,假设用户身份不变、订单表示不变、缓存键匹配,没有主动要求验证;缓存允许保存,初始年龄为 0,忽略网络耗时与时钟误差。实际刷新行为稍后单独讨论。
三轮请求,分别省了什么?
第一轮,客户端还没有这份详情,发送:
GET /orders/O204 HTTP/1.1
Host: shop.example.com服务器在 08:00:00 返回:
HTTP/1.1 200 OK
Date: Fri, 09 Oct 2026 08:00:00 GMT
Cache-Control: private, max-age=60
ETag: "O204-v1"
Content-Type: application/json
Content-Length: 37
{"orderId":"O204","status":"created"}private 限定共享缓存不能保存,用户自己的私有缓存仍可按规则保存。max-age=60 给出 60 秒新鲜期。ETag 是服务器为这个表示生成的验证标识;这里的 "O204-v1" 是教学约定,不是浏览器自动从订单号推算出来的值。
第二轮在 08:00:30:当前年龄为 30 秒,小于 60 秒。在本例条件下,缓存直接提供已存的 200 响应和正文;没有向服务器发送本资源请求,也没有线上 304 响应。 开发者工具可能标记来自 memory cache 或 disk cache,那是本地来源提示,不是新的 HTTP 状态码。
第三轮在 08:01:10:当前年龄为 70 秒,已经过期。缓存有 ETag,于是发出条件请求:
GET /orders/O204 HTTP/1.1
Host: shop.example.com
If-None-Match: "O204-v1"服务器发现所选表示仍匹配,返回:
HTTP/1.1 304 Not Modified
Date: Fri, 09 Oct 2026 08:01:10 GMT
Cache-Control: private, max-age=60
ETag: "O204-v1"304 没有消息体。缓存用验证结果更新已存响应的相关元信息,再把已有正文用于此次访问。应用得到可用的数据,不意味着线上重新传输了 JSON。RFC 9110:304 与 If-None-Match
查看 Mermaid 源码
sequenceDiagram
participant A as 客户端 A
participant C as A 的私有缓存
participant S as 订单服务 S
A->>C: 08:00:00 获取 O204 详情
C->>S: GET /orders/O204
S-->>C: 200 + 正文 + max-age=60 + ETag v1
C-->>A: 提供正文并保存响应
A->>C: 08:00:30 再次获取
Note over C: 年龄 30 秒,可直接复用
C-->>A: 已存正文,不请求 S
A->>C: 08:01:10 再次获取
C->>S: GET + If-None-Match v1
S-->>C: 304 + 更新的缓存字段,无正文
C-->>A: 更新元信息后提供已有正文| 本例轮次 | 是否请求 S | S 的响应 | 节省的内容 |
|---|---|---|---|
| 第一次获取 | 是 | 200 + 正文 | 尚无可复用响应 |
| 新鲜期内再次获取 | 否 | 没有线上响应 | 本资源的一轮交互及正文传输 |
| 过期后验证且未变 | 是 | 304,无正文 | 本次正文传输;仍有请求与处理成本 |
面试里常把第二种叫“强缓存”,第三种叫“协商缓存”。这两个说法方便交流;更准确的判断对象是新鲜度与验证,不是“缓存位于内存还是硬盘”。私有缓存与共享缓存都可能支持这些机制。
如果第三轮所选表示已经变成 v2,正常条件 GET 将返回 200、更新后的字段和新正文,而不是 304。缓存没有保存可用正文时,单拿到一份 304 也无法凭空还原 JSON。
动手走一遍复用与验证
先在第 30 秒获取,再在第 70 秒获取;随后更新服务端版本。重置后,在新鲜期内先更新服务端再获取,看看客户端为何仍可能看到旧内容。最后比较 no-cache 与 no-store。
首次GET已返回v1。切换策略会重置这一轮实验;再推进时间、更新服务端或再次获取。
当前时间:第 0 秒
ETag: "O204-v1"
状态:created
当前年龄 0 秒
仍新鲜
有请求
下载 v1 正文
本次提供的表示:v1 · created
首次获取,没有已有响应;本策略允许私有缓存保存。
私有缓存、固定用户与GET目标、初始/成功验证后年龄为0,忽略网络耗时和时钟误差;服务端正确维护ETag。无主动刷新、Serve Stale、Service Worker、Vary或共享缓存。推进时间与更新服务器不会自动发请求,缓存新鲜不代表业务实时;不访问真实服务。
60 秒从什么时候开始算?
不要把 max-age 理解成“每个浏览器收到时重新倒计时 60 秒”。响应可能已经在共享缓存里待过一段时间,也可能花了时间经过网络。
协议根据 Date、Age、收发时间等计算年龄。Age 描述响应从生成或成功验证以来的估计年龄;缓存还需计入本地驻留时间。RFC 9111:年龄计算
用一个已经算好初始年龄的例子:某公共响应的校正初始年龄为 20 秒,收到后在本地待了 15 秒,则当前年龄为 20 + 15 = 35 秒。若新鲜期为 60 秒,还剩 25 秒。继续待到本地驻留 40 秒,年龄达到 60 秒,不再满足 当前年龄 < 新鲜期。
这只是年龄算例,不把订单响应的 private 与公共缓存混用。第一节三轮时间线特意设初始年龄为 0,因此才能直接用经过的 30 秒、70 秒判断。
Expires 给出一个绝对到期时间。适用的 max-age 优先于 Expires;对共享缓存,适用的 s-maxage 又优先于 max-age,并要求过期后成功验证再复用。没有显式新鲜期也不必然等于“完全不缓存”,某些响应可以按规则计算启发式新鲜期;需要明确行为时,服务端应给出清晰策略。
Cache-Control 的名字,不能只按字面翻译
下表讨论响应字段里无参数的 no-cache/private 等常见写法,不扩展到字段名限定形式。
| 响应指令 | 核心含义 | 常见误读 |
|---|---|---|
| max-age=60 | 给出响应的新鲜期 | 每次收到都从零开始计龄 |
| s-maxage=300 | 共享缓存使用的最大年龄,覆盖适用的 max-age/Expires | 浏览器也一定直接用 300 秒 |
| no-cache | 可以保存,但复用前必须成功验证 | 完全禁止存储 |
| no-store | 要求缓存不保存本次响应 | 自动擦除所有历史副本和其他业务存储 |
| private | 共享缓存不能保存,私有缓存仍可按规则保存 | 响应已经加密,或完全不能缓存 |
| public | 明确允许缓存,仍需满足相应规则 | 所有人应获得同一份用户数据 |
| must-revalidate | 过期后未成功验证,不得继续复用 | 即使新鲜也每次验证 |
因此 Cache-Control: no-cache 配 ETag 很有用:保存正文,但下次使用前询问版本是否仍适用。max-age=0 表示马上不新鲜,不等于 no-store。RFC 9111:响应缓存指令
服务器不能靠 no-store 保证所有组件都不会留下数据。日志、应用数据库、浏览器其他保存机制,以及不遵守规则的缓存,都需要分别管理。对登录用户的订单,也不能因为 GET 就盲目 public;授权校验、身份切换与缓存策略要一起设计。本章的 private、60 秒是教学设定,不是推荐所有订单状态都允许旧 60 秒。
还有两个常见扩展:stale-while-revalidate 允许在限定窗口内先用旧响应、后台验证;stale-if-error 允许在符合条件的错误下有限度地使用旧响应。它们是有条件、有时间边界的陈旧响应复用,不等于“过期内容永远可用”,也不能忽略更严格的限制。RFC 5861:陈旧响应扩展
ETag 和 Last-Modified:验证的是什么?
ETag 是表示的验证标识,可以由版本号、摘要或其他规则生成,协议不要求一定是 MD5。引号是字段语法的一部分;强标签例如 "v1",弱标签例如 W/"v1"。强弱验证反映可比较的精度,不能把弱标签理解为“没有用”。
条件 GET 的 If-None-Match 使用弱比较,可以在语义等价时复用缓存。要求字节级相同的场景或防止并发覆盖时,验证要求可能更严格;不要把一个弱标签用于所有条件请求。RFC 9110:验证标识与条件请求
Last-Modified 是服务器报告的修改时间,对应验证请求字段 If-Modified-Since。时间通常精确到秒,不同修改可能落在同一秒;服务端还要正确处理自己的表示与时间。它不天然比 ETag 更“真实”。
如果请求同时有 If-None-Match 和 If-Modified-Since,服务器按条件评估规则优先处理 If-None-Match,不再用 If-Modified-Since 作为第二道独立裁决。也不能背成“任意方法验证匹配都返回 304”:本章讨论 GET;其他方法的条件失败可以返回 412。RFC 9110:条件评估顺序
同一个 URL,为什么还需要 Vary?
假设公共指南 GET /guides/network 根据 Accept-Language 返回中文或英文。如果缓存只按 URL 找响应,英文请求可能拿到之前存的中文内容。
服务器可以声明:
Vary: Accept-Language缓存选择已存响应时,要把 Vary 指出的请求字段纳入匹配。简化地看,这个例子至少有两份候选:
| 请求目标 | 本例请求字段 | 对应的已存表示 |
|---|---|---|
| /guides/network | Accept-Language: zh-CN | 中文指南 |
| /guides/network | Accept-Language: en | 英文指南 |
实际匹配还包含方法、目标 URI 等规则,字段比较也不是把整条原始文本盲目拼起来。Vary 是额外的选择条件,不会自行提供新鲜期、保证可缓存,或自动按登录用户隔离数据。RFC 9111:Vary 与缓存键
Vary: Accept-Encoding 常用于压缩表示的选择。Vary: * 则使缓存无法仅按常规请求字段匹配复用。字段维度越多,候选表示通常越分散;应声明真正影响响应的字段,而不是把所有请求头都塞进去。
为什么按了刷新,还会看到不同结果?
“三轮时间线”解释一般获取流程,不保证普通刷新、强制刷新、开发者工具禁用缓存都走同一条路径。客户端也可以发送请求侧 Cache-Control,要求验证或约束缓存使用。
在 Chrome DevTools 的 Network 面板里,Disable cache 可以在 DevTools 打开时禁用浏览器缓存,适合观察请求;这不等于清空 CDN 的缓存。Chrome 官方:禁用浏览器缓存
看结果时应同时检查请求字段、响应字段、缓存来源、Age,以及有没有真实网络请求。HTTP 缓存、Service Worker 自己管理的缓存和页面脚本的状态,也不是同一个存储机制,不能只凭一个“没下载”的现象认定命中了 HTTP 新鲜缓存。
版本化资源可以使用长新鲜期,例如文件名包含内容摘要;内容变化时换新 URL。immutable 告诉客户端在新鲜期内表示不会改变,可减少不必要的验证,不会把过期响应变成永久新鲜。若把会变化的文件放在固定 URL 下并给很长新鲜期,服务器发布新版不意味着已存缓存立刻获知。RFC 8246:immutable
30—60 秒参考回答
HTTP 缓存要先判断响应能否保存、请求是否匹配,再看年龄与新鲜期。新鲜且允许复用时,可以直接用本地响应,省掉本资源的请求;需要验证时,可发送 If-None-Match 或 If-Modified-Since,未变则用 304 更新元信息并复用已有正文,变了则获取新正文。no-cache 允许保存但使用前要验证,no-store 要求不保存;private 限制共享缓存。ETag 是验证标识,Vary 用于区分同一目标的不同表示。缓存策略必须考虑身份与可接受的陈旧时间,不能把 GET 等同于随意共享。
四个追问及解析
1. 命中“强缓存”,服务器返回了什么状态码?
在本章第二轮中,没有向服务器发送本资源请求,所以没有服务器新返回的状态码。工具展示的 200 可以来自已存响应,memory cache/disk cache 表达来源。先判断有没有网络交互,再判断状态码归属。
2. 为什么 304 比完整下载便宜,但不一定比直接复用快?
304 不带正文,节省传输量;验证仍需要请求、服务器或上游缓存处理以及返回响应。直接复用本地响应可以免去这轮交互。具体时间还受 RTT、连接复用和验证成本影响,不能给出固定提升倍数。
3. 设置了 no-cache,为什么硬盘里还看得到缓存?
no-cache 的核心约束是复用前成功验证,不是禁止存储。no-store 才表达不保存的要求。即便 no-store,也不能据此承诺历史副本、应用保存或其他机制都已清除。
4. 服务器已经更新订单,客户端为何仍显示 created?
在允许直接复用的新鲜期内,客户端未必联系服务器。也可能缓存键配置、验证标识更新或中间层策略有问题。先核对请求与响应证据,再判断;如果要求及时看到状态,就应调整验证策略与刷新机制,而不是只增大缓存时间。
两个易错辨析
“过期就是正文已经错了。” 过期只表示不再满足新鲜度要求;验证后可能仍是原版本,因此返回 304。表示是否改变与年龄是否超期,是两个判断。
“缓存一定只存 200,404 不会缓存。” 某些其他状态也可以按规则缓存,404 就可能被缓存。修复路径后仍看到旧的不存在结果,需要检查缓存策略与已存响应,不能只看源站当前数据。RFC 9111:可保存响应与启发式新鲜度
下一章从用户登录后访问订单出发,解释 Cookie、Session、Token 怎样关联多次请求,以及跨域与浏览器安全策略的边界。