引言:HTTP缓存的重要性
HTTP缓存是Web性能优化的核心技术之一,它通过在客户端(浏览器)和服务器端存储资源副本,减少网络传输和服务器负载,从而显著提升网站加载速度和用户体验。根据Google的研究,页面加载时间每增加1秒,用户跳出率会上升32%。合理的缓存策略不仅能提升性能,还能降低带宽成本和服务器压力。
本文将深入探讨HTTP缓存的各个层面,从浏览器缓存机制到服务端优化策略,并详细解析缓存失效与一致性难题的解决方案。我们将通过实际案例和代码示例,帮助开发者全面掌握缓存技术。
一、浏览器缓存机制详解
1.1 缓存分类与存储位置
浏览器缓存主要分为强缓存和协商缓存两类,它们在不同的缓存阶段发挥作用:
- 强缓存:直接从浏览器本地缓存读取资源,不发送HTTP请求到服务器
- 协商缓存:浏览器发送请求到服务器,由服务器判断缓存是否有效
缓存存储位置按优先级排序:
- Service Worker:PWA技术中的独立线程,可完全控制网络请求
- Memory Cache:内存缓存,读取最快但容量小,随页面关闭消失
- Disk Cache:磁盘缓存,容量大持久化存储
- Push Cache:HTTP/2的服务器推送缓存,仅在会话中存在
1.2 强缓存:Expires与Cache-Control
Expires是HTTP/1.0的产物,表示资源的过期时间(GMT格式):
Expires: Thu, 31 Dec 2023 23:59:59 GMT
Cache-Control是HTTP/1.1的产物,采用相对时间,优先级更高:
Cache-Control: max-age=3600, public, must-revalidate
常用指令说明:
max-age=seconds:资源最大新鲜时间(秒)public:可被任何缓存(CDN、浏览器)缓存private:仅浏览器缓存,CDN不缓存no-cache:跳过强缓存,进入协商缓存no-store:不缓存,每次都从服务器获取must-revalidate:缓存过期后必须向服务器验证
代码示例:Node.js设置强缓存
const http = require('http');
const fs = require('fs');
http.createServer((req, res) => {
// 设置强缓存1小时
res.setHeader('Cache-Control', 'max-age=3600');
res.setHeader('Content-Type', 'text/html');
res.end('<h1>缓存测试页面</h1>');
}).listen(3000);
1.3 协商缓存:Last-Modified与ETag
当强缓存过期或使用no-cache时,进入协商缓存阶段:
Last-Modified / If-Modified-Since:
- 服务器返回
Last-Modified(资源最后修改时间) - 浏览器下次请求时携带
If-Modified-Since,服务器比较时间
ETag / If-None-Match:
- 服务器返回
ETag(资源唯一标识,通常是哈希值) - 浏览器下次请求时携带
If-None-Match,服务器比较标识
代码示例:Node.js实现协商缓存
const http = require('http');
const crypto = require('crypto');
const fs = require('fs');
http.createServer((req, res) => {
const content = fs.readFileSync('./index.html');
const etag = crypto.createHash('md5').update(content).digest('hex');
// 检查If-None-Match请求头
if (req.headers['if-none-match'] === etag) {
res.writeHead(304); // Not Modified
res.end();
return;
}
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'no-cache'); // 强制协商缓存
res.end(content);
}).listen(3000);
1.4 缓存优先级与决策流程
浏览器缓存决策流程如下:
- 检查Service Worker缓存
- 检查Memory Cache
- 检查Disk Cache
- 检查是否强缓存有效(Cache-Control/Expires)
- 强缓存无效则发送请求,进入协商缓存
- 协商缓存无效返回200,有效返回304
流程图示意:
请求资源 → Service Worker → Memory Cache → Disk Cache → 强缓存检查 → 协商缓存检查 → 网络请求
二、服务端缓存策略与实现
2.1 CDN缓存策略
CDN(内容分发网络)通过边缘节点缓存资源,减少回源请求:
配置示例(Nginx):
# 静态资源缓存1年
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Cache-Status $upstream_cache_status;
}
# HTML文件缓存策略
location ~* \.html$ {
expires 5m; # 5分钟
add_header Cache-Control "public, must-revalidate";
}
# API接口不缓存
location /api/ {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
}
CDN缓存键规则:
- 默认:完整URL(包含查询参数)
- 自定义:可忽略特定参数,如
utm_跟踪参数
2.2 反向代理缓存(Nginx Proxy Cache)
Nginx可以作为反向代理缓存后端动态内容:
# 缓存路径配置
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g
inactive=60m use_temp_path=off;
# 缓存规则
server {
listen 80;
server_name example.com;
location /api/ {
proxy_pass http://backend;
proxy_cache my_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 302 10m; # 200/302缓存10分钟
proxy_cache_valid 404 1m; # 404缓存1分钟
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
# 缓存状态头(调试用)
add_header X-Cache-Status $upstream_cache_status;
# 后端响应头控制
proxy_ignore_headers Cache-Control;
proxy_cache_valid any 5m;
}
}
2.3 应用层缓存(Redis/Memcached)
对于动态内容,应用层缓存是关键:
Node.js + Redis示例:
const redis = require('redis');
const client = redis.createClient();
// 缓存中间件
async function cacheMiddleware(req, res, next) {
const key = `cache:${req.url}`;
try {
// 尝试从缓存获取
const cached = await client.get(key);
if (cached) {
console.log('Cache HIT:', key);
return res.json(JSON.parse(cached));
}
// 重写res.json以缓存结果
const originalJson = res.json.bind(res);
res.json = function(data) {
// 缓存5分钟
client.setex(key, 300, JSON.stringify(data));
return originalJson(data);
};
next();
} catch (err) {
next();
}
}
// 路由使用
app.get('/api/user/:id', cacheMiddleware, async (req, res) => {
// 数据库查询等耗时操作
const user = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
res.json(user);
});
2.4 数据库查询缓存
MySQL查询缓存(注意:MySQL 8.0已移除):
-- 查看查询缓存状态
SHOW VARIABLES LIKE 'query_cache%';
-- 在my.cnf配置
[mysqld]
query_cache_type = 1
query_cache_size = 64M
query_cache_limit = 2M
-- 显式缓存查询(SQL_NO_CACHE)
SELECT SQL_CACHE * FROM products WHERE category_id = 5;
SELECT SQL_NO_CACHE * FROM products WHERE category_id = 5;
对于现代数据库,更推荐使用应用层缓存或数据库内置缓存(如MySQL的InnoDB Buffer Pool)。
三、缓存失效与一致性难题
3.1 缓存失效策略
主动失效 vs 被动失效:
- 主动失效:数据更新时立即清除相关缓存
- 被动失效:依赖缓存过期时间,期间可能读取旧数据
缓存更新模式:
Cache-Aside(旁路缓存):最常用模式
- 读取:先读缓存,命中返回;未命中读数据库并写入缓存
- 写入:先更新数据库,再删除缓存
Write-Through(写穿透):数据同时写入缓存和数据库
Write-Behind(写回):先写缓存,异步批量写入数据库
代码示例:Cache-Aside模式实现
class CacheAside {
constructor(redisClient, dbClient) {
this.redis = redisClient;
this.db = dbClient;
}
async get(key) {
// 1. 读缓存
const cached = await this.redis.get(key);
if (cached) return JSON.parse(cached);
// 2. 读数据库
const data = await this.db.query(`SELECT * FROM data WHERE id = ?`, [key]);
// 3. 写缓存
if (data) {
await this.redis.setex(key, 300, JSON.stringify(data));
}
return data;
}
async set(key, value) {
// 1. 更新数据库
await this.db.query(`UPDATE data SET value = ? WHERE id = ?`, [value, key]);
// 2. 删除缓存(推荐)或更新缓存
await this.redis.del(key);
// 注意:这里存在"先删缓存后更新数据库"期间的并发问题
// 解决方案:延迟双删、分布式锁等
}
}
3.2 缓存一致性挑战
经典问题:缓存与数据库不一致
场景:用户更新用户名,数据库更新成功,但缓存删除失败,导致后续读取旧数据。
解决方案:
方案1:延迟双删
async function updateUser(id, newName) {
// 1. 删除缓存
await redis.del(`user:${id}`);
// 2. 更新数据库
await db.query('UPDATE users SET name = ? WHERE id = ?', [newName, id]);
// 3. 延迟再次删除(防止并发写入导致的脏数据)
setTimeout(async () => {
await redis.del(`user:${id}`);
}, 500);
}
方案2:基于Binlog的缓存同步(Canal)
// Canal客户端监听MySQL Binlog变化
public class CacheUpdater {
public void handleUpdate(RowData rowData) {
String table = rowData.getTableName();
List<String> changedKeys = extractKeys(rowData);
// 根据变更表删除对应缓存
if ("users".equals(table)) {
for (String key : changedKeys) {
redis.del("user:" + key);
}
}
}
}
方案3:版本号/时间戳控制
// 在缓存中存储版本号
async function getWithVersion(key) {
const data = await redis.get(key);
if (!data) return null;
const { value, version } = JSON.parse(data);
const currentVersion = await redis.get(`version:${key}`);
if (version !== currentVersion) {
await redis.del(key);
return null;
}
return value;
}
// 更新时递增版本号
async function updateWithVersion(key, value) {
await db.query('UPDATE data SET value = ? WHERE id = ?', [value, key]);
await redis.incr(`version:${key}`);
}
3.3 缓存穿透、缓存击穿、缓存雪崩
缓存穿透:查询不存在的数据,导致请求直接打到数据库
- 解决方案:
- 布隆过滤器(Bloom Filter)拦截无效请求
- 缓存空值(设置较短过期时间)
// 布隆过滤器示例(使用redisbloom模块)
const BloomFilter = require('bloom-filter');
// 初始化布隆过滤器
const filter = BloomFilter.create(1000, 0.01); // 1000容量,1%误判率
// 插入有效ID
validIds.forEach(id => filter.insert(id.toString()));
// 查询时先检查
function queryUser(id) {
if (!filter.contains(id.toString())) {
return null; // 肯定不存在
}
// 再走正常缓存流程
return cacheAside.get(`user:${id}`);
}
缓存击穿:热点key过期瞬间大量请求涌入数据库
- 解决方案:
- 互斥锁(Mutex)保证单线程重建缓存
- 热点数据永不过期 + 异步更新
// 互斥锁实现缓存重建
async function getHotKey(key) {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
// 获取分布式锁
const lockKey = `lock:${key}`;
const lockValue = Date.now().toString();
const acquired = await redis.set(lockKey, lockValue, 'NX', 'EX', 10);
if (acquired) {
try {
// 双重检查,防止锁过期后重复计算
const cached2 = await redis.get(key);
if (cached2) return JSON.parse(cached2);
// 重建缓存
const data = await expensiveOperation();
await redis.setex(key, 300, JSON.stringify(data));
return data;
} finally {
// 释放锁(使用Lua脚本保证原子性)
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
await redis.eval(script, 1, lockKey, lockValue);
}
} else {
// 未获取锁,等待后重试
await sleep(50);
return getHotKey(key);
}
}
缓存雪崩:大量缓存同时过期,导致数据库压力激增
- 解决方案:
- 设置随机过期时间(基础时间 + 随机值)
- 热点数据永不过期 + 后台异步更新
- 多级缓存架构(本地缓存 + 分布式缓存)
// 随机过期时间设置
function setRandomExpire(key, baseTTL, randomRange) {
const ttl = baseTTL + Math.floor(Math.random() * randomRange);
return redis.setex(key, ttl, value);
}
// 多级缓存示例
class MultiLevelCache {
constructor() {
this.localCache = new Map();
this.redis = redis.createClient();
}
async get(key) {
// 1. 本地缓存
const local = this.localCache.get(key);
if (local && local.expiry > Date.now()) {
return local.value;
}
// 2. Redis缓存
const remote = await this.redis.get(key);
if (remote) {
// 回填本地缓存
this.localCache.set(key, {
value: JSON.parse(remote),
expiry: Date.now() + 60000 // 1分钟
});
return JSON.parse(remote);
}
// 3. 数据库
const data = await this.queryDB(key);
await this.redis.setex(key, 300, JSON.stringify(data));
return data;
}
}
3.4 缓存预热与监控
缓存预热:在系统启动或低峰期提前加载热点数据
// 缓存预热脚本
async function cacheWarmup() {
const hotKeys = [
'home:products',
'home:banner',
'config:app',
// 从数据库或配置中心获取热点key列表
];
for (const key of hotKeys) {
try {
const data = await generateData(key);
await redis.setex(key, 3600, JSON.stringify(data));
console.log(`Warmed up: ${key}`);
} catch (err) {
console.error(`Failed to warmup ${key}:`, err);
}
}
}
// 定时预热(每天凌晨执行)
const cron = require('node-cron');
cron.schedule('0 2 * * *', cacheWarmup); // 每天凌晨2点
缓存监控指标:
- 缓存命中率(Hit Rate):目标 > 95%
- 缓存大小与内存使用
- 缓存失效频率
- 缓存响应时间
// 监控示例
async function monitorCache() {
const info = await redis.info('stats');
const hitRate = parseInt(info.match(/keyspace_hits:(\d+)/)[1]) /
(parseInt(info.match(/keyspace_hits:(\d+)/)[1]) +
parseInt(info.match(/keyspace_misses:(\d+)/)[1]));
if (hitRate < 0.95) {
await alertLowHitRate(hitRate);
}
}
四、现代前端缓存策略
4.1 Webpack资源打包与缓存
长期缓存策略:
- 使用
[contenthash]:文件内容变化时hash才变化 - 提取第三方库到vendor chunk
- 使用
immutable指令
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js',
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
},
},
},
},
plugins: [
// 生成带hash的文件名
new HtmlWebpackPlugin({
template: './src/index.html',
// 自动注入带hash的资源
}),
],
};
Service Worker缓存策略:
// service-worker.js
const CACHE_NAME = 'app-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/bundle.js',
];
// 安装时缓存
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache))
);
});
// 拦截请求并返回缓存
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
// 缓存命中
if (response) return response;
// 缓存未命中,网络请求并缓存
return fetch(event.request).then(response => {
// 只缓存成功的响应
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
const responseToCache = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
// 更新时清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
4.2 HTTP/2 Server Push与缓存
HTTP/2 Server Push可以主动推送资源,但需谨慎使用:
// Node.js + HTTP/2 Push
const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer({
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt'),
});
server.on('stream', (stream, headers) => {
// 主推CSS和JS
stream.pushStream({ ':path': '/styles/main.css' }, (pushStream) => {
pushStream.respond({ ':status': 200 });
pushStream.end('body{color:red}');
});
stream.pushStream({ ':path': '/scripts/bundle.js' }, (pushStream) => {
pushStream.respond({ ':status': 200 });
pushStream.end('console.log("pushed")');
});
stream.respond({ ':status': 200 });
stream.end('<html>...</html>');
});
注意:Server Push的资源会被浏览器缓存,但推送的资源如果已在缓存中,会造成带宽浪费。Chrome 106+已默认禁用Server Push。
4.3 ETag在前端构建中的应用
ETag可用于实现灰度发布和A/B测试:
// 根据用户分组返回不同ETag
function handleRequest(req, res) {
const userId = getUserId(req);
const group = getABTestGroup(userId); // 'A' or 'B'
const content = group === 'A' ? versionA : versionB;
const etag = `"${group}-${hash(content)}"`;
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.setHeader('ETag', etag);
res.send(content);
}
五、高级缓存优化技巧
5.1 缓存键设计最佳实践
设计原则:
- 包含请求方法:
GET:/api/users/123 - 包含用户标识(私有缓存):
user:123:profile - 版本控制:
v2:product:456 - 规范化:忽略无关参数
// 缓存键生成器
class CacheKeyGenerator {
static generate(req, options = {}) {
const parts = [];
// 版本
if (options.version) {
parts.push(`v${options.version}`);
}
// 用户ID(私有缓存)
if (options.userId) {
parts.push(`user:${options.userId}`);
}
// 请求方法和路径
parts.push(`${req.method}:${req.path}`);
// 查询参数(规范化)
if (req.query && Object.keys(req.query).length > 0) {
const filtered = { ...req.query };
// 移除跟踪参数
delete filtered.utm_source;
delete filtered.utm_medium;
if (Object.keys(filtered).length > 0) {
const sorted = Object.keys(filtered).sort().map(k => `${k}=${filtered[k]}`).join('&');
parts.push(sorted);
}
}
return parts.join(':');
}
}
// 使用示例
const key = CacheKeyGenerator.generate(req, { version: 2, userId: 123 });
// 结果: "v2:user:123:GET:/api/products:category=electronics&sort=price"
5.2 缓存压缩与序列化优化
压缩存储:
const zlib = require('zlib');
const { promisify } = require('util');
const gzip = promisify(zlib.gzip);
async function setCompressed(key, data) {
const json = JSON.stringify(data);
const compressed = await gzip(json);
// 存储二进制数据
await redis.setBuffer(key, compressed);
}
async function getCompressed(key) {
const compressed = await redis.getBuffer(key);
if (!compressed) return null;
const decompressed = await promisify(zlib.gunzip)(compressed);
return JSON.parse(decompressed.toString());
}
序列化优化:
- 使用MessagePack代替JSON(更快更小)
- 避免存储大对象(>100KB)
- 只存储必要字段
const msgpack = require('msgpack-lite');
// 使用MessagePack序列化
async function setWithMsgpack(key, data) {
const encoded = msgpack.encode(data);
await redis.setBuffer(key, encoded);
}
async function getWithMsgpack(key) {
const buffer = await redis.getBuffer(key);
return msgpack.decode(buffer);
}
5.3 缓存分层与多级架构
典型多级缓存架构:
客户端 → 浏览器缓存 → Service Worker → 本地缓存(Redis/Memcached) → CDN → 数据库
实现示例:
class HierarchicalCache {
constructor() {
this.levels = [
new LocalCache(), // Level 0: 进程内缓存(如Node.js内存)
new RedisCache(), // Level 1: 分布式缓存
new DatabaseCache(), // Level 2: 数据库查询缓存
];
}
async get(key) {
let value = null;
// 从高级别缓存开始查找
for (let i = 0; i < this.levels.length; i++) {
value = await this.levels[i].get(key);
if (value !== null) {
// 回填低级别缓存
for (let j = i - 1; j >= 0; j--) {
await this.levels[j].set(key, value, this.levels[i].ttl);
}
return value;
}
}
return null;
}
async set(key, value, ttl) {
// 写入所有级别
await Promise.all(this.levels.map(level => level.set(key, value, ttl)));
}
}
5.4 缓存安全与防护
缓存污染攻击防护:
// 限制缓存键长度和数量
const MAX_KEY_LENGTH = 255;
const MAX_CACHE_SIZE = 10000;
function validateCacheKey(key) {
if (key.length > MAX_KEY_LENGTH) {
throw new Error('Cache key too long');
}
// 检查是否包含恶意字符
if (/[<>]/.test(key)) {
throw new Error('Invalid characters in cache key');
}
return true;
}
// 限制缓存值大小
function validateCacheValue(value) {
const size = Buffer.byteLength(JSON.stringify(value));
if (size > 1024 * 1024) { // 1MB
throw new Error('Cache value too large');
}
return true;
}
敏感数据缓存防护:
// 不缓存包含敏感信息的响应
function shouldCacheResponse(req, res) {
// 检查响应头
if (res.getHeader('Cache-Control')?.includes('no-store')) {
return false;
}
// 检查响应体是否包含敏感信息
const body = res.getBody();
if (body && containsSensitiveData(body)) {
return false;
}
// 检查请求路径
const sensitivePaths = ['/api/user/profile', '/api/payment'];
if (sensitivePaths.some(path => req.path.startsWith(path))) {
return false;
}
return true;
}
六、实战案例分析
6.1 电商网站缓存策略
场景:商品详情页,包含基本信息、库存、价格、评论等。
分层缓存设计:
// 商品详情服务
class ProductService {
async getProduct(productId, userId) {
const cacheKey = `product:${productId}:v2`; // 版本控制
// 1. 浏览器缓存(通过HTTP头)
// 2. CDN缓存(5分钟,公共)
// 3. Redis缓存(10分钟)
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 4. 数据库查询(分步查询)
const [basicInfo, inventory, price] = await Promise.all([
db.query('SELECT * FROM products WHERE id = ?', [productId]),
db.query('SELECT stock FROM inventory WHERE product_id = ?', [productId]),
db.query('SELECT price FROM prices WHERE product_id = ?', [productId]),
]);
const product = { ...basicInfo, inventory: inventory[0], price: price[0] };
// 5. 写入缓存
await redis.setex(cacheKey, 600, JSON.stringify(product));
return product;
}
async updateProductPrice(productId, newPrice) {
// 更新数据库
await db.query('UPDATE prices SET price = ? WHERE product_id = ?', [newPrice, productId]);
// 删除缓存(主动失效)
await redis.del(`product:${productId}:v2`);
// 发送MQ消息,异步清理CDN缓存
await mq.send('cache:invalidate', { key: `product:${productId}` });
}
}
HTTP缓存头配置:
# 商品详情页HTML
Cache-Control: public, max-age=300, must-revalidate
# 商品JSON API
Cache-Control: private, max-age=60, must-revalidate
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# 静态资源(图片、CSS、JS)
Cache-Control: public, max-age=31536000, immutable
6.2 社交媒体Feed流缓存
挑战:个性化内容、实时性强、数据量大。
解决方案:
class FeedService {
async getFeed(userId, page = 1) {
const cacheKey = `feed:${userId}:${page}`;
// 1. 尝试缓存
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 2. 计算Feed(复杂查询)
const feed = await this.calculateFeed(userId, page);
// 3. 短期缓存(1分钟)
await redis.setex(cacheKey, 60, JSON.stringify(feed));
return feed;
}
async calculateFeed(userId, page) {
// 关注的人
const following = await db.query(
'SELECT following_id FROM relationships WHERE follower_id = ?',
[userId]
);
if (following.length === 0) return [];
const followingIds = following.map(f => f.following_id);
// 获取帖子(带权重排序)
const posts = await db.query(`
SELECT p.*, u.username, u.avatar,
(p.likes * 0.5 + p.comments * 0.3 + p.shares * 0.2) as score
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.user_id IN (?) AND p.status = 'published'
ORDER BY score DESC, p.created_at DESC
LIMIT ? OFFSET ?
`, [followingIds, 10, (page - 1) * 10]);
return posts;
}
// 发布新帖子时清理相关缓存
async onNewPost(userId) {
// 清理该用户所有粉丝的Feed缓存
const followers = await db.query(
'SELECT follower_id FROM relationships WHERE following_id = ?',
[userId]
);
const pipeline = redis.pipeline();
followers.forEach(f => {
// 清理第一页
pipeline.del(`feed:${f.follower_id}:1`);
});
await pipeline.exec();
}
}
6.3 API网关缓存
场景:聚合多个微服务的API,减少下游调用。
// API网关缓存中间件
class GatewayCache {
constructor() {
this.redis = redis.createClient();
this.rateLimiter = new RateLimiterRedis({
storeClient: this.redis,
keyPrefix: 'gw_limit',
points: 100, // 100 requests
duration: 1,
});
}
async handleRequest(req, res, next) {
// 1. 限流检查
try {
await this.rateLimiter.consume(req.ip);
} catch (rejRes) {
return res.status(429).json({ error: 'Too Many Requests' });
}
// 2. 生成缓存键
const cacheKey = this.generateKey(req);
// 3. 检查缓存
const cached = await this.redis.get(cacheKey);
if (cached) {
const data = JSON.parse(cached);
res.setHeader('X-Cache', 'HIT');
res.setHeader('X-Cache-Key', cacheKey);
return res.json(data);
}
// 4. 代理请求到后端
res.setHeader('X-Cache', 'MISS');
// 重写res.json以缓存结果
const originalJson = res.json.bind(res);
res.json = function(data) {
// 根据响应决定缓存时间
const ttl = this.getCacheTTL(req, data);
if (ttl > 0) {
this.redis.setex(cacheKey, ttl, JSON.stringify(data));
}
return originalJson(data);
}.bind(this);
next();
}
generateKey(req) {
// 包含用户ID(私有缓存)
const userId = req.user?.id || 'anonymous';
// 规范化查询参数
const query = Object.keys(req.query).sort().join('&');
return `api:${userId}:${req.method}:${req.path}:${query}`;
}
getCacheTTL(req, data) {
// 根据数据特征决定缓存时间
if (req.path.startsWith('/api/products')) {
return 300; // 5分钟
}
if (req.path.startsWith('/api/user/profile')) {
return 60; // 1分钟
}
if (data.error) {
return 10; // 错误响应缓存短
}
return 0; // 不缓存
}
}
七、缓存测试与调试
7.1 缓存命中率监控
// 缓存监控类
class CacheMonitor {
constructor() {
this.metrics = {
hits: 0,
misses: 0,
total: 0,
startTime: Date.now(),
};
}
recordHit() {
this.metrics.hits++;
this.metrics.total++;
}
recordMiss() {
this.metrics.misses++;
this.metrics.total++;
}
getStats() {
const duration = (Date.now() - this.metrics.startTime) / 1000;
return {
hitRate: this.metrics.hits / this.metrics.total,
hits: this.metrics.hits,
misses: this.metrics.misses,
total: this.metrics.total,
requestsPerSecond: this.metrics.total / duration,
};
}
// 每分钟打印统计
startReporting() {
setInterval(() => {
const stats = this.getStats();
console.log(`Cache Stats: Hit Rate ${(stats.hitRate * 100).toFixed(2)}%`);
// 重置计数器
this.metrics = { hits: 0, misses: 0, total: 0, startTime: Date.now() };
}, 60000);
}
}
// 使用
const monitor = new CacheMonitor();
monitor.startReporting();
// 在缓存访问处埋点
async function getCachedData(key) {
const data = await redis.get(key);
if (data) {
monitor.recordHit();
return JSON.parse(data);
}
monitor.recordMiss();
// ...从数据库获取
}
7.2 缓存调试工具
使用Chrome DevTools:
- Network面板:查看
Size列,(from disk cache)表示磁盘缓存 - Application面板:查看Cache Storage、Local Storage等
- 命令行:
chrome://net-internals/#httpCache
使用curl测试缓存:
# 第一次请求(MISS)
curl -I -H "Cache-Control: no-cache" https://example.com/resource
# 第二次请求(HIT)
curl -I https://example.com/resource
# 携带If-None-Match
curl -I -H "If-None-Match: \"abc123\"" https://example.com/resource
# 查看缓存详情
curl -v -H "Cache-Control: no-cache" https://example.com/resource 2>&1 | grep -i cache
Nginx缓存状态监控:
# 在响应头添加缓存状态
add_header X-Cache-Status $upstream_cache_status;
# 日志格式
log_format cache '$remote_addr - $upstream_cache_status - $request_uri';
access_log /var/log/nginx/cache.log cache;
7.3 缓存问题排查清单
常见问题与解决方案:
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 缓存不生效 | Cache-Control头被覆盖 | 检查中间件顺序,确认没有重复设置 |
| 缓存命中率低 | 缓存键设计不合理 | 检查是否包含用户标识、版本号 |
| 数据不一致 | 删除缓存失败 | 检查Redis连接,添加重试机制 |
| 缓存过大 | 存储了完整响应 | 只存储必要字段,启用压缩 |
| 缓存穿透 | 查询大量不存在的数据 | 实现布隆过滤器或缓存空值 |
八、总结与最佳实践
8.1 缓存策略决策树
资源类型?
├── 静态资源(JS/CSS/图片)
│ └── Cache-Control: public, max-age=31536000, immutable
│
├── 动态HTML
│ └── Cache-Control: public/private, max-age=300, must-revalidate
│
├── API数据
│ ├── 个性化数据 → private, max-age=60
│ ├── 公共数据 → public, max-age=300
│ └── 实时数据 → no-store
│
└── 用户特定内容
└── ETag + private, max-age=0, must-revalidate
8.2 黄金法则
- 缓存一切可缓存的:从静态资源到API响应
- 分层缓存:浏览器 → CDN → 应用 → 数据库
- 主动失效:数据更新时立即清理缓存
- 监控驱动:持续监控命中率并优化
- 安全第一:防止缓存穿透、击穿、雪崩
- 版本控制:使用hash或版本号避免缓存污染
8.3 性能指标参考
- 缓存命中率:> 95%(静态资源应接近100%)
- TTFB(首字节时间):缓存命中应 < 50ms
- CDN回源率:> 80%(理想情况)
- 缓存响应时间:P99 < 10ms
8.4 未来趋势
- HTTP/3:QUIC协议对缓存的影响
- 边缘计算:在CDN边缘节点执行缓存逻辑
- 智能缓存:基于机器学习预测缓存热点
- Web Bundles:资源打包对缓存的影响
通过本文的详细解析,你应该已经掌握了HTTP缓存的完整知识体系。记住,缓存是性能优化的利器,但需要根据业务场景合理设计,持续监控和调优,才能发挥最大价值。
