Appearance
缓存与条件请求
本章导读:缓存是 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正确性场景。 - 弱 ETag(
W/"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/RefreshAPI(Fastly/VarnishBAN,云厂商各有产品化接口)——在写路径后异步触发; 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. 缓存键与隐私安全(事故高发)
缓存投毒/串数据三板斧防护:
- 私有数据永不
public:GET /users/me若被 CDN 缓存,用户 B 会拿到用户 A 的资料——私有接口显式private/no-store,网关层做默认兜底; - 认证响应必须
Vary: Authorization?——错误!缓存按 Authorization 头分键等于给每个用户存一份(且可能泄露)。正解:私有数据private, no-store或干脆让 CDN 不缓存(Cache-Control: no-store覆盖边缘); - 可变头部参与缓存键必须进
Vary(如Vary: X-Organization)并配 tag-purge 方案; - 会话 Cookie 认证 + CDN:仅静态公开路径允许边缘缓存,或按
Cookie存在与否 bypass。
6. 预取提示:Link: rel="preload"
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-store、Vary完整声明、写路径联动 purge(Cache-Tag)。
8. 下一步
读性能有了缓存护体,写与身份呢? → 认证与授权