3. HTTP 缓存机制:强缓存与协商缓存
HTTP 缓存是提升 API 响应速度、降低服务器负载的核心手段。在 OpenAPI 契约中,正确声明缓存策略能让客户端和网关自动复用已缓存资源,避免重复请求。本章深入剖析强缓存与协商缓存的完整链路,以及 Cache-Control、Expires、ETag、Last-Modified 四个关键头的语义与协作方式。
3.1 强缓存(本地缓存)
强缓存是指客户端在缓存有效期内直接使用本地副本,不发送任何请求到服务器。其核心判断依据是缓存是否“新鲜”。
关键特征: 状态码 200(from disk cache / from memory cache),无网络往返,延迟接近 0ms。
3.1.1 Expires 头(HTTP/1.0 方案)
Expires 是一个绝对时间戳,格式为 GMT。客户端将当前时间与 Expires 对比,若未过期则直接使用缓存。
Expires: Wed, 21 Oct 2025 07:28:00 GMT
局限: 依赖客户端本地时间,若用户修改系统时间,缓存可能失效或异常延长。HTTP/1.1 已不推荐单独使用。
3.1.2 Cache-Control 头(HTTP/1.1 标准方案)
Cache-Control 使用指令(directive)组合,优先级高于 Expires。常用指令如下:
| 指令 | 值示例 | 含义 |
|---|---|---|
max-age |
3600 |
缓存有效期为 3600 秒(从响应生成时刻起算) |
s-maxage |
3600 |
仅对共享缓存(如 CDN)生效,覆盖 max-age |
public |
— | 允许任何中间缓存(代理、CDN)缓存该响应 |
private |
— | 仅允许客户端(浏览器)缓存,禁止中间缓存 |
no-cache |
— | 强制每次请求都向服务器验证(但允许缓存副本) |
no-store |
— | 禁止任何缓存,完全不留副本 |
must-revalidate |
— | 过期后必须向源服务器验证,不可使用过期缓存 |
# 典型强缓存配置:允许客户端和 CDN 缓存 1 小时
Cache-Control: public, max-age=3600, must-revalidate
3.2 协商缓存(条件请求)
当强缓存过期或 no-cache 指令生效时,客户端发起条件请求,携带缓存标识符,由服务器判断资源是否变更。若未变更,返回 304 Not Modified(无 body),客户端继续使用本地缓存;若已变更,返回 200 OK 及最新资源。
性能收益: 304 响应仅包含头信息,无 body 传输,带宽消耗极低,延迟远低于完整 200 响应。
3.2.1 Last-Modified / If-Modified-Since
服务器在响应中返回 Last-Modified(资源最后修改时间)。客户端后续请求携带 If-Modified-Since(值等于上次收到的 Last-Modified),服务器对比时间戳决定返回 304 或 200。
# 服务器响应
Last-Modified: Tue, 15 Oct 2025 12:00:00 GMT
# 客户端条件请求
If-Modified-Since: Tue, 15 Oct 2025 12:00:00 GMT
精度缺陷: 时间戳精度为秒,无法感知毫秒级变更;若资源周期性重写但内容未变,仍会触发重新下载。
3.2.2 ETag / If-None-Match
ETag 是资源内容的唯一标识符(通常为哈希值或版本号)。客户端携带 If-None-Match(值等于上次收到的 ETag),服务器对比标识符决定缓存状态。ETag 分为强验证和弱验证两种:
| 类型 | 格式 | 说明 |
|---|---|---|
| 强 ETag | "abc123" |
资源每个字节都相同才视为匹配,适用于精确校验 |
| 弱 ETag | W/"abc123" |
语义等价即可匹配(如 HTML 换行符差异可忽略),适用于动态页面 |
# 服务器响应
ETag: "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
# 客户端条件请求
If-None-Match: "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
3.3 强缓存与协商缓存协作流程
下图展示了客户端从发起请求到命中缓存的完整决策链路:
3.4 四种缓存头的优先级与协作关系
当多个缓存头同时存在时,遵循以下优先级规则:
- Cache-Control 优先级最高,覆盖
Expires。 - ETag 优先级高于
Last-Modified,服务器优先使用 ETag 做条件验证。 - 若同时存在
If-None-Match和If-Modified-Since,服务器必须同时验证两者,只有都通过才返回 304。 no-cache指令强制走协商缓存,即使max-age未过期。
最佳实践: 在 OpenAPI 契约中,建议始终同时声明
Cache-Control: public, max-age=3600 和 ETag,前者控制强缓存时长,后者提供精确的协商验证能力。
3.5 OpenAPI 契约中的缓存声明示例
以下是一个符合 OpenAPI 3.0 规范的缓存响应定义:
paths:
/api/users/{id}:
get:
summary: 获取用户信息
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: 成功返回用户信息
headers:
Cache-Control:
schema:
type: string
example: public, max-age=3600, must-revalidate
ETag:
schema:
type: string
example: "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
Last-Modified:
schema:
type: string
format: date-time
example: Tue, 15 Oct 2025 12:00:00 GMT
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'304':
description: 资源未修改,使用缓存
headers:
ETag:
schema:
type: string
Last-Modified:
schema:
type: string
format: date-time
3.6 缓存策略选择矩阵
| 场景 | 推荐 Cache-Control | 协商头 | 说明 |
|---|---|---|---|
| 静态资源(图片、CSS、JS) | public, max-age=31536000, immutable |
ETag(强) | 长期缓存,配合版本号或哈希文件名实现更新 |
| 用户信息 API | private, max-age=60 |
ETag + Last-Modified | 仅客户端缓存,短有效期,精确验证 |
| 实时数据(股票行情) | no-cache |
ETag(弱) | 每次请求都验证,但可利用 304 减少 body 传输 |
| 敏感数据(支付订单) | no-store |
无 | 完全禁止缓存,确保数据安全 |
3.7 常见避坑指南
- 不要混用
Expires和Cache-Control: max-age: 两者语义冲突时,Cache-Control优先,但混用容易造成维护混乱。 no-cache不等于no-store:no-cache允许缓存副本,但每次必须验证;no-store完全禁止存储。- ETag 生成要稳定: 避免使用
inode或mtime作为 ETag 来源,多节点部署时可能不一致。推荐使用内容哈希(如 MD5、SHA-1)。 - 代理缓存注意
private指令: 包含用户隐私的响应必须标记private,防止 CDN 或公共代理缓存泄露数据。 - 304 响应不能包含 body: 服务器返回 304 时必须省略响应体,否则客户端可能解析异常。
OpenAPI 契约建议: 在
components/headers 中定义可复用的缓存头组件,避免在每个路径中重复声明。同时使用 examples 字段提供典型值,方便客户端 SDK 生成器自动处理缓存逻辑。