协商缓存是什么
强缓存过期后,客户端不立刻重新下载整个资源,而是带着校验值回源问一句「我这份还能不能用」:资源没变,服务器回 304 Not Modified(没有响应体);变了则回 200 并带新内容。校验值有两个来源:ETag 是内容指纹(强校验),Last-Modified 是秒级修改时间(弱校验)。它解决的是「既想复用本地副本、又怕拿到旧数据」的矛盾。
基本信息
| 适用平台 | 浏览器、Nginx/Apache、CDN 边缘节点等所有 HTTP 缓存 |
|---|---|
| 请求头 | If-None-Match(配 ETag)、If-Modified-Since(配 Last-Modified) |
| 响应 | 304 Not Modified 或 200 OK |
| 前提 | 缓存条目已过期(stale)但允许再验证 |
| 规范 | RFC 9111 §4.3、RFC 9110 §13 |
典型使用场景
- 静态资源加长 max-age 后需要兜底校验,避免内容更新被缓存挡住
- 体积大但更新频率低的 HTML 页与 JSON 接口,省掉重复传输的流量
- CDN 回源:边缘节点对源站做再验证,降低回源带宽
- 移动端弱网环境:304 只花一个来回,比重新下载整个响应体便宜得多
- 作为强缓存的降级策略,在无法预知内容是否变更时使用
观察协商过程
观察协商过程
curl -sI https://www.example.com/ | grep -i etag # 第一次:记下 ETag
curl -sI -H 'If-None-Match: "abc123"' https://www.example.com/ | head -1 # 期望 304
curl -sI -H 'If-Modified-Since: Sat, 11 Oct 2026 12:00:00 GMT' https://www.example.com/ | head -1 # 时间校验
curl -sv https://www.example.com/ -o /dev/null 2>&1 | grep -E '^(<|>) (If-|ETag|Last-Modified)' # 看完整往返三种返回对照
| 返回 | 含义 | 客户端动作 |
|---|---|---|
| 304 Not Modified | 校验值命中,本地副本仍有效 | 复用本地副本,并按新响应头更新新鲜期 |
| 200 OK | 资源已变更或服务端不做条件请求 | 用响应体替换本地副本 |
| 412 Precondition Failed | If-Match / If-Unmodified-Since 不满足 | 并发写被拒,应先取最新版本再提交 |
304 可以携带新的 Cache-Control、ETag 等头,客户端不能因为它没有 body 就整条忽略——这些头才是刷新新鲜期的依据。
协商缓存每次仍至少多一个 RTT。高频小接口更适合「短 max-age 强缓存 + stale-while-revalidate 后台刷新」,而不是每次都再验证。
常见误用对照
- 把 304 当成「缓存已过期」:它恰恰说明本地副本仍然可用,只是顺便刷新了新鲜期
- 在 ETag 里混入用户 ID、时间戳等个性化信息,导致不同用户之间完全无法互相命中,缓存形同虚设
- 同时把强缓存与协商缓存都配成 no-store,等于主动放弃全部缓存能力
- 只用 If-Modified-Since 做写操作的并发控制,秒级精度会漏掉同一秒内的连写冲突