条件请求与 ETag

用 ETag/Last-Modified 与 If-None-Match/If-Match 实现缓存校验与并发控制:读取省流量,写入用 If-Match 防丢更新(乐观锁)。

条件请求与 ETag 是什么

条件请求让客户端把已知的版本信息带在请求里,服务端据此决定是返回完整内容还是只回一个状态码。读取场景用 If-None-Match 携带 ETag,内容未变时返回 304,省下带宽;写入场景用 If-Match 携带 ETag 做乐观锁,版本不匹配就返回 412,从而避免两个编辑者互相覆盖——这种丢失更新在协同编辑与配置管理里非常常见。ETag 通常取内容哈希或版本号,多节点部署时要保证同一版本在各节点生成结果一致。

基本信息

读取缓存If-None-Match + ETag → 未变化返回 304
写入并发控制If-Match + ETag → 版本不符返回 412
时间戳方式Last-Modified + If-Modified-Since,精度到秒,适合静态资源
弱校验ETag 加 W/ 前缀表示语义等价即可,不适合做并发控制
多节点要求同一资源版本的 ETag 生成规则在各节点一致

典型使用场景

  • 列表与详情缓存:内容未变时返回 304,降低带宽与服务端渲染开销
  • 协同编辑:两个用户同时编辑同一份配置,后提交者得到 412 而不是静默覆盖
  • 配置管理:变更前确认版本,防止覆盖他人刚提交的修改
  • 对象存储上传:校验对象未被他人替换过再写入
  • CDN 与网关优化:静态资源用版本标识控制缓存有效期
  • 幂等重放保护:重复提交同一版本请求时可被识别

交互流程

交互流程
GET /docs/1
→ 200 OK, ETag: "v7"            # 服务端返回当前版本标识

GET /docs/1  If-None-Match: "v7"
→ 304 Not Modified               # 内容未变,客户端直接用本地副本

PUT /docs/1  If-Match: "v7"      # 写入前声明自己基于的版本
→ 200 OK                          # 版本仍为 v7,写入成功
→ 412 Precondition Failed         # 已被他人改为 v8,提示重新拉取合并

# 注意:412 不是错误而是正常并发路径,客户端必须有合并或重试逻辑

关键点与常见误用

要点正确做法常见误用
ETag 粒度按资源整体版本生成按字段生成,导致缓存频繁失效
一致性多节点使用同一内容哈希或版本号各节点用本地时间戳,缓存不断失效
写入校验使用强校验并真实比较忽略 If-Match 或使用弱校验,丢更新照旧发生
状态码版本不符返回 412 或 409统一返回 400,客户端无法区分冲突与参数错误
性能内容大时用版本号代替哈希计算每次请求都全量计算哈希,成为瓶颈
ETag 的生成规则要写进接口规范:多节点用内容哈希或版本号时,必须保证同一版本在不同节点得到相同结果,否则缓存命中率会异常偏低。
412 属于正常并发路径,不是系统故障:客户端应当有拉取最新版本、重新合并与提示用户的流程,而不是把报错直接抛给使用者。

易错点

  • 忽略或未真实比较 If-Match:丢失更新仍然发生,只是看起来规范
  • 使用弱校验做并发控制:语义等价但内容不同时校验通过,覆盖他人改动
  • 多节点 ETag 生成规则不一致:缓存频繁失效,带宽与延迟不降反升
  • 冲突返回 400 或 500:客户端无法区分版本冲突与真实错误
  • 内容过大仍全量计算哈希:每次请求都做重活,成为新的性能瓶颈

数据来源与参考

  • RFC 9110 HTTP Semantics(第 13 章条件请求)
  • RFC 7232:Conditional Requests
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌