计算机网络 23:缓存返回的还是同一份资源吗,新鲜度、验证、Vary 与条件请求
同一个 URL 连续返回 200,能否证明每次都访问了源站?过期后收到 304,是否意味着正文为空?把响应状态、正文来源和缓存决策混在一起,会同时误判这两个问题。缓存可以直接生成带正文的 200,也可以在上游验证成功后继续使用已有正文。
第 22 篇解决消息边界。本篇在两个回环 HTTP 服务之间加入受限教学缓存,分别记录客户端、缓存与源站的交换,研究新鲜度、验证器和表示变体。实验不使用浏览器缓存或公共 CDN。
同一资源可以有不同表示
URL 标识目标资源,不保证每次选中的表示逐字节相同。请求头、协商条件以及资源随时间的变化都可能影响结果。例如同一路径按 Accept-Language 返回中文或英文;只按路径保存一份正文,会让后来的另一种语言请求取得错误变体。
缓存保存的对象也不仅是正文。后续复用至少需要关联请求条件、响应字段、验证器和时间信息。丢失 Vary 信息会混用变体;丢失 Date 与 Age 会误算有效期;收到 304 后丢失旧正文则无法构造完整响应。
这里分开回答两个问题:当前请求能匹配哪份已存响应,以及这份响应能否直接复用。变体匹配正确,不等于新鲜;时间尚新鲜,也不等于适合另一个请求的语言。
新鲜度是复用条件,不是源站未变化的证明
RFC 9111 §4.2用新鲜寿命与当前年龄比较,基本判定为 freshness_lifetime > current_age。等号不满足这个条件。源站可以在寿命内发生修改,但已保存响应仍可能按缓存政策被复用;新鲜不是每次都重新核对内容的结果。
显式寿命按适用规则取得:共享缓存先考虑 s-maxage,其次 max-age,再考虑 Expires 与 Date 的差。并非所有缓存都把所有字段取最大值;no-cache 等约束还可能要求即使年龄很小也先验证。本实验只实现自己的固定响应指令,不把这个简化程序当作完整缓存标准实现。
Age 不能简单等于“进入这一层缓存之后过了多久”。上游已经缓存的时间和响应传输时间也会影响当前年龄。RFC 9111 §4.2.3给出计算方式;下面采用其中较保守的初始年龄估计,数值是手算输入而非网络测量:
1 | |
只计算本地驻留时间会得到 5 秒,从而可能把寿命 10 秒的条目误判为新鲜。真实实现还要区分墙上时钟与单调时钟:Date 是带日期的时间,驻留和等待可以用单调时钟,不能把两种时间值直接相减。实验保存相关输入,便于独立复算。
过期以后,先问验证器是否仍匹配
ETag 是源站给表示分配的不透明验证器,不要求是内容哈希。RFC 9110 §13.1.2规定 If-None-Match 的条件语义及弱比较。对 GET/HEAD,条件不满足时可以得到 304;其他方法的条件失败处理不同,不能一律返回 304。
缓存持有一个已过期条目时,可以向源站发送带其 ETag 的条件请求。若当前表示仍与该验证器匹配,源站省去正文传输;若已经变化,则返回新的完整表示。过期只意味着不能沿原有新鲜度判断直接复用,不证明内容必然改变。
弱验证器允许语义等价而字节不同的情况,所以不能把所有标签匹配都表述为字节级身份。实验使用明确版本和语言对应的强标签,只比较本次保存的实际正文,不推广为所有 ETag 的生成算法。
上游 304 与下游 200 可以同时成立
1 | |
这是缓存主动验证时的一种路径。客户端没有声明持有某个版本,不能把上游无正文的 304 原样当作它所请求的完整表示交付。缓存给客户端构造的 200 使用已有正文,验证流量发生在另一条连接上。
RFC 9111 §4.3.4要求先确定哪些存储响应可以被更新,不能拿任意 304 刷新任意条目。确认验证器与候选条目关系后,再按RFC 9111 §3.2处理字段更新;并非无条件覆盖所有字段。正文仍取自原条目,不能被“304 正文为空”替换成空数据。
因此,日志应至少分别记录上游状态、下游状态、请求条件和正文摘要。只看到客户端 200,无法区分冷查询、命中、成功验证或完整更新;只看到源站 304,也无法证明客户端最终获得了可用正文。
Vary 把请求条件带入选择过程
RFC 9111 §4.1要求比较 Vary 指定的请求字段。若响应含 Vary: Accept-Language,缓存就需要保存产生该响应的原请求语言条件,并在后续选择时匹配。缺失字段只能与同样缺失的情形匹配,不能擅自认为缺失等同于某个显式值。
Vary: * 总不能匹配后续请求。字段值的归一化应遵循该字段的语义,不能把所有值一律转小写或随意排序。实验仅支持固定的 zh、en 与缺失三种输入,不实现完整语言优先级列表和 q 值协商。
对于固定源站、固定 GET 方法、固定路径的本地实验,键可以显式包含路径及 Accept-Language 原值。这个键的成立依赖上述限制;不能照抄为任意代理的全局缓存键,从而遗漏源站、方法或其他 Vary 条件。
no-cache 和 no-store 的差别可以直接观察
本篇只讨论不带参数的响应指令。no-cache 允许保存响应,但复用前必须成功验证;no-store 禁止将对应请求和响应存入缓存并用于后续请求。它们不会因为名字都带 no 就产生相同效果。must-revalidate 又不同,主要约束已过期条目的复用。RFC 9111 §5.2.2分别定义这些指令及例外条件。
验证 no-cache 不能只数源站访问次数,还要检查第二次是否携带已保存验证器,以及 304 后是否保留正文。验证 no-store 则需检查条目没有被存入、下一次仍重新取数。记录响应正文用于实验审计,与把它放入可复用缓存表是不同操作;附件仅含合成测试内容。
这些指令也不等同于加密或数据保密保证。本篇没有传输敏感内容,不根据 no-store 推断链路不可被观察。
本机双服务实验
记录环境为 macOS 27.0、Python 3.14.4。源站与缓存分别监听 127.0.0.1 的随机端口,客户端仅请求这个缓存,缓存仅访问固定源站。每次运行重新分配端口,不改系统代理、DNS 或路由。
源站对 /cache 返回 max-age=2、语言相关的 ETag 和 Vary: Accept-Language。驱动改变受控源站版本,并实际等待已存响应的单调时间加 3.1 秒;没有把时间变量直接改成“过期”。附件记录如下:
| 请求阶段 | 源站累计请求数 | 上游结果 | 客户端实际正文 |
|---|---|---|---|
| 中文冷请求 | 0 → 1 | 200 | 你好 v1 |
| 中文新鲜命中 | 1 → 1 | 未请求源站 | 你好 v1 |
| 中文过期,内容未变 | 1 → 2 | 304,无正文 | 你好 v1 |
| 修改内容后再次过期 | 2 → 3 | 200 | 你好 v2 |
| 英文、缺失语言各自冷请求 | 3 → 4 → 5 | 各一次 200 | hello v2、default v2 |
| 英文、缺失语言、中文各自命中 | 5 → 5 → 5 → 5 | 均未请求源站 | 各自原正文 |
| no-cache 首次与再次请求 | 5 → 6 → 7 | 200、304 | 均为你好 v2 |
| no-store 两次请求 | 7 → 8 → 9 | 两次 200 | 均为你好 v2 |
13 次下游响应全部是 200,却包含直接命中、上游 304 和上游完整更新三类不同过程。源站请求记录、缓存决策和客户端正文逐次对应,才能确定哪些数据来自缓存。
第一次过期前的年龄约 3.758 秒,第二次约 3.883 秒,都超过 2 秒寿命。no-cache 第二次请求的年龄约 0.902 秒,仍然发起条件验证;no-store 两次的 stored 均为 false。Date 只有秒精度,因此刚存入的年龄也可以非零,不能要求它恰好为 0。
独立复跑得到相同的状态与计数分支;另按日志输入重算全部 13 个年龄,并核对下游 Content-Length 与实际正文的字节数。这里记录的是应用层 HTTP 交换,没有据此推断 TCP 分段、公网命中率或节省的链路带宽。
复现与练习
1 | |
需要 Python 3.11 或更新版本及本机回环监听权限。默认运行包含离线自检、两次真实等待和服务清理;--self-check 只验证年龄公式、等号边界、304 保留正文及错误 ETag 拒绝,不启动网络。日志末尾的 owned_threads_stopped 与 owned_servers_closed 记录退出清理。
程序只接受固定 GET 路径及 zh、en、缺失三种语言条件,下游限定无条件请求。源站只处理单个强 ETag 的精确匹配,没有实现标准要求的完整弱比较、标签列表或通配符处理;304 元数据合并也依赖本实验固定字段集。正文中的协议规则比附件实现范围更广,不能把示例复制后称为通用代理。
一份条目 max-age 为 30 秒,当前年龄为 30 秒,是否满足本篇的新鲜判断?不满足严格的大于关系。若上游 Age 已是 20 秒,本地才保存 1 秒,能否报告 Age 为 1?不能,必须考虑进入本层前的年龄及响应延迟。
相同路径的 zh 与 en 请求只改变一个字段,为什么仍可能需要两份缓存条目?因为 Vary 指定的条件影响表示选择。若源站漏发 Vary,不能假定缓存能从正文自动猜到语言分支。
源站返回 304,下游无条件请求却收到带原正文的 200,是否矛盾?不矛盾,两条连接承担不同任务;前者验证已有表示,后者向客户端交付完整表示。需要核对正文摘要和元数据更新,不能仅凭状态码判断实现正确。
一手资料与验证边界
写作当次核查 RFC 9111 的缓存匹配、新鲜度、年龄、验证和响应指令,以及 RFC 9110 的条件请求和验证器语义。对应链接在正文。实验仅证明自建教学缓存的限定路径,未验证公共 CDN、浏览器缓存、共享认证、范围请求、启发式寿命、故障时陈旧服务或生产并发行为。





