Skip to content

缓存与条件请求

本章导读:缓存是 REST 六大约束里"免费性能"的来源(RFC 9111),也是私有 API 最常见的裸奔区——要么完全禁掉错过红利,要么配错导致用户 A 看到用户 B 的数据。本章按"强缓存 → 协商缓存 → 失效策略 → 缓存键安全"四层展开。

1. 全景:两级缓存模型

                 ┌─ 强缓存(不访网络): Cache-Control: max-age → 命中直接返回
客户端/CDN ──────┤
                 └─ 协商缓存(访网络问一句): ETag/Last-Modified → 304 省带宽
源服务器:数据变更 → 缓存失效机制(改 Cache-Control 键 / 主动 purge / 短 TTL)

2. 强缓存:Cache-Control(RFC 9111 §5.2)

响应侧指令:

指令含义API 场景
max-age=60新鲜度 60 秒公开配置、热搜列表
s-maxage=3600共享缓存(CDN)专用时长文章详情给 CDN 缓存 1 小时
public响应可被任何缓存存储(含共享缓存)仅公开数据
private仅允许客户端私有缓存,CDN 必须拒存用户私有数据默认值
no-store任何层都不得存储含敏感信息(身份证、支付)
no-cache可存,但每次使用前必须回源校验"既要缓存加速又要绝对新鲜"(配合 ETag)
must-revalidate过期后不得"过期服务",必须回源金融一致性要求
stale-while-revalidate=30过期后 30s 内可先返回旧值同时后台刷新容忍短暂陈旧的列表/详情(CDN 真香参数)
immutable声明该 URI 的表示永不变化带内容哈希的静态资源 /v2/app.9f8e.js

请求侧指令:no-cache(无视本地新鲜度直接校验)、max-stale=60(可接受 60 秒内陈旧值)。

API 基线策略(写进团队规范):

公开只读集合    Cache-Control: public, max-age=30, s-maxage=300, stale-while-revalidate=60
私有只读资源    Cache-Control: private, max-age=60          ← 或 no-store(敏感)
写响应/错误     Cache-Control: no-store
未知默认        no-store                                     ← 安全兜底

3. 协商缓存:验证器(Validators)

3.1 ETag —— 强/弱两档

http
GET /articles/42
HTTP/1.1 200 OK
ETag: "1a2b3c"            ← 强验证器:逐字节等价才匹配
http
GET /articles/42
If-None-Match: "1a2b3c"   ← 客户端带已知标签询问
HTTP/1.1 304 Not Modified  ← 表示未变:无 body,省全部下行
  • 强 ETag:表示字节级一致(规范化后的内容哈希);用于 If-Match/If-None-Match 正确性场景。
  • 弱 ETagW/"xxx"):语义等价即可(如忽略格式/时间戳);只能用于 If-None-Match 缓存校验,禁止用于 If-Match 乐观锁。
  • 生成建议:hash(canonical(resource-representation))(JCS/RFC 8785 式规范化),不要用 updatedAt 时间戳凑数——精度、时钟回拨、多实例都会坑你。
  • 变化敏感性:写操作必须让 ETag 改变;同一内容的重复 PUT 可以不改变(RFC 9110 建议不变,利于去重)。

3.2 Last-Modified / If-Modified-Since

秒级精度 + 时钟依赖,ETag 的从属方案;二者可同发(AND 语义:都要过)。

3.3 乐观并发家族(写侧条件头)

语义失败码
If-Match: "etag"版本匹配才执行写412
If-Unmodified-Since指定时间后没被人改过412 / 423
If-None-Match: *仅当资源不存在才创建(防并发重复建)412
428 Precondition Required服务器要求必须带条件头428

实战组合:创建防重复 If-None-Match: *;更新防覆盖 If-Match——一套 ETag 打通读写。

4. 失效:让缓存"知道"资源变了

4.1 方法不匹配问题(RFC 9110 §4.4)

RFC 定义:安全且幂等的方法(GET/HEAD)之外的成功响应会使同 URI 的缓存失效;GET 不失效其它 URI。 推论:

  • PUT /articles/42 成功后,本 URI 的客户端缓存自动作废——但 /articles(集合)与 /authors/7/articles 不会!集合缓存只能靠短 TTL / stale-while-revalidate / 主动 purge。
  • 这是 REST 缓存的固有局限:写只精确失效单资源,集合失效靠策略

4.2 主动失效通道

  • CDN:PURGE/Refresh API(Fastly/Varnish BAN,云厂商各有产品化接口)——在写路径后异步触发;
  • Cache-Tag/Surrogate-Key(Fastly/Cloudflare 事实标准,IETF draft-http-cache-tag-purge):响应打 Cache-Tag: article-42,变更时按 tag 批量 purge——集合缓存精准失效的正解

4.3 Age 与 110 stale

Age: 37 告诉客户端这响应已缓存多久;Warning/stale 语义用于诊断。监控应采集 x-cache: HIT/MISS(虽非标准,事实通用)。

5. 缓存键与隐私安全(事故高发)

缓存投毒/串数据三板斧防护:

  1. 私有数据永不 publicGET /users/me 若被 CDN 缓存,用户 B 会拿到用户 A 的资料——私有接口显式 private/no-store,网关层做默认兜底;
  2. 认证响应必须 Vary: Authorization?——错误!缓存按 Authorization 头分键等于给每个用户存一份(且可能泄露)。正解:私有数据 private, no-store 或干脆让 CDN 不缓存(Cache-Control: no-store 覆盖边缘);
  3. 可变头部参与缓存键必须进 Vary(如 Vary: X-Organization)并配 tag-purge 方案;
  4. 会话 Cookie 认证 + CDN:仅静态公开路径允许边缘缓存,或按 Cookie 存在与否 bypass。
http
HTTP/1.1 200 OK
Link: </authors/7>; rel="preload"; as="json"     ← 提示客户端下一步会用到
Link: </articles/42/comments>; rel="preload"; as="json"
Link: </articles?page=2>; rel="next"              ← RFC 8288 分页链接

配合 103 Early Hints 可在主响应前让浏览器发起预取——API 也可借鉴做"响应内提示 + 客户端预拉取",显著降首屏串行 RTT。

7. 本章小结

  • 强缓存解决"不打到服务器"(max-age/s-maxage/stale-while-revalidate),协商缓存解决"打到了很便宜"(ETag→304)。
  • ETag 双重身份:读缓存校验 + 写乐观锁(If-Match/If-None-Match: *)——API 应全资源启用强 ETag。
  • 缓存正确性三防线:私有数据 private/no-storeVary 完整声明、写路径联动 purge(Cache-Tag)。

8. 下一步

读性能有了缓存护体,写与身份呢? → 认证与授权