阶段二:RESTful进阶
缓存是API性能最有效的优化手段之一——合理的缓存策略能让API响应速度提升10倍以上,同时减少服务器负载。但缓存不当会导致数据不一致、内存泄漏和难以调试的bug。本课深入HTTP缓存、应用层缓存、CDN缓存和缓存失效策略。
| 层级 | 位置 | 典型工具 | 特点 |
|---|---|---|---|
| 浏览器缓存 | 客户端 | HTTP Cache-Control | 零网络开销,用户级 |
| CDN缓存 | 边缘节点 | Cloudflare, Fastly | 就近响应,全球加速 |
| 反向代理缓存 | 网关层 | Nginx, Varnish | 服务端第一道防线 |
| 应用缓存 | 应用层 | Redis, Memcached | 精细控制,可编程 |
# Cache-Control 指令
Cache-Control: max-age=3600 # 缓存1小时
Cache-Control: no-cache # 必须验证(常见误解:不是不缓存!)
Cache-Control: no-store # 完全不缓存(敏感数据)
Cache-Control: private # 仅浏览器缓存(CDN不缓存)
Cache-Control: public # 允许所有缓存
Cache-Control: must-revalidate # 过期后必须验证
Cache-Control: immutable # 过期前不会变化(资源指纹)
# Expires(HTTP/1.0,不推荐)
Expires: Wed, 15 Jan 2025 08:00:00 GMT
# ETag 验证
# 首次响应
ETag: "abc123"
# 后续请求
If-None-Match: "abc123"
# 如果未变化 → 304 Not Modified(无响应体)
# 如果已变化 → 200 + 新资源 + 新ETag
# Last-Modified 验证
# 首次响应
Last-Modified: Wed, 15 Jan 2025 08:00:00 GMT
# 后续请求
If-Modified-Since: Wed, 15 Jan 2025 08:00:00 GMT
| API类型 | 缓存策略 | 示例头 |
|---|---|---|
| 公共只读数据 | 强缓存+长TTL | Cache-Control: public, max-age=86400 |
| 用户个人数据 | 协商缓存 | Cache-Control: private, no-cache |
| 实时数据 | 不缓存 | Cache-Control: no-store |
| 敏感数据 | 不缓存 | Cache-Control: no-store, private |
| 静态资源 | 强缓存+immutable | Cache-Control: public, max-age=31536000, immutable |
// caching-api.js - 带完整缓存策略的API
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// ===== 应用层缓存(LRU) =====
class LRUCache {
constructor(maxSize = 1000, defaultTTL = 60) {
this.maxSize = maxSize;
this.defaultTTL = defaultTTL; // 默认TTL(秒)
this.cache = new Map();
this.stats = { hits: 0, misses: 0, sets: 0, evictions: 0 };
}
get(key) {
const entry = this.cache.get(key);
if (!entry) {
this.stats.misses++;
return null;
}
// 检查过期
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
this.stats.misses++;
return null;
}
// LRU: 移到末尾
this.cache.delete(key);
this.cache.set(key, entry);
this.stats.hits++;
return entry.data;
}
set(key, data, ttl) {
const effectiveTTL = ttl !== undefined ? ttl : this.defaultTTL;
// 容量检查
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
this.stats.evictions++;
}
this.cache.set(key, {
data,
expiresAt: Date.now() + effectiveTTL * 1000,
createdAt: Date.now(),
});
this.stats.sets++;
}
invalidate(pattern) {
if (pattern === '*') {
this.cache.clear();
return;
}
for (const key of this.cache.keys()) {
if (key.startsWith(pattern)) {
this.cache.delete(key);
}
}
}
getStats() {
const total = this.stats.hits + this.stats.misses;
return {
...this.stats,
size: this.cache.size,
hitRate: total > 0 ? (this.stats.hits / total * 100).toFixed(2) + '%' : '0%',
};
}
}
const cache = new LRUCache(500, 60);
// ===== 数据层 =====
const products = [];
for (let i = 1; i <= 100; i++) {
products.push({
id: i,
name: `产品${i}`,
price: Math.round(Math.random() * 5000 * 100) / 100,
stock: Math.floor(Math.random() * 200),
updatedAt: new Date().toISOString(),
});
}
// ===== ETag工具 =====
function generateETag(data) {
const str = JSON.stringify(data);
return '"' + crypto.createHash('md5').update(str).digest('hex').substring(0, 12) + '"';
}
// ===== 缓存中间件 =====
// HTTP缓存中间件(ETag + Cache-Control)
function httpCache(options = {}) {
const { maxAge = 60, private = false, noCache = false, noStore = false } = options;
return (req, res, next) => {
if (noStore) {
res.setHeader('Cache-Control', 'no-store, private');
return next();
}
if (noCache) {
res.setHeader('Cache-Control', 'no-cache, private');
} else {
const directives = [private ? 'private' : 'public', `max-age=${maxAge}`];
res.setHeader('Cache-Control', directives.join(', '));
}
// 覆盖res.json以自动添加ETag
const originalJson = res.json.bind(res);
res.json = function(data) {
const etag = generateETag(data);
res.setHeader('ETag', etag);
// 检查客户端的If-None-Match
const clientETag = req.headers['if-none-match'];
if (clientETag === etag) {
return res.status(304).end();
}
return originalJson(data);
};
next();
};
}
// 应用层缓存中间件
function appCache(ttl) {
return (req, res, next) => {
const cacheKey = `${req.method}:${req.originalUrl}`;
const cached = cache.get(cacheKey);
if (cached) {
res.setHeader('X-Cache', 'HIT');
res.setHeader('X-Cache-Age', Math.floor((Date.now() - cached.cachedAt) / 1000) + 's');
return res.json(cached.data);
}
res.setHeader('X-Cache', 'MISS');
// 覆盖res.json以缓存响应
const originalJson = res.json.bind(res);
res.json = function(data) {
// 只缓存成功响应
if (res.statusCode >= 200 && res.statusCode < 300) {
cache.set(cacheKey, { data, cachedAt: Date.now() }, ttl);
}
return originalJson(data);
};
next();
};
}
// 缓存失效中间件
function invalidateCache(...patterns) {
return (req, res, next) => {
const originalEnd = res.end.bind(res);
res.end = function(...args) {
if (res.statusCode >= 200 && res.statusCode < 300) {
patterns.forEach(p => cache.invalidate(p));
}
return originalEnd(...args);
};
next();
};
}
// ===== 路由 =====
// 产品列表 - 双层缓存(HTTP + 应用层)
app.get('/api/products', httpCache({ maxAge: 60, private: false }), appCache(30), (req, res) => {
const { page = 1, limit = 20 } = req.query;
const start = (page - 1) * limit;
const data = products.slice(start, start + Number(limit));
res.json({
success: true,
data,
pagination: { page: Number(page), limit: Number(limit), total: products.length },
});
});
// 产品详情 - ETag缓存
app.get('/api/products/:id', httpCache({ maxAge: 300 }), appCache(120), (req, res) => {
const product = products.find(p => p.id === parseInt(req.params.id));
if (!product) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
res.json({ success: true, data: product });
});
// 创建产品 - 失效列表缓存
app.post('/api/products', invalidateCache('GET:/api/products'), (req, res) => {
const id = products.length + 1;
const product = { id, ...req.body, updatedAt: new Date().toISOString() };
products.push(product);
res.status(201).json({ success: true, data: product });
});
// 更新产品 - 失效详情+列表缓存
app.patch('/api/products/:id',
invalidateCache('GET:/api/products', `GET:/api/products/${req?.params?.id}`),
(req, res) => {
const product = products.find(p => p.id === parseInt(req.params.id));
if (!product) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
Object.assign(product, req.body, { updatedAt: new Date().toISOString() });
res.json({ success: true, data: product });
}
);
// 用户数据 - 不缓存(私有数据)
app.get('/api/users/me', httpCache({ noStore: true }), (req, res) => {
res.json({ success: true, data: { id: 1, name: '当前用户', email: 'me@example.com' } });
});
// 缓存管理端点
app.get('/api/admin/cache/stats', (req, res) => {
res.json({ success: true, data: cache.getStats() });
});
app.delete('/api/admin/cache', (req, res) => {
cache.invalidate('*');
res.json({ success: true, data: { message: '缓存已清除' } });
});
app.listen(3000, () => console.log('🗄️ 缓存策略API运行在 http://localhost:3000'));
# 1. 首次请求 - MISS
curl -s http://localhost:3000/api/products -D - | head -20
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
ETag: "a1b2c3d4e5f6"
X-Cache: MISS
Content-Type: application/json
# 2. 应用层缓存命中 - HIT
curl -s http://localhost:3000/api/products -D - | grep X-Cache
# 3. HTTP ETag验证 - 304
curl -s http://localhost:3000/api/products \
-H 'If-None-Match: "a1b2c3d4e5f6"' -D - | head -5
HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4e5f6"
Cache-Control: public, max-age=60
# 4. 缓存统计
curl -s http://localhost:3000/api/admin/cache/stats | jq .
# 5. 清除缓存
curl -s -X DELETE http://localhost:3000/api/admin/cache | jq .
# 6. 私有数据 - 不缓存
curl -s http://localhost:3000/api/users/me -D - | grep Cache-Control
| 策略 | 说明 | 适用场景 |
|---|---|---|
| TTL过期 | 设置过期时间,到时自动失效 | 时间敏感度低的数据 |
| 主动失效 | 数据变更时主动清除相关缓存 | 强一致性要求 |
| 版本号/时间戳 | 数据变更时更新版本号 | 分布式缓存同步 |
| 事件通知 | 通过消息队列广播失效事件 | 多实例部署 |
// 主动缓存失效的完整模式
// 1. 写操作 → 失效读缓存
// 创建产品后,列表缓存失效
app.post('/api/products', (req, res) => {
const product = createProduct(req.body);
// 失效所有产品列表缓存(无论分页参数如何)
cache.invalidate('GET:/api/products');
// 也失效相关分类的缓存
cache.invalidate(`GET:/api/categories/${product.category}/products`);
res.status(201).json({ success: true, data: product });
});
// 2. 更新操作 → 失效详情+列表缓存
app.patch('/api/products/:id', (req, res) => {
const product = updateProduct(req.params.id, req.body);
// 失效详情缓存
cache.invalidate(`GET:/api/products/${req.params.id}`);
// 失效列表缓存
cache.invalidate('GET:/api/products');
res.json({ success: true, data: product });
});
// 3. 删除操作 → 失效所有相关缓存
app.delete('/api/products/:id', (req, res) => {
deleteProduct(req.params.id);
cache.invalidate('GET:/api/products'); // 列表
cache.invalidate(`GET:/api/products/${req.params.id}`); // 详情
cache.invalidate('GET:/api/stats'); // 统计
res.status(204).end();
});
组合LRU内存缓存(热数据,1分钟TTL)和Redis缓存(温数据,10分钟TTL),实现二级缓存。查询顺序:内存→Redis→数据库。
当缓存过期时,大量请求同时到达数据库(缓存击穿)。实现singleflight模式:同一个key只允许一个请求查询数据库,其他请求等待结果。