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 强缓存与协商缓存协作流程

下图展示了客户端从发起请求到命中缓存的完整决策链路:

HTTP 缓存决策流程 客户端发起请求 强缓存是否有效? (检查 max-age / Expires) 是 使用本地缓存 否 发起条件请求 携带 ETag / Last-Modified 服务器验证资源 对比 ETag / Last-Modified 未变更 返回 304 Not Modified 使用本地缓存 已变更 返回 200 + 新资源 更新本地缓存 渲染 / 返回响应

3.4 四种缓存头的优先级与协作关系

当多个缓存头同时存在时,遵循以下优先级规则:

  1. Cache-Control 优先级最高,覆盖 Expires。
  2. ETag 优先级高于 Last-Modified,服务器优先使用 ETag 做条件验证。
  3. 若同时存在 If-None-Match 和 If-Modified-Since,服务器必须同时验证两者,只有都通过才返回 304。
  4. 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 生成器自动处理缓存逻辑。