第 8 章:HTTP 缓存
第 8 章:HTTP 缓存
缓存是提升 Web 性能最重要的手段之一。理解 HTTP 缓存机制,能显著减少网络延迟、降低服务器负载、提升用户体验。
8.1 缓存概述
什么是 HTTP 缓存
HTTP 缓存是指浏览器或中间代理(CDN、反向代理)存储服务器响应的副本,后续相同请求可直接使用缓存,无需访问源服务器。
缓存位置
请求 → 浏览器缓存 → 代理缓存(CDN) → 源服务器
↓ 未命中 ↓ 未命中 ↓ 生成响应
使用缓存 使用缓存 返回响应
| 缓存层级 | 位置 | 特点 |
|---|---|---|
| 浏览器缓存 | 用户设备 | 仅该用户可用 |
| 代理缓存 | CDN / 反向代理 | 多用户共享 |
| 网关缓存 | 应用层缓存 | Redis、Memcached |
缓存的好处
| 好处 | 说明 |
|---|---|
| 减少延迟 | 缓存读取比网络请求快 100-1000 倍 |
| 减少带宽 | 减少数据传输量 |
| 降低服务器负载 | 减少服务器处理压力 |
| 提高可用性 | 服务器故障时缓存仍可用 |
8.2 强缓存(Strong Caching)
强缓存命中时,浏览器直接使用缓存,不发送请求到服务器。
Cache-Control: max-age
# 响应头:缓存 1 小时
Cache-Control: max-age=3600
from http.server import HTTPServer, BaseHTTPRequestHandler
import time
class CacheHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/api/data':
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Cache-Control', 'max-age=60') # 缓存 60 秒
self.end_headers()
self.wfile.write(f'{{"time": "{time.time()}"}}'.encode())
Expires(旧方案)
# 过期时间点(HTTP/1.0 遗留)
Expires: Wed, 10 May 2026 11:00:00 GMT
max-age vs Expires
| 特性 | max-age | Expires |
|---|---|---|
| 格式 | 相对秒数 | 绝对时间 |
| 优先级 | 高 | 低 |
| 时区问题 | 无 | 有 |
| 推荐 | ✓ | 仅兼容旧浏览器 |
强缓存流程
第一次请求:
浏览器 ──GET /style.css──→ 服务器
浏览器 ←── 200 + Cache-Control: max-age=3600 ── 服务器
浏览器存储响应到缓存
3600 秒内再次请求:
浏览器 ──GET /style.css──→ (直接从缓存读取,不发送请求)
显示缓存内容
状态码显示 200 (from disk cache)
8.3 协商缓存(Conditional Caching)
协商缓存时,浏览器发送请求到服务器验证缓存是否有效。
ETag / If-None-Match
# 首次请求
GET /api/users HTTP/1.1
# 响应
HTTP/1.1 200 OK
ETag: "v1-abc123"
Cache-Control: no-cache
Content-Length: 256
[响应体]
# 后续请求(带 ETag)
GET /api/users HTTP/1.1
If-None-Match: "v1-abc123"
# 如果未修改
HTTP/1.1 304 Not Modified
ETag: "v1-abc123"
# 如果已修改
HTTP/1.1 200 OK
ETag: "v2-def456"
[新的响应体]
Last-Modified / If-Modified-Since
# 首次请求
GET /image.jpg HTTP/1.1
# 响应
HTTP/1.1 200 OK
Last-Modified: Wed, 10 May 2026 10:00:00 GMT
Content-Length: 102400
# 后续请求
GET /image.jpg HTTP/1.1
If-Modified-Since: Wed, 10 May 2026 10:00:00 GMT
# 如果未修改
HTTP/1.1 304 Not Modified
ETag vs Last-Modified
| 特性 | ETag | Last-Modified |
|---|---|---|
| 精度 | 精确(内容 hash) | 秒级 |
| 优先级 | 高 | 低 |
| 开销 | 计算 hash | 文件系统 mtime |
| 适用场景 | 频繁修改 | 静态文件 |
| 多服务器 | 一致 | 可能不一致 |
8.4 缓存策略设计决策树
资源是否经常变化?
├── 静态资源(JS/CSS/图片)
│ ├── 使用 hash 文件名? → Cache-Control: public, max-age=31536000, immutable
│ └── 不使用 hash? → Cache-Control: public, max-age=86400 + ETag
│
├── API 响应
│ ├── 实时数据? → Cache-Control: no-store
│ ├── 短期缓存? → Cache-Control: max-age=60 + ETag
│ └── 用户相关? → Cache-Control: private, max-age=300
│
├── HTML 页面
│ ├── SPA? → Cache-Control: no-cache + ETag
│ └── 静态? → Cache-Control: public, max-age=3600
│
└── 敏感数据
└── Cache-Control: no-store
常见资源的缓存策略
| 资源类型 | Cache-Control | 理由 |
|---|---|---|
| 带 hash 的 JS/CSS | public, max-age=31536000, immutable | 文件名变化 = 新资源 |
| 不带 hash 的 JS/CSS | public, max-age=86400 | 1 天后重新验证 |
| HTML 页面 | no-cache | 每次验证最新版本 |
| API 数据 | no-store 或 max-age=60 | 根据实时性要求 |
| 用户信息 | private, max-age=300 | 仅浏览器缓存 |
| 图片(静态) | public, max-age=2592000 | 30 天缓存 |
| 字体 | public, max-age=31536000 | 长期缓存 |
8.5 ETag 生成策略
常见 ETag 算法
import hashlib
import json
# 1. 内容 hash(最精确)
def generate_etag_by_content(content):
hash_val = hashlib.md5(content.encode()).hexdigest()
return f'"{hash_val}"'
# 2. 版本号(适合 API)
def generate_etag_by_version(version):
return f'"v{version}"'
# 3. 修改时间 + 大小(适合静态文件)
def generate_etag_by_mtime(mtime, size):
hash_val = hashlib.md5(f"{mtime}-{size}".encode()).hexdigest()
return f'"{hash_val}"'
# Express.js ETag
const express = require('express');
const app = express();
// 使用自定义 ETag
app.set('etag', 'strong'); // 默认
// 或自定义 ETag 生成
app.use((req, res, next) => {
const originalJson = res.json;
res.json = function(data) {
const etag = generate_etag_by_content(JSON.stringify(data));
res.set('ETag', etag);
return originalJson.call(this, data);
};
next();
});
强 ETag vs 弱 ETag
# 强 ETag — 字节级精确匹配
ETag: "abc123"
# 弱 ETag — 语义等价即可
ETag: W/"abc123"
8.6 实战:完整缓存策略
Nginx 缓存配置
server {
listen 443 ssl;
server_name example.com;
# 静态资源 — 长期缓存
location ~* \.(css|js|woff2|png|jpg|gif|ico)$ {
# 文件名带 hash 时
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary "Accept-Encoding";
# gzip
gzip on;
gzip_types text/css application/javascript;
}
# HTML — 协商缓存
location / {
add_header Cache-Control "no-cache";
add_header Vary "Accept-Encoding";
try_files $uri $uri/ /index.html;
}
# API — 不缓存或短时间缓存
location /api/ {
add_header Cache-Control "no-store";
proxy_pass http://backend;
}
# 图片 — 中期缓存
location /images/ {
add_header Cache-Control "public, max-age=2592000";
}
}
Node.js 服务端缓存实现
const express = require('express');
const crypto = require('crypto');
const app = express();
// ETag 中间件
function conditionalCache(req, res, next) {
const originalJson = res.json.bind(res);
res.json = function(data) {
const body = JSON.stringify(data);
const etag = `"${crypto.createHash('md5').update(body).digest('hex')}"`;
res.set('ETag', etag);
res.set('Last-Modified', new Date().toUTCString());
// 检查 If-None-Match
const clientEtag = req.headers['if-none-match'];
if (clientEtag === etag) {
return res.status(304).end();
}
return originalJson(data);
};
next();
}
app.use(conditionalCache);
// API 路由 — 短期缓存
app.get('/api/products', (req, res) => {
res.set('Cache-Control', 'public, max-age=60');
res.json({ products: [...] });
});
// 用户数据 — 私有缓存
app.get('/api/profile', (req, res) => {
res.set('Cache-Control', 'private, max-age=300');
res.json({ user: {...} });
});
// 实时数据 — 不缓存
app.get('/api/notifications', (req, res) => {
res.set('Cache-Control', 'no-store');
res.json({ notifications: [...] });
});
8.7 Vary 头部
Vary 指定哪些请求头影响缓存响应。
Vary: Accept, Accept-Encoding, Accept-Language
# Nginx Vary 配置
gzip_vary on; # 自动添加 Vary: Accept-Encoding
# 根据 Accept 缓存不同格式
location /api/data {
if ($http_accept ~* "application/json") {
add_header Vary "Accept";
}
}
8.8 缓存失效与刷新
用户操作对缓存的影响
| 操作 | 地址栏 | F5 刷新 | Ctrl+F5 强刷 |
|---|---|---|---|
| 强缓存 | 使用缓存 | 跳过强缓存 | 跳过所有缓存 |
| 协商缓存 | 使用缓存 | 验证 | 跳过验证 |
| 请求头 | 正常 | 带 Cache-Control: max-age=0 | 带 Cache-Control: no-cache |
缓存清除策略
// 版本号方式 — 清除旧版本缓存
const ASSET_VERSION = '20260510';
app.get('/', (req, res) => {
res.send(`
<html>
<head>
<link rel="stylesheet" href="/style.css?v=${ASSET_VERSION}">
<script src="/app.js?v=${ASSET_VERSION}"></script>
</head>
</html>
`);
});
// Content Hash 方式 — 自动缓存失效
// webpack 输出: app.a1b2c3d4.js
// 内容变化 → hash 变化 → 新文件名 → 缓存自动失效
8.9 业务场景:电商网站缓存策略
┌─────────────────────────────────────────────────┐
│ CDN 边缘节点 │
│ 静态资源: max-age=31536000, immutable │
│ 图片: max-age=2592000 │
└─────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────┐
│ 应用服务器 │
│ HTML: no-cache + ETag │
│ API (商品列表): max-age=60 │
│ API (用户数据): private, max-age=300 │
│ API (购物车): no-store │
└─────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────┐
│ 浏览器缓存 │
│ 静态资源 → disk cache │
│ API 响应 → memory cache │
│ 敏感数据 → 不缓存 │
└─────────────────────────────────────────────────┘
⚠️ 注意事项
- 不要缓存敏感数据:使用
Cache-Control: no-store - ETag 计算开销:大文件使用
Last-Modified而非内容 hash - Vary: *:会导致缓存几乎失效,谨慎使用
- CDN 缓存清除:部署时需要清除 CDN 缓存或使用版本号
- 测试缓存:使用 DevTools 的 Network 面板检查缓存状态
- 304 响应无 body:确保客户端处理 304 时不读取响应体
🔗 扩展阅读
- RFC 9111 — HTTP Caching
- MDN — HTTP Caching
- web.dev — HTTP Caching
- Jake Archibald — Caching Best Practices
下一章:第 9 章:认证与授权 — Basic/Bearer/OAuth2/JWT/API Key 方案对比与实现