强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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-ageExpires
格式相对秒数绝对时间
优先级
时区问题
推荐仅兼容旧浏览器

强缓存流程

第一次请求:
浏览器 ──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

特性ETagLast-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/CSSpublic, max-age=31536000, immutable文件名变化 = 新资源
不带 hash 的 JS/CSSpublic, max-age=864001 天后重新验证
HTML 页面no-cache每次验证最新版本
API 数据no-storemax-age=60根据实时性要求
用户信息private, max-age=300仅浏览器缓存
图片(静态)public, max-age=259200030 天缓存
字体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=0Cache-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                        │
│  敏感数据 → 不缓存                               │
└─────────────────────────────────────────────────┘

⚠️ 注意事项

  1. 不要缓存敏感数据:使用 Cache-Control: no-store
  2. ETag 计算开销:大文件使用 Last-Modified 而非内容 hash
  3. Vary: *:会导致缓存几乎失效,谨慎使用
  4. CDN 缓存清除:部署时需要清除 CDN 缓存或使用版本号
  5. 测试缓存:使用 DevTools 的 Network 面板检查缓存状态
  6. 304 响应无 body:确保客户端处理 304 时不读取响应体

🔗 扩展阅读


下一章第 9 章:认证与授权 — Basic/Bearer/OAuth2/JWT/API Key 方案对比与实现