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-Match或If-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');
});
📌 这段代码做了几件事:
- 使用
express.static提供强缓存支持(maxAge=1h) - 手动拦截
/assets/*路径,实现更精细的协商缓存逻辑 - 在每次请求中判断:
- 是否有
If-None-Match或If-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。
记住一句话:
“真正的缓存优化,不在你设多久,而在你能不能精准告诉浏览器‘我变了还是没变’。”
谢谢大家!