HTTP 缓存 从新鲜度到条件请求
从一个具体请求或状态变化出发,追踪数据流与方法成立的条件。
章节速览8 节 · 展开选择
网络基础解释一次请求怎样到达服务器,LRU解释空间不足时淘汰哪个条目。HTTP 缓存还要回答另一个问题:条目虽然仍在,下一次请求能否直接使用它?
答案依赖表示的身份、缓存指令、年龄和验证结果。先把这几个条件分开,就不会把“命中缓存”“没有下载正文”和“没有发送请求”混成同一件事。
先固定这篇文章的范围
本文以私有缓存、GET、完整 200 响应、显式 max-age 为主线,服务器提供 ETag 时使用条件验证。图和时间数值均为自拟教学例子。内部状态模型不实现共享代理的全部规则、启发式新鲜度、范围响应、缓存请求指令或允许过期复用的扩展,不把一次模型核验当作真实浏览器兼容性测试。
协议依据为 RFC 9111:HTTP Caching 和 RFC 9110:HTTP Semantics。私有缓存服务单个用户,共享缓存还涉及 private、s-maxage、认证等额外约束;不能直接把本例当作 CDN 配置。
一份缓存要过四道判断
| 问题 | 看什么 | 不能混淆的概念 |
|---|---|---|
| 能否保存 | 请求方法、状态及存储限制等 | 能传输不等于能保存 |
| 是否是本次请求的表示 | 方法、目标 URI、Vary 指定字段 | 同一 URL 也可能有不同语言版本 |
| 是否仍新鲜 | freshness_lifetime 与 current_age | 还在内存里不代表新鲜 |
| 需要验证时怎样处理 | 验证器、条件请求及响应 | 304 不提供新正文,但需要已有正文 |
RFC 9111 §4列出了复用的前提。因此,单看文件名、磁盘上有没有文件或 LRU 是否命中,都不足以决定 HTTP 的复用行为。
flowchart TD
A[收到 GET 请求] --> B{找到允许保存且变体匹配的条目}
B -- 否 --> C[向源站请求表示]
B -- 是 --> D{允许直接复用且仍新鲜}
D -- 是 --> E[使用已有正文]
D -- 否 --> F{有可用验证器}
F -- 是 --> G[发送条件请求]
F -- 否 --> C
这张图省略了显式允许过期复用等分支,不表示 RFC 中所有过期响应都必然走同一条路径。
max-age 给寿命,Age 不是此刻的完整年龄
设响应包含 Cache-Control: max-age=60。这里的 60 是新鲜寿命,不是“无论何时收到都再保鲜 60 秒”。响应到达本地前,可能已经在源站或上游缓存经历时间。
按 RFC 9111 §4.2.3,用同一套秒单位与时间基准记。这里采用同时考虑 Date 与 Age 的较保守计算:
1 | apparent_age = max(0, response_time − date_value) |
request_time 是发出请求的时刻,response_time 是收到响应的时刻;date_value 来自 Date,age_value 来自 Age,缺少 Age 时按 0 处理。Date 与本地时钟偏差也会影响 apparent_age。规范也允许直接把 corrected_age_value 用作 corrected_initial_age;若路径上存在可能不正确写入 Age 的旧缓存,可取两者最大值。本文始终使用后一种较保守方式,并不把它说成唯一允许的实现。
新鲜判断是 freshness_lifetime > current_age,相等时已经过期。本例私有缓存的寿命取 max-age;更完整的选择顺序、Expires 与启发式规则见 §4.2.1。
一条时间线算出为什么还剩十八秒
取请求发出时刻 100、收到响应时刻 104、响应 Date 对应 90、Age 为 12,当前时刻 130,max-age 为 60。这里用相对秒代替真实 HTTP 日期,便于手算;实际 Date 字段仍需使用 HTTP 日期格式。
| 量 | 计算 | 结果(秒) | 作用 |
|---|---|---|---|
| 表观年龄 | max(0,104−90) | 14 | Date 提供的年龄估计 |
| 校正 Age | 12+(104−100) | 16 | 计入请求响应耗时 |
| 校正初始年龄 | max(14,16) | 16 | 收到时的年龄估计 |
| 本地驻留 | 130−104 | 26 | 收到之后经过的时间 |
| 当前年龄 | 16+26 | 42 | 与寿命比较 |
| 新鲜余额 | 60−42 | 18 | 仍满足60>42 |
若只算 Age 12 加驻留 26,会得到 38,漏掉了本例的请求耗时。若从收到时重新计满 60,又会错把已有的 16 秒年龄抹掉。保持其他条件不变,到时刻 148 时 current_age=60,恰好过期。
五个常见指令,各自管哪一道门
以下讨论响应中的未带字段名限定的指令,依据 RFC 9111 §5.2.2。
| 响应指令 | 本文私有缓存里的含义 | 典型误读 |
|---|---|---|
| max-age=60 | 明确新鲜寿命为60秒 | 收到后无条件再计60秒 |
| no-cache | 可以保存;复用前必须成功验证 | 等于不保存 |
| no-store | 不保存本次请求/响应的相应信息 | 只是每次验证一下 |
| must-revalidate | 过期后,成功验证前不得复用;断连也不能忽略 | 只要带它,每个新鲜响应也要验证 |
| private | 共享缓存不得保存该响应;私有缓存仍需满足其他条件 | 所有缓存都禁止保存 |
no-store 不是安全擦除机制,也不使过去已经合法保存的所有旧副本自动消失。no-cache 则关心复用前的验证。对于过期复用,规范还有显式授权与扩展机制;本篇教学模型统一不实现离线过期回退。
ETag 与 304 怎样一起工作
初次获得表示的报文可以包含:
1 | HTTP/1.1 200 OK |
后续需要验证时,请求携带已保存的验证器:
1 | GET /guide HTTP/1.1 |
服务器判断所选表示与条件匹配时,GET 可得到 304;不匹配则通常得到包含新表示的 200。这里验证的是所选表示,不是根据 ETag 的字面名称猜版本先后。
对 GET/HEAD,If-None-Match 使用弱比较;W/"v1" 与 "v1" 的 opaque-tag 相同即可匹配。它与要求强比较的 If-Match 不可混用。请求同时带 If-None-Match 与 If-Modified-Since 时,前者存在就忽略后者,见 RFC 9110 §13.1.2 与 §13.1.3。
收到 304 后,缓存按 RFC 9111 §4.3.4识别应更新的已存响应,再按 §3.2更新适用字段。新提供的 Date、ETag、Cache-Control 等需要按规则合并;不能无条件更新同 URL 的任意条目,也不能把全部旧头清空。
304 本身不带表示正文,RFC 9110 §15.4.5定义了它的语义。客户端继续使用相应已有正文;年龄则结合本次验证响应重新计算,不能简单写成“304 后年龄必为 0”。例如验证请求160发出、164收到、Date为160、Age为0,收到时校正年龄就是4秒。
flowchart LR
A[缓存条目需要验证] --> B[发送 If None Match]
B --> C{源站响应}
C -- 304 --> D[选中匹配条目 保留已有正文]
D --> E[更新适用字段 重算年龄]
C -- 200 --> F[使用新正文与元数据]
E --> G[按新指令决定后续保存和复用]
F --> G
Vary 让同一地址有多个候选
若保存响应含 Vary: Accept-Language,下一次请求选择候选时还要比较该字段。zh-CN 的正文不能仅因 URL 一样就拿去满足 en 请求。头字段缺失与存在也是不同情况,具体匹配还要考虑规范允许的归一化;本篇内部模型只使用已经规范化的字符串。
Vary 是响应对选择条件的声明,并不是让缓存自动理解“这些正文差不多”。Vary: * 表示无法以这种匹配方式找到可直接复用的存储响应,详见 §4.1。隐私隔离、认证身份和缓存分区仍有各自规则,Vary 不能代替完整的权限设计。
边界、自检与内部核验
| 场景 | 应检查的结果 |
|---|---|
| current_age 恰好等于 max-age | 已经过期 |
| 条目新鲜但响应有 no-cache | 仍需验证 |
| 条目新鲜且仅有 must-revalidate | 该指令本身不额外强制此刻验证 |
| 过期且 must-revalidate,源站不可达 | 不能擅自复用过期响应 |
| 304 但没有能识别的已有表示 | 不能凭空产生正文 |
| 200 提供新表示且带 no-store | 可以处理本次响应,但不把它作为新缓存条目保存 |
| Vary 字段不匹配 | 不能使用这一候选满足请求 |
自检:缓存命中且返回给应用的页面来自本地正文,是否就说明没有网络请求?
不一定。条件验证可以经过网络得到304,再复用已有正文。“没有重新传正文”与“没有发送请求”是两个判断。
自检:收到304,能否把任意旧副本的年龄设为0?
不能。先按请求选择与验证器规则识别已有表示,再更新适用元数据。年龄包含本次响应的Date、Age和请求耗时,收到时也可能已经大于0。
tests/verify-http-cache-article.cjs 检查本文的时间算术与限定状态模型:独立时间轴公式、严格新鲜边界、指令与变体组合、条件匹配和已选条目的304/200更新。模型只对单一已选候选、同一强 ETag 演示304更新;弱比较另外检查,不实现弱验证器与多个候选的完整更新选择规则。它没有实现完整缓存产品,也没有把教学测试称为浏览器或 CDN 的协议认证。
回到 LRU 的复用距离,可以把两个问题合在一张笔记上:容量策略决定条目有没有被留下;HTTP 规则决定这个条目此刻能怎样使用。再结合网络请求过程,就能把直接复用、条件验证和完整下载放进同一条请求链路。