引言:HTTP缓存的重要性
HTTP缓存是Web性能优化的核心技术之一,它通过在客户端(浏览器)和中间代理服务器上存储资源副本,显著减少网络请求、降低服务器负载并提升用户体验。根据Google的研究,合理的缓存策略可以将页面加载时间减少50%以上。
缓存的核心价值
- 减少网络延迟:避免重复下载相同资源
- 降低服务器压力:减少服务器处理相同请求的次数
- 节省带宽成本:减少数据传输量
- 提升用户体验:更快的页面响应速度
HTTP缓存基础机制
缓存决策流程
当浏览器发起HTTP请求时,会按照以下流程判断是否使用缓存:
客户端请求 → 检查本地缓存 → 是否过期? → 未过期 → 直接使用缓存
↓
已过期 → 发送请求到服务器 → 服务器返回304? → 是 → 使用缓存
↓
否 → 下载新资源并更新缓存
缓存分类
- 强缓存:浏览器直接使用缓存,不与服务器通信
- 协商缓存:浏览器发送请求,服务器返回304状态码表示缓存有效
强缓存策略
强缓存通过Cache-Control和Expires头部控制,浏览器在缓存有效期内直接使用本地副本。
Cache-Control头部
Cache-Control是HTTP/1.1标准,提供更精细的控制:
Cache-Control: max-age=3600, public, immutable
常用指令详解:
max-age=seconds:缓存最大有效期(秒)public:允许任何缓存(包括CDN)private:仅允许用户浏览器缓存no-cache:注意:这不表示不缓存,而是必须重新验证no-store:完全不缓存immutable:资源在缓存期间不会改变(适用于版本化资源)
Expires头部
HTTP/1.0遗留头部,使用绝对时间:
Expires: Thu, 31 Dec 2025 23:59:59 GMT
优先级:Cache-Control > Expires
实战示例:静态资源缓存
对于版本化的静态资源(如app.v123.js),可以设置长期缓存:
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable
Content-Type: application/javascript
// 资源内容...
这样浏览器会在一年内直接使用缓存,无需任何验证。
协商缓存策略
当强缓存过期后,浏览器会与服务器进行协商,确认资源是否更新。
基于时间的协商:Last-Modified / If-Modified-Since
流程:
- 服务器返回资源时携带
Last-Modified头部 - 浏览器下次请求时携带
If-Modified-Since头部 - 服务器比较时间,未修改则返回304
# 首次响应
HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
Content-Type: text/html
# 后续请求
GET /index.html HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
# 服务器响应(未修改)
HTTP/1.1 304 Not Modified
局限性:
- 时间精度只有秒级
- 文件内容修改但时间戳未变不会触发更新
- 服务器时区问题可能导致误判
基于内容的协商:ETag / If-None-Match
流程:
- 服务器生成资源的唯一标识(ETag)
- 浏览器下次请求时携带
If-None-Match头部 - 服务器比较ETag,匹配则返回304
# 首次响应
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Content-Type: text/html
# 后续请求
GET /index.html HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# 服务器响应(未修改)
HTTP/1.1 304 Not Modified
ETag生成策略:
- 强ETag:完全字节匹配
- 弱ETag:语义等价(W/“33a64df551425fcc55e4d42a148795d9f25f89d4”)
缓存策略最佳实践
1. 静态资源(版本化)
策略:长期缓存 + 版本号/哈希值
// Webpack配置示例
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
}
}
HTTP响应头:
Cache-Control: public, max-age=31536000, immutable
原理:文件名变化强制浏览器重新下载,相同文件名可缓存一年。
2. 静态资源(非版本化)
策略:较短缓存 + 验证
Cache-Control: public, max-age=86400
ETag: "abc123"
3. HTML文档
策略:不缓存或极短缓存
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0
原因:HTML是入口文件,需要确保用户获取最新版本。
4. API响应
策略:根据业务场景选择
// 动态数据 - 不缓存
Cache-Control: no-cache, private
// 用户特定数据
Cache-Control: private, max-age=60
// 公共数据(如城市列表)
Cache-Control: public, max-age=3600
服务器端实现示例
Node.js + Express 实现
const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const app = express();
// 1. 静态资源(版本化)- 长期缓存
app.use('/static', express.static('public', {
maxAge: '1y',
etag: true,
lastModified: true,
setHeaders: (res, path) => {
if (path.endsWith('.js') || path.endsWith('.css')) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
}
}));
// 2. HTML - 不缓存
app.get('/', (req, res) => {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.sendFile(path.join(__dirname, 'index.html'));
});
// 3. API端点 - 条件缓存
app.get('/api/user/:id', (req, res) => {
const userId = req.params.id;
const userData = getUserData(userId); // 假设从数据库获取
// 生成ETag
const etag = crypto
.createHash('md5')
.update(JSON.stringify(userData))
.digest('hex');
// 检查客户端ETag
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // 未修改
}
res.set('ETag', etag);
res.set('Cache-Control', 'private, max-age=300'); // 5分钟
res.json(userData);
});
// 4. 处理POST请求 - 禁止缓存
app.post('/api/submit', (req, res) => {
res.set('Cache-Control', 'no-store');
// 处理业务逻辑...
res.json({ success: true });
});
app.listen(3000);
Nginx 配置示例
# 静态资源(版本化)- 长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
root /var/www/html;
expires 1y;
add_header Cache-Control "public, immutable";
# 开启ETag
etag on;
}
# HTML文件 - 不缓存
location ~* \.html$ {
root /var/www/html;
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# API代理
location /api/ {
proxy_pass http://backend;
proxy_cache_valid 200 5m; # 缓存5分钟
proxy_cache_key "$scheme$request_method$host$request_uri";
# 根据请求类型设置不同缓存
if ($request_method = POST) {
proxy_cache_valid any 0; # POST不缓存
}
}
Apache .htaccess 配置
# 静态资源长期缓存
<FilesMatch "\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
# HTML文件不缓存
<FilesMatch "\.html$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
</FilesMatch>
# API端点
<If "%{REQUEST_URI} =~ m#^/api/#">
Header set Cache-Control "private, max-age=300"
</If>
常见缓存难题及解决方案
难题1:缓存雪崩
问题:大量缓存同时过期,导致请求瞬间打到服务器。
解决方案:
// 1. 错开过期时间
function getRandomTTL(baseTTL = 3600, variance = 600) {
return baseTTL + Math.floor(Math.random() * variance);
}
// 2. 热点数据永不过期 + 后台更新
const热点数据 = new Map();
async function getHotData(key) {
if (热点数据.has(key)) {
return 热点数据.get(key);
}
// 异步更新,不阻塞读取
updateHotData(key).catch(console.error);
// 返回旧数据或默认值
return 热点数据.get(key) || null;
}
// 3. 多级缓存
async function getWithMultiCache(key) {
// L1: 本地内存缓存(秒级)
const local = localCache.get(key);
if (local) return local;
// L2: Redis缓存(分钟级)
const redisData = await redis.get(key);
if (redisData) {
localCache.set(key, redisData, 5); // 本地缓存5秒
return redisData;
}
// L3: 数据库
const dbData = await db.query(key);
await redis.setex(key, 300, dbData); // Redis缓存5分钟
localCache.set(key, dbData, 5);
return dbData;
}
难题2:缓存穿透
问题:查询不存在的数据,导致缓存无效,请求直接打到数据库。
解决方案:
// 1. 布隆过滤器(Bloom Filter)
const BloomFilter = require('bloom-filter');
const filter = BloomFilter.create(100000, 0.01); // 10万条目,1%误判率
function queryData(id) {
// 先检查布隆过滤器
if (!filter.contains(id)) {
// 确定不存在,返回空
return null;
}
// 可能存在,继续查缓存和数据库
return getFromCacheOrDB(id);
}
// 2. 缓存空值
async function queryWithNullCache(id) {
const cacheKey = `data:${id}`;
const cached = await redis.get(cacheKey);
if (cached !== null) {
return cached === 'NULL' ? null : JSON.parse(cached);
}
const data = await db.query('SELECT * FROM table WHERE id = ?', [id]);
if (!data) {
// 缓存空值,设置较短TTL
await redis.setex(cacheKey, 60, 'NULL');
return null;
}
await redis.setex(cacheKey, 300, JSON.stringify(data));
return data;
}
难题3:缓存击穿
问题:热点key过期瞬间,大量请求同时查询数据库。
解决方案:
// 1. 互斥锁(Mutex)
async function getHotKeyWithLock(key) {
const lockKey = `lock:${key}`;
const cacheKey = `data:${key}`;
// 尝试获取缓存
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// 尝试获取分布式锁
const lock = await redis.set(lockKey, '1', 'NX', 'EX', 10);
if (lock) {
try {
// 只有拿到锁的线程查询数据库
const data = await db.query(key);
await redis.setex(cacheKey, 300, JSON.stringify(data));
return data;
} finally {
await redis.del(lockKey);
}
} else {
// 未拿到锁,等待后重试
await sleep(50);
return getHotKeyWithLock(key);
}
}
// 2. 逻辑过期
async function getWithLogicalExpiration(key) {
const cacheKey = `data:${key}`;
const result = await redis.get(cacheKey);
if (!result) {
return await updateCache(key);
}
const { data, expireTime } = JSON.parse(result);
if (Date.now() > expireTime) {
// 异步更新,返回旧数据
updateCache(key).catch(console.error);
}
return data;
}
async function updateCache(key) {
const data = await db.query(key);
const expireTime = Date.now() + 300000; // 5分钟后过期
await redis.setex(key, 600, JSON.stringify({ data, expireTime }));
return data;
}
难题4:缓存与数据库一致性
问题:更新数据库后,缓存未更新导致数据不一致。
解决方案:
// 1. Cache Aside Pattern(旁路缓存)
// 读取:先缓存 → 后数据库
// 更新:先数据库 → 后缓存
async function updateData(id, newData) {
// 方案A:先更新数据库,再删除缓存(推荐)
await db.update(id, newData);
await redis.del(`data:${id}`);
// 方案B:延迟双删(解决主从延迟)
await db.update(id, newData);
await redis.del(`data:${id}`);
setTimeout(async () => {
await redis.del(`data:${id}`);
}, 500);
}
// 2. Write Through Pattern(写穿)
async function writeThrough(id, data) {
// 同时更新缓存和数据库
await Promise.all([
db.update(id, data),
redis.setex(`data:${id}`, 300, JSON.stringify(data))
]);
}
// 3. 基于Binlog的最终一致性
// 使用Canal等工具监听数据库变更,自动更新缓存
const canal = require('canal-node');
canal.on('UPDATE', (event) => {
const { table, data } = event;
const key = `data:${data.id}`;
redis.del(key); // 或重新查询更新
});
难题5:移动端缓存控制
问题:移动端网络环境复杂,缓存策略难以统一。
解决方案:
// 1. 按网络类型调整缓存
function getCacheStrategy(networkType) {
switch (networkType) {
case '4G':
return 'max-age=300'; // 5分钟
case '3G':
return 'max-age=600'; // 10分钟
case '2G':
return 'max-age=1800'; // 30分钟
case 'wifi':
return 'max-age=60'; // 1分钟
default:
return 'no-cache';
}
}
// 2. 按设备存储调整
async function getDeviceAwareCache(key, deviceInfo) {
const { storage, platform } = deviceInfo;
if (storage < 100) {
// 存储空间不足,不缓存或短缓存
return await fetchFromServer(key);
}
if (platform === 'iOS') {
// iOS可能有更严格的缓存限制
return await getWithShortTTL(key);
}
return await getWithStandardCache(key);
}
高级缓存技术
1. Service Worker缓存
// service-worker.js
const CACHE_NAME = 'app-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
];
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;
});
})
);
});
2. CDN缓存策略
# Cloudflare Workers 示例
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
// 根据路径设置不同缓存
if (url.pathname.startsWith('/api/')) {
// API响应 - 短缓存
const response = await fetch(request);
response.headers.set('Cache-Control', 'private, max-age=60');
return response;
} else if (url.pathname.match(/\.(js|css|png)$/)) {
// 静态资源 - 长缓存
const response = await fetch(request);
response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
return response;
}
return fetch(request);
}
缓存监控与调试
1. 浏览器开发者工具
- Network面板:查看请求的
Size列(来自缓存/来自服务器) - Application面板:查看Cache Storage、Local Storage等
- Coverage面板:分析未使用的缓存资源
2. 服务器日志分析
// Node.js中间件记录缓存命中率
function cacheMetricsMiddleware(req, res, next) {
const originalSetHeader = res.setHeader;
res.setHeader = function(name, value) {
if (name.toLowerCase() === 'x-cache-status') {
// 记录缓存状态
console.log(`[${new Date().toISOString()}] ${req.url} - ${value}`);
}
return originalSetHeader.call(this, name, value);
};
next();
}
// 使用
app.use(cacheMetricsMiddleware);
3. 缓存命中率监控
// Redis监控脚本
async function monitorCacheHitRate() {
const stats = {
hits: 0,
misses: 0,
get total() { return this.hits + this.misses; },
get rate() { return this.total > 0 ? (this.hits / this.total * 100).toFixed(2) + '%' : '0%'; }
};
// 模拟监控
setInterval(() => {
console.log(`缓存命中率: ${stats.rate} (${stats.hits}/${stats.total})`);
}, 60000);
return stats;
}
总结与建议
核心原则
- 分层缓存:浏览器 → CDN → 应用服务器 → 数据库
- 差异化策略:根据资源类型、用户、场景制定不同策略
- 监控驱动:持续监控缓存命中率,优化策略
- 一致性优先:在性能与一致性之间找到平衡点
推荐工具
- Redis:分布式缓存
- Memcached:内存缓存
- Varnish:HTTP缓存加速器
- Canal:数据库Binlog监听
- Webpack:资源版本化
检查清单
- [ ] 静态资源是否使用版本化文件名?
- [ ] HTML文档是否设置
no-cache? - [ ] API响应是否根据业务场景设置合理TTL?
- [ ] 是否实现缓存穿透/击穿/雪崩防护?
- [ ] 是否有缓存命中率监控?
- [ ] 是否测试过不同网络环境下的缓存行为?
通过合理运用HTTP缓存策略,可以显著提升网站性能,解决常见的缓存难题。记住,没有银弹,最佳策略需要根据具体业务场景持续调整和优化。
