引言
HTTP缓存是Web性能优化的核心技术之一,它通过在客户端(浏览器)和服务器端存储资源副本,显著减少网络请求、降低服务器负载并提升用户体验。本文将从HTTP缓存的基本原理出发,深入探讨各种缓存策略的实现方式,并通过实际案例展示如何在不同场景下应用这些策略。
一、HTTP缓存基础概念
1.1 什么是HTTP缓存
HTTP缓存是指在HTTP请求-响应链中,将资源副本存储在客户端(浏览器)、代理服务器或CDN等中间节点,以便后续请求可以直接使用这些副本,而无需重新从源服务器获取资源。
1.2 缓存的分类
根据缓存存储位置的不同,HTTP缓存可分为:
- 浏览器缓存:存储在用户浏览器中
- 代理缓存:存储在代理服务器(如公司网络代理)
- CDN缓存:存储在内容分发网络的边缘节点
- 服务器缓存:存储在源服务器(如Redis、Memcached)
1.3 缓存的好处
- 减少网络延迟:避免重复下载相同资源
- 降低服务器负载:减少源服务器的请求处理量
- 节省带宽:减少数据传输量
- 提升用户体验:页面加载更快
二、HTTP缓存机制详解
2.1 缓存控制头字段
HTTP协议通过一系列头部字段来控制缓存行为,主要分为两类:
2.1.1 强缓存相关字段
Cache-Control:最常用的缓存控制字段,支持多种指令
Cache-Control: max-age=3600, public, must-revalidate
常用指令:
max-age=<seconds>:资源在客户端缓存的最大时间(秒)no-store:禁止任何缓存no-cache:缓存但每次使用前需验证public:资源可被任何缓存存储private:资源只能被浏览器缓存must-revalidate:缓存过期后必须重新验证
Expires:指定资源过期的具体时间(HTTP/1.0遗留字段)
Expires: Wed, 21 Oct 2025 07:28:00 GMT
2.1.2 协商缓存相关字段
ETag:资源的唯一标识符(哈希值)
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified:资源最后修改时间
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
If-None-Match:客户端发送的ETag验证请求头
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-Modified-Since:客户端发送的最后修改时间验证请求头
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
2.2 缓存决策流程
浏览器处理HTTP缓存的决策流程如下:
1. 检查请求是否命中强缓存
├── 命中:直接使用缓存(200 from cache)
└── 未命中:进入协商缓存流程
2. 检查请求是否命中协商缓存
├── 命中:返回304 Not Modified
└── 未命中:重新请求资源(200 OK)
2.3 缓存验证流程
2.3.1 强缓存验证
当浏览器发现资源在Cache-Control: max-age指定的时间内时,直接使用缓存,不发送网络请求。
// 示例:浏览器控制台查看缓存状态
fetch('/api/data', { cache: 'force-cache' })
.then(response => {
console.log(response.status); // 200(来自缓存)
console.log(response.headers.get('age')); // 缓存年龄
});
2.3.2 协商缓存验证
当强缓存过期后,浏览器发送请求时携带验证头,服务器比较后决定返回304还是200。
ETag验证流程:
- 浏览器发送请求,携带
If-None-Match: "ETag值" - 服务器比较ETag,如果匹配返回304,否则返回200
Last-Modified验证流程:
- 浏览器发送请求,携带
If-Modified-Since: "时间戳" - 服务器比较修改时间,如果未修改返回304,否则返回200
三、缓存策略实践指南
3.1 不同类型资源的缓存策略
3.1.1 静态资源(CSS/JS/图片)
策略:长期缓存 + 版本控制
# Nginx配置示例
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Cache-Control "max-age=31536000";
# 版本控制:文件名包含哈希值
# 如:app.a1b2c3d4.js
}
原理:静态资源通常不会频繁修改,通过文件名哈希(如app.a1b2c3d4.js)实现版本控制,确保更新后浏览器能获取新版本。
3.1.2 动态API数据
策略:协商缓存 + 适当过期时间
// Node.js Express示例
app.get('/api/user/:id', (req, res) => {
const userId = req.params.id;
const data = getUserData(userId);
const etag = generateETag(data);
// 检查客户端ETag
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.set({
'ETag': etag,
'Cache-Control': 'public, max-age=60', // 60秒
'Last-Modified': new Date().toUTCString()
});
res.json(data);
});
3.1.3 HTML文档
策略:短时间缓存 + no-cache
# Apache配置示例
<FilesMatch "\.html$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
</FilesMatch>
原因:HTML是页面入口,需要确保用户获取最新版本,避免缓存导致的版本不一致问题。
3.2 缓存策略配置示例
3.2.1 Nginx配置
# 完整的缓存配置示例
server {
listen 80;
server_name example.com;
# 静态资源缓存
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# 1年缓存
expires 1y;
add_header Cache-Control "public, immutable, max-age=31536000";
# 开启Gzip压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# 禁止访问隐藏文件
location ~ /\. {
deny all;
}
}
# API接口缓存
location /api/ {
# 默认不缓存
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
# 特定API缓存
location /api/public/ {
expires 300s;
add_header Cache-Control "public, max-age=300";
}
# 代理到后端
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# HTML页面缓存
location ~* \.html$ {
expires 300s;
add_header Cache-Control "public, max-age=300";
# 禁止缓存特定页面
location ~* /(admin|login|dashboard) {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
}
3.2.2 Apache配置
# .htaccess配置示例
<IfModule mod_expires.c>
ExpiresActive On
# 默认缓存策略
ExpiresDefault "access plus 1 month"
# 图片缓存1年
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
# CSS/JS缓存1年
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
# HTML缓存1小时
ExpiresByType text/html "access plus 1 hour"
# 字体文件缓存1年
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/ttf "access plus 1 year"
ExpiresByType font/eot "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
# 添加Cache-Control头
<FilesMatch "\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$">
Header set Cache-Control "public, immutable, max-age=31536000"
</FilesMatch>
# HTML页面禁止缓存
<FilesMatch "\.html$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
</FilesMatch>
</IfModule>
3.2.3 Express.js中间件配置
const express = require('express');
const compression = require('compression');
const helmet = require('helmet');
const app = express();
// 压缩中间件
app.use(compression());
// 安全头中间件
app.use(helmet({
contentSecurityPolicy: false, // 根据需要配置
hsts: false
}));
// 静态资源缓存中间件
app.use('/static', express.static('public', {
maxAge: '1y', // 1年
etag: true,
lastModified: true,
setHeaders: (res, path) => {
if (path.endsWith('.html')) {
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
} else {
res.set('Cache-Control', 'public, immutable, max-age=31536000');
}
}
}));
// API缓存中间件
const cacheMiddleware = (duration) => {
return (req, res, next) => {
// 检查是否为GET请求
if (req.method !== 'GET') {
return next();
}
// 生成缓存键
const cacheKey = `${req.method}:${req.originalUrl}`;
// 检查缓存
const cached = cache.get(cacheKey);
if (cached) {
// 检查ETag
const clientETag = req.headers['if-none-match'];
if (clientETag && clientETag === cached.etag) {
res.status(304).end();
return;
}
// 返回缓存数据
res.set('ETag', cached.etag);
res.set('Cache-Control', `public, max-age=${duration}`);
res.json(cached.data);
return;
}
// 重写res.json方法以捕获响应
const originalJson = res.json.bind(res);
res.json = function(data) {
const etag = generateETag(data);
cache.set(cacheKey, {
data: data,
etag: etag,
timestamp: Date.now()
}, duration * 1000);
res.set('ETag', etag);
res.set('Cache-Control', `public, max-age=${duration}`);
return originalJson(data);
};
next();
};
};
// 应用缓存中间件
app.get('/api/products', cacheMiddleware(300), (req, res) => {
// 业务逻辑
res.json({ products: [...] });
});
// 启动服务器
app.listen(3000, () => {
console.log('Server running on port 3000');
});
3.3 缓存失效策略
3.3.1 版本控制
文件名哈希策略:
// Webpack配置示例
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
}
})
]
};
生成的文件名:
app.a1b2c3d4.jsvendor.e5f6g7h8.jsstyles.i9j0k1l2.css
3.3.2 缓存清除策略
主动清除:
// 使用Cache API清除缓存
async function clearCache() {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(name => caches.delete(name))
);
}
// 使用Service Worker清除
self.addEventListener('message', event => {
if (event.data.action === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data.action === 'CLEAR_CACHE') {
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});
}
});
被动清除:
- 设置合理的
max-age时间 - 使用
must-revalidate确保过期后验证 - 对于关键数据,使用
no-cache
四、高级缓存策略
4.1 CDN缓存策略
4.1.1 CDN缓存配置
# CDN边缘节点配置
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# 缓存1年
expires 1y;
add_header Cache-Control "public, immutable, max-age=31536000";
# CDN特定头
add_header X-Cache-Status $upstream_cache_status;
add_header X-Cache-Date $upstream_http_date;
# 缓存键配置
proxy_cache_key "$scheme$request_method$host$request_uri$is_args$args";
# 缓存区域
proxy_cache STATIC_CACHE;
proxy_cache_valid 200 304 1y;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_lock on;
}
4.1.2 多级缓存策略
用户浏览器 → CDN边缘节点 → CDN中心节点 → 源服务器
配置示例:
// 多级缓存配置
const multiLevelCache = {
// 浏览器缓存
browser: {
static: 'max-age=31536000, immutable',
api: 'max-age=300, must-revalidate'
},
// CDN缓存
cdn: {
static: 'max-age=31536000',
api: 'max-age=300'
},
// 源服务器缓存
origin: {
redis: {
ttl: 300,
keyPrefix: 'api:'
}
}
};
4.2 Service Worker缓存
4.2.1 Service Worker基础配置
// sw.js - Service Worker文件
const CACHE_NAME = 'my-app-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
];
// 安装阶段:缓存静态资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Caching static assets');
return cache.addAll(STATIC_ASSETS);
})
.then(() => self.skipWaiting())
);
});
// 激活阶段:清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
}).then(() => self.clients.claim())
);
});
// 拦截请求并返回缓存
self.addEventListener('fetch', event => {
// 只处理GET请求
if (event.request.method !== 'GET') {
return;
}
// 忽略chrome扩展等请求
if (!event.request.url.startsWith(self.location.origin)) {
return;
}
// 策略1: 网络优先,缓存回退
event.respondWith(
fetch(event.request)
.then(response => {
// 克隆响应,因为响应只能使用一次
const responseClone = response.clone();
// 缓存成功的响应
if (response.status === 200) {
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, responseClone));
}
return response;
})
.catch(() => {
// 网络失败时返回缓存
return caches.match(event.request);
})
);
});
// 后台同步
self.addEventListener('sync', event => {
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});
// 推送通知
self.addEventListener('push', event => {
const options = {
body: event.data ? event.data.text() : 'New notification',
icon: '/images/icon-192x192.png',
badge: '/images/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{ action: 'explore', title: 'Go to the site' },
{ action: 'close', title: 'Close notification' }
]
};
event.waitUntil(
self.registration.showNotification('Push Notification', options)
);
});
4.2.2 高级缓存策略
// 策略模式实现
const strategies = {
// 网络优先
networkFirst: async (request) => {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
await cache.put(request, response.clone());
return response;
} catch (error) {
const cached = await caches.match(request);
if (cached) return cached;
throw error;
}
},
// 缓存优先
cacheFirst: async (request) => {
const cached = await caches.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
await cache.put(request, response.clone());
return response;
} catch (error) {
throw error;
}
},
// 仅缓存
cacheOnly: async (request) => {
const cached = await caches.match(request);
if (cached) return cached;
throw new Error('No cache available');
},
// 仅网络
networkOnly: async (request) => {
return fetch(request);
},
// 快照策略(适用于离线应用)
snapshot: async (request) => {
const cached = await caches.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
await cache.put(request, response.clone());
return response;
} catch (error) {
// 返回离线页面
return caches.match('/offline.html');
}
}
};
// 根据URL选择策略
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 静态资源:缓存优先
if (url.pathname.startsWith('/static/')) {
event.respondWith(strategies.cacheFirst(event.request));
}
// API请求:网络优先
else if (url.pathname.startsWith('/api/')) {
event.respondWith(strategies.networkFirst(event.request));
}
// HTML页面:快照策略
else if (url.pathname.endsWith('.html')) {
event.respondWith(strategies.snapshot(event.request));
}
// 其他:网络优先
else {
event.respondWith(strategies.networkFirst(event.request));
}
});
4.3 缓存预热策略
4.3.1 预热配置
// 缓存预热脚本
const axios = require('axios');
const fs = require('fs');
const path = require('path');
class CacheWarmer {
constructor(config) {
this.config = config;
this.cache = new Map();
}
// 预热静态资源
async warmStaticAssets() {
const assets = this.config.staticAssets;
for (const asset of assets) {
try {
const response = await axios.get(asset.url, {
timeout: 5000,
responseType: 'stream'
});
// 缓存到内存
this.cache.set(asset.url, {
data: response.data,
headers: response.headers,
timestamp: Date.now()
});
console.log(`✅ 预热成功: ${asset.url}`);
} catch (error) {
console.error(`❌ 预热失败: ${asset.url}`, error.message);
}
}
}
// 预热API数据
async warmApiData() {
const apis = this.config.apis;
for (const api of apis) {
try {
const response = await axios.get(api.url, {
timeout: 5000,
headers: api.headers || {}
});
// 生成ETag
const etag = this.generateETag(response.data);
// 缓存到Redis
await this.cacheToRedis(api.cacheKey, {
data: response.data,
etag: etag,
timestamp: Date.now()
}, api.ttl);
console.log(`✅ API预热成功: ${api.url}`);
} catch (error) {
console.error(`❌ API预热失败: ${api.url}`, error.message);
}
}
}
// 生成ETag
generateETag(data) {
const crypto = require('crypto');
const hash = crypto.createHash('md5');
hash.update(JSON.stringify(data));
return hash.digest('hex');
}
// 缓存到Redis
async cacheToRedis(key, data, ttl) {
const Redis = require('ioredis');
const redis = new Redis();
await redis.setex(key, ttl, JSON.stringify(data));
await redis.quit();
}
// 定时预热
startScheduledWarm() {
// 每小时预热一次
setInterval(async () => {
console.log('开始定时预热...');
await this.warmStaticAssets();
await this.warmApiData();
}, 3600000);
// 每天凌晨2点全量预热
const cron = require('node-cron');
cron.schedule('0 2 * * *', async () => {
console.log('开始每日全量预热...');
await this.warmStaticAssets();
await this.warmApiData();
});
}
}
// 使用示例
const warmer = new CacheWarmer({
staticAssets: [
{ url: 'https://example.com/static/app.js' },
{ url: 'https://example.com/static/styles.css' },
{ url: 'https://example.com/images/logo.png' }
],
apis: [
{
url: 'https://api.example.com/products',
cacheKey: 'api:products',
ttl: 300,
headers: { 'Authorization': 'Bearer token' }
}
]
});
// 启动预热
warmer.warmStaticAssets();
warmer.warmApiData();
warmer.startScheduledWarm();
五、缓存性能监控与调试
5.1 浏览器开发者工具
5.1.1 Chrome DevTools Network面板
// 在控制台查看缓存状态
fetch('/api/data')
.then(response => {
console.log('Status:', response.status);
console.log('From Cache:', response.headers.get('age') ? 'Yes' : 'No');
console.log('Cache-Control:', response.headers.get('cache-control'));
console.log('ETag:', response.headers.get('etag'));
console.log('Last-Modified:', response.headers.get('last-modified'));
});
5.1.2 缓存存储查看
在Chrome DevTools中:
- 打开Application面板
- 查看Cache Storage
- 检查Service Worker缓存
- 查看IndexedDB存储
5.2 服务器端监控
5.2.1 Nginx日志分析
# 自定义日志格式
log_format cache_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'Cache-Control: "$sent_http_cache_control" '
'ETag: "$sent_http_etag" '
'Age: "$sent_http_age" '
'X-Cache-Status: "$upstream_cache_status"';
# 应用日志格式
access_log /var/log/nginx/cache.log cache_log;
5.2.2 缓存命中率监控
// Node.js监控脚本
const fs = require('fs');
const readline = require('readline');
class CacheMonitor {
constructor(logPath) {
this.logPath = logPath;
this.stats = {
total: 0,
hits: 0,
misses: 0,
byStatus: {}
};
}
async analyze() {
const fileStream = fs.createReadStream(this.logPath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
this.stats.total++;
// 解析日志行
const match = line.match(/X-Cache-Status: "(\w+)"/);
if (match) {
const status = match[1];
this.stats.byStatus[status] = (this.stats.byStatus[status] || 0) + 1;
if (status === 'HIT') {
this.stats.hits++;
} else if (status === 'MISS') {
this.stats.misses++;
}
}
}
// 计算命中率
this.stats.hitRate = this.stats.total > 0
? (this.stats.hits / this.stats.total * 100).toFixed(2)
: 0;
return this.stats;
}
generateReport() {
return `
缓存性能报告
================
总请求数: ${this.stats.total}
缓存命中数: ${this.stats.hits}
缓存未命中数: ${this.stats.misses}
缓存命中率: ${this.stats.hitRate}%
详细统计:
${Object.entries(this.stats.byStatus)
.map(([status, count]) => ` ${status}: ${count} (${(count/this.stats.total*100).toFixed(2)}%)`)
.join('\n')}
`;
}
}
// 使用示例
const monitor = new CacheMonitor('/var/log/nginx/cache.log');
monitor.analyze().then(stats => {
console.log(monitor.generateReport());
});
5.3 缓存调试工具
5.3.1 curl测试缓存
# 测试强缓存
curl -I -H "Cache-Control: no-cache" https://example.com/static/app.js
# 测试协商缓存
curl -I -H "If-None-Match: \"abc123\"" https://example.com/api/data
# 测试Last-Modified
curl -I -H "If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT" https://example.com/api/data
# 查看完整响应头
curl -v https://example.com/static/app.js
5.3.2 缓存验证脚本
// 缓存验证脚本
const https = require('https');
const http = require('http');
class CacheValidator {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async validateCache(url, options = {}) {
const fullUrl = this.baseUrl + url;
const protocol = fullUrl.startsWith('https') ? https : http;
return new Promise((resolve, reject) => {
const req = protocol.request(fullUrl, {
method: 'GET',
headers: options.headers || {}
}, (res) => {
const headers = res.headers;
const result = {
status: res.statusCode,
headers: headers,
cacheControl: headers['cache-control'],
etag: headers['etag'],
lastModified: headers['last-modified'],
age: headers['age'],
fromCache: res.statusCode === 304 || !!headers['age']
};
resolve(result);
});
req.on('error', reject);
req.end();
});
}
async testScenario(url, scenarios) {
console.log(`\n测试URL: ${url}`);
console.log('='.repeat(50));
for (const scenario of scenarios) {
console.log(`\n场景: ${scenario.name}`);
console.log('-'.repeat(30));
const result = await this.validateCache(url, scenario.options);
console.log(`状态码: ${result.status}`);
console.log(`来自缓存: ${result.fromCache ? '是' : '否'}`);
console.log(`Cache-Control: ${result.cacheControl || '无'}`);
console.log(`ETag: ${result.etag || '无'}`);
console.log(`Age: ${result.age || '无'}`);
}
}
}
// 使用示例
const validator = new CacheValidator('https://example.com');
const scenarios = [
{
name: '首次请求',
options: { headers: {} }
},
{
name: '强缓存验证',
options: { headers: { 'Cache-Control': 'no-cache' } }
},
{
name: 'ETag验证',
options: { headers: { 'If-None-Match': '"abc123"' } }
},
{
name: 'Last-Modified验证',
options: { headers: { 'If-Modified-Since': 'Wed, 21 Oct 2025 07:28:00 GMT' } }
}
];
validator.testScenario('/static/app.js', scenarios);
六、常见问题与解决方案
6.1 缓存不一致问题
问题:更新资源后,用户仍然看到旧版本。
解决方案:
- 版本控制:使用文件名哈希
- 缓存清除:主动清除CDN和浏览器缓存
- 缓存验证:使用ETag确保最新版本
// 版本控制示例
const version = process.env.APP_VERSION || '1.0.0';
const cacheKey = `app-${version}`;
// 在构建时注入版本号
const webpackConfig = {
output: {
filename: `[name].${version}.[contenthash:8].js`
}
};
6.2 缓存穿透
问题:大量请求不存在的资源,导致每次都穿透到源服务器。
解决方案:
- 布隆过滤器:快速判断资源是否存在
- 空值缓存:缓存不存在的资源
- 请求合并:合并相同请求
// 空值缓存示例
const cache = new Map();
async function getResource(id) {
const cacheKey = `resource:${id}`;
// 检查缓存
if (cache.has(cacheKey)) {
const cached = cache.get(cacheKey);
if (cached === null) {
return null; // 明确返回null表示不存在
}
return cached;
}
// 查询数据库
const resource = await db.query('SELECT * FROM resources WHERE id = ?', [id]);
if (resource) {
cache.set(cacheKey, resource);
return resource;
} else {
// 缓存空值,设置较短过期时间
cache.set(cacheKey, null, 60000); // 60秒
return null;
}
}
6.3 缓存雪崩
问题:大量缓存同时过期,导致请求集中到源服务器。
解决方案:
- 随机过期时间:避免同时过期
- 缓存预热:提前加载热点数据
- 多级缓存:分散压力
// 随机过期时间
function getRandomTTL(baseTTL, variance = 0.2) {
const randomFactor = 1 + (Math.random() * 2 - 1) * variance;
return Math.floor(baseTTL * randomFactor);
}
// 缓存设置
function setCacheWithRandomTTL(key, data, baseTTL) {
const ttl = getRandomTTL(baseTTL);
cache.set(key, data, ttl);
return ttl;
}
6.4 缓存击穿
问题:热点数据过期瞬间,大量请求同时访问。
解决方案:
- 互斥锁:同一时间只有一个请求查询数据库
- 提前刷新:在过期前刷新缓存
- 永不过期:后台异步更新
// 互斥锁实现
const locks = new Map();
async function getHotData(id) {
const cacheKey = `hot:${id}`;
// 尝试获取缓存
const cached = await cache.get(cacheKey);
if (cached) return cached;
// 获取锁
const lockKey = `lock:${id}`;
if (locks.has(lockKey)) {
// 等待锁释放
await locks.get(lockKey);
return getHotData(id);
}
// 创建锁
const lock = new Promise((resolve) => {
setTimeout(async () => {
// 查询数据库
const data = await db.query('SELECT * FROM hot_data WHERE id = ?', [id]);
// 设置缓存(随机TTL避免雪崩)
const ttl = getRandomTTL(300); // 5分钟基础TTL
await cache.set(cacheKey, data, ttl);
// 释放锁
locks.delete(lockKey);
resolve(data);
}, 100);
});
locks.set(lockKey, lock);
return lock;
}
七、最佳实践总结
7.1 缓存策略选择指南
| 资源类型 | 推荐策略 | Cache-Control | 说明 |
|---|---|---|---|
| 静态资源 | 强缓存 | public, immutable, max-age=31536000 |
文件名带哈希,1年缓存 |
| API数据 | 协商缓存 | public, max-age=300, must-revalidate |
5分钟缓存,过期验证 |
| HTML页面 | 短缓存/无缓存 | no-cache, no-store |
确保获取最新版本 |
| 用户数据 | 私有缓存 | private, max-age=60 |
仅浏览器缓存,1分钟 |
| 实时数据 | 无缓存 | no-store |
禁止任何缓存 |
7.2 配置检查清单
静态资源
- [ ] 使用文件名哈希
- [ ] 设置长缓存时间(1年)
- [ ] 添加
immutable指令 - [ ] 启用Gzip压缩
API接口
- [ ] 实现ETag或Last-Modified
- [ ] 设置合理的max-age
- [ ] 对敏感数据使用
private - [ ] 考虑使用Redis缓存
HTML文档
- [ ] 避免长缓存
- [ ] 使用
no-cache或短时间缓存 - [ ] 确保版本一致性
CDN配置
- [ ] 配置缓存规则
- [ ] 设置缓存键
- [ ] 监控缓存命中率
- [ ] 配置缓存清除机制
7.3 性能优化建议
监控指标
- 缓存命中率(目标>90%)
- 平均响应时间
- 服务器负载
- 带宽使用量
优化方向
- 提高静态资源缓存命中率
- 优化API缓存策略
- 实现智能预热
- 建立缓存监控体系
持续改进
- 定期分析缓存日志
- 调整缓存策略
- 测试不同场景
- 收集用户反馈
八、总结
HTTP缓存是Web性能优化的基石,通过合理配置缓存策略,可以显著提升用户体验并降低服务器成本。本文从基础概念到高级实践,全面介绍了HTTP缓存的原理、配置方法和最佳实践。
关键要点:
- 理解缓存机制:掌握强缓存和协商缓存的工作原理
- 合理配置策略:根据资源类型选择合适的缓存策略
- 实施版本控制:使用文件名哈希确保资源更新
- 监控优化:持续监控缓存性能并优化策略
- 应对挑战:解决缓存穿透、雪崩、击穿等常见问题
通过本文的指导,您可以构建高效、可靠的HTTP缓存系统,为用户提供更快的Web体验。记住,缓存策略需要根据具体业务场景不断调整和优化,没有一成不变的完美方案。
