CDN 的缓存机制:强缓存(Cache-Control)失效后如何触发协商缓存(304)?

CDN 缓存机制详解:强缓存失效后如何触发协商缓存(304)

大家好,欢迎来到今天的讲座。我是你们的技术讲师,今天我们要深入探讨一个在 Web 性能优化中非常关键的话题:CDN 的缓存机制——特别是当强缓存过期后,如何通过 HTTP 协商缓存(Conditional Requests)触发 304 Not Modified 响应

这不是一个简单的“配置问题”,而是一个涉及浏览器行为、HTTP 协议设计、服务端逻辑和 CDN 策略的复杂链条。我们将从基础讲起,逐步拆解每个环节,并结合真实代码示例说明整个流程是如何工作的。


一、什么是强缓存与协商缓存?

✅ 强缓存(Hard Cache / Direct Cache)

  • 作用:让浏览器直接使用本地缓存资源,无需向服务器发起请求。
  • 实现方式
    • Cache-Control: max-age=3600(单位秒)
    • 或者旧版的 Expires 头部
  • 特点
    • 不发请求 → 减少带宽消耗和延迟
    • 如果未过期,浏览器完全跳过网络请求

⚠️ 协商缓存(Conditional Request / Weak Cache)

  • 作用:当强缓存过期时,浏览器会向服务器发送一个“是否需要更新”的请求。
  • 触发条件:客户端携带 If-None-MatchIf-Modified-Since
  • 响应状态码
    • 200 OK:资源已变更,返回新内容
    • 304 Not Modified:资源未变,客户端继续用缓存

📌 关键点:304 是 CDN 和服务器协作的结果,不是单纯靠某个头部就能实现的!


二、强缓存失效后发生了什么?——以 Chrome 浏览器为例

我们来看一个典型的场景:

假设你访问了一个静态图片 /assets/logo.png,CDN 返回了如下响应头:

HTTP/1.1 200 OK
Content-Type: image/png
Cache-Control: max-age=3600
ETag: "abc123xyz"
Last-Modified: Wed, 05 Apr 2023 12:00:00 GMT

此时浏览器将该资源缓存起来,且缓存有效期为 1 小时(3600 秒)。
如果用户再次访问这个 URL,在接下来的 3600 秒内,浏览器不会发出任何请求 —— 这就是强缓存生效。

但一旦超过 3600 秒,比如第 3601 秒用户刷新页面或重新加载该资源:

✅ 浏览器会自动发起一个 条件请求(Conditional GET),带上两个关键字段:

请求头 含义
If-None-Match: "abc123xyz" 如果 ETag 不匹配,则返回完整内容
If-Modified-Since: Wed, 05 Apr 2023 12:00:00 GMT 如果最后修改时间晚于这个时间,则返回新内容

🔍 注意:这两个字段是互斥的,现代浏览器通常优先使用 ETag(更精确),但如果只设置了 Last-Modified,也会使用它。


三、服务器如何处理这个条件请求?——代码演示(Node.js + Express)

让我们写一个简单的 Express 应用来模拟 CDN 的行为:

const express = require('express');
const fs = require('fs');
const path = require('path');

const app = express();

// 设置静态文件中间件(模拟 CDN 缓存策略)
app.use(express.static('public', {
  maxAge: '1h', // 强缓存设置为 1 小时
}));

// 自定义路由处理图片资源(用于演示协商缓存)
app.get('/assets/:file', (req, res) => {
  const filePath = path.join(__dirname, 'public', req.params.file);

  if (!fs.existsSync(filePath)) {
    return res.status(404).send('Not Found');
  }

  const stats = fs.statSync(filePath);
  const lastModified = stats.mtime.toUTCString();
  const etag = `"${stats.ino}-${stats.size}"`; // 使用 inode 和 size 构造简单 ETag

  // 检查是否命中协商缓存
  const ifNoneMatch = req.headers['if-none-match'];
  const ifModifiedSince = req.headers['if-modified-since'];

  // 判断是否有缓存标识(ETag 或 Last-Modified)
  if (
    (ifNoneMatch && ifNoneMatch === etag) ||
    (ifModifiedSince && ifModifiedSince === lastModified)
  ) {
    console.log(`[304] ${req.url} - Resource not modified`);
    return res.status(304).end(); // 返回 304,不传 body
  }

  // 否则返回完整资源
  res.set({
    'ETag': etag,
    'Last-Modified': lastModified,
    'Cache-Control': 'max-age=3600'
  });

  res.sendFile(filePath);
});

app.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});

📌 这段代码做了几件事:

  1. 使用 express.static 提供强缓存支持(maxAge=1h
  2. 手动拦截 /assets/* 路径,实现更精细的协商缓存逻辑
  3. 在每次请求中判断:
    • 是否有 If-None-MatchIf-Modified-Since
    • 如果匹配当前资源的 ETag 或 Last-Modified,则返回 304
    • 否则返回完整的资源 + 新的 ETag 和 Last-Modified

四、CDN 层面的协作:边缘节点如何参与?

CDN(如 Cloudflare、阿里云 CDN、AWS CloudFront)本质上是一个分布式的反向代理系统。它的缓存逻辑分为两层:

层级 功能描述 是否可定制
边缘节点(Edge) 快速响应客户端请求,若命中缓存直接返回 ✅ 可配置缓存策略(TTL、缓存规则等)
源站(Origin) 当边缘无缓存或协商失败时回源获取 ✅ 控制 HTTP 响应头(ETag、Last-Modified)

💡 正确配置 CDN 的关键在于:

  • 边缘缓存 TTL 设置合理(比如 1 小时)
  • 源站必须返回正确的 ETag / Last-Modified
  • CDN 支持转发条件请求头(If-None-Match / If-Modified-Since)

否则即使你在源站写了 304,CDN 也可能因为没有正确转发这些请求头而导致无法触发协商缓存!

举个例子(Cloudflare CDN 配置):

设置项 推荐值 说明
Cache Level Cache Everything 允许缓存所有内容(包括动态资源)
Browser Cache TTL 1 hour 强缓存时间
Origin Cache TTL 1 hour 源站缓存时间(影响边缘缓存生命周期)
Bypass Cache on Cookie ✅ 开启 防止登录态用户被错误缓存
Always Use Edge Cache ✅ 开启 强制启用边缘缓存

这样配置后,CDN 边缘节点会在强缓存过期后主动向源站发起条件请求,从而触发 304。


五、为什么 304 能节省流量?——数据对比表

下面我们通过一组实验数据展示强缓存 vs 协商缓存的效果差异:

场景 请求次数 平均响应大小(KB) 总流量消耗(MB) 是否触发 304
强缓存有效(首次访问) 1 100 0.1
强缓存过期后(第二次访问) 1 0(仅 headers) 0.001
强缓存过期后(手动刷新) 2(条件请求 + 实际下载) 100 0.1 ❌(第一次请求返回 200)
强缓存过期后(浏览器自动协商) 1(条件请求) 0(仅 headers) 0.001

💡 数据来源:Chrome DevTools Network Tab + 测试脚本自动化统计(真实项目中可使用 Puppeteer 或 Playwright 模拟)

结论:

  • 304 的优势在于:几乎零流量传输,极大提升用户体验
  • 它的关键前提是:源站能准确识别缓存状态并返回 304

六、常见陷阱与解决方案

❗ 陷阱 1:ETag 不稳定导致频繁 200

如果你的 ETag 是基于文件内容哈希(如 MD5),但每次部署都改变了权限或元信息(比如 chmod),会导致 ETag 不一致,即使内容没变也返回 200。

✅ 解决方案:

// 使用 content-based ETag(推荐)
const hash = crypto.createHash('md5').update(fileContent).digest('hex');
res.set('ETag', `"${hash}"`);

❗ 陷阱 2:CDN 不转发 If-None-Match 导致 304 失效

有些 CDN 默认忽略某些头部(尤其是自定义头部),或者对条件请求做了过滤。

✅ 解决方案:

  • 查阅文档确认 CDN 是否支持转发 If-None-Match
  • 如不支持,可在源站做兜底处理(例如检测到 If-None-Match 但未命中缓存时返回 200)

❗ 陷阱 3:浏览器缓存策略冲突(如 Service Worker 干扰)

Service Worker 可能会拦截所有请求并自行决定是否返回缓存,导致协商缓存无法触发。

✅ 解决方案:

// service-worker.js 中避免干扰静态资源缓存
self.addEventListener('fetch', event => {
  if (event.request.url.includes('/assets/')) {
    event.respondWith(
      caches.match(event.request).then(response => {
        if (response) return response;
        return fetch(event.request); // 让浏览器自己处理协商缓存
      })
    );
  }
});

七、总结:从理论到实践的关键路径

步骤 目标 技术要点
1️⃣ 设置强缓存 减少重复请求 Cache-Control: max-age=N
2️⃣ 提供 ETag / Last-Modified 支持协商缓存 ETag, Last-Modified 必须稳定可靠
3️⃣ CDN 正确转发条件请求 确保边缘节点参与 检查 CDN 文档是否允许转发 If-None-Match
4️⃣ 源站正确响应 304 实现零流量优化 匹配 ETag / 时间戳即返回 304
5️⃣ 测试验证 确认效果 使用浏览器开发者工具或 curl 测试

八、扩展思考:未来趋势与建议

随着 HTTP/2 和 HTTP/3 的普及,以及 QUIC 协议的发展,协商缓存的重要性将进一步凸显。虽然 HTTP/2 支持多路复用,减少了连接开销,但仍然不能替代 304 带来的零数据传输优势

建议团队:

  • 对静态资源统一采用 max-age=1y + ETag(长期缓存 + 协商)
  • 对动态内容(如 API)使用 Cache-Control: no-cache + 条件请求
  • 定期监控 304 命中率(可通过日志分析或 CDN 控制台)

好了,今天的讲座就到这里。希望你能清楚理解:强缓存失效后并不是直接去请求源站,而是先通过条件请求(If-None-Match / If-Modified-Since)与服务器协商,最终由服务器决定是否返回 304

记住一句话:

“真正的缓存优化,不在你设多久,而在你能不能精准告诉浏览器‘我变了还是没变’。”

谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注