您现在的位置是:网站首页 > 缓存策略与实现方案文章详情
缓存策略与实现方案
陈川
【
Node.js
】
41199人已围观
8291字
缓存策略的基本概念
缓存是提升应用性能的关键手段之一,通过将频繁访问的数据存储在快速访问的介质中,减少对慢速存储(如数据库)的依赖。在Express框架中,缓存策略的选择直接影响应用的响应速度和资源利用率。常见的缓存类型包括内存缓存、分布式缓存和浏览器缓存。
内存缓存将数据存储在应用进程的内存中,访问速度最快但容量有限。Redis等分布式缓存解决了单机内存缓存的容量和持久性问题。浏览器缓存则利用HTTP缓存头控制客户端资源的缓存行为。
// 简单的内存缓存实现示例
const cache = {};
function getFromCache(key) {
return cache[key] || null;
}
function setToCache(key, value, ttl = 60) {
cache[key] = value;
setTimeout(() => delete cache[key], ttl * 1000);
}
Express中的内存缓存实现
在Express应用中实现内存缓存可以直接使用Node.js的内存对象。对于小型应用,这种方案简单有效。关键要考虑缓存淘汰策略,常见的有FIFO(先进先出)、LRU(最近最少使用)和TTL(生存时间)。
const express = require('express');
const app = express();
const cache = {};
app.get('/api/data', (req, res) => {
const cacheKey = req.originalUrl;
// 检查缓存
if (cache[cacheKey] && cache[cacheKey].expiry > Date.now()) {
return res.json(cache[cacheKey].data);
}
// 模拟数据库查询
const data = { id: 1, content: '缓存数据' };
// 设置缓存(TTL 60秒)
cache[cacheKey] = {
data,
expiry: Date.now() + 60000
};
res.json(data);
});
更复杂的场景可以使用node-cache
或memory-cache
这类专门的内存缓存库:
const NodeCache = require('node-cache');
const myCache = new NodeCache({ stdTTL: 60, checkperiod: 120 });
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
const cacheKey = `user_${userId}`;
let user = myCache.get(cacheKey);
if (user) {
return res.json(user);
}
user = await UserModel.findById(userId);
myCache.set(cacheKey, user);
res.json(user);
});
Redis缓存集成
对于需要跨进程或跨服务器共享缓存的场景,Redis是最常用的解决方案。在Express中集成Redis可以通过ioredis
或redis
客户端库实现。
const Redis = require('ioredis');
const redis = new Redis();
app.get('/api/products', async (req, res) => {
const cacheKey = 'all_products';
try {
const cachedData = await redis.get(cacheKey);
if (cachedData) {
return res.json(JSON.parse(cachedData));
}
const products = await ProductModel.find();
await redis.setex(cacheKey, 3600, JSON.stringify(products)); // 缓存1小时
res.json(products);
} catch (err) {
console.error('Redis error:', err);
// 降级处理:直接查询数据库
const products = await ProductModel.find();
res.json(products);
}
});
对于更复杂的缓存策略,可以使用Redis的哈希类型存储结构化数据:
app.get('/api/user/:id/profile', async (req, res) => {
const userId = req.params.id;
const cacheKey = `user:${userId}`;
const cachedProfile = await redis.hgetall(cacheKey);
if (cachedProfile && Object.keys(cachedProfile).length > 0) {
return res.json(cachedProfile);
}
const profile = await UserProfileModel.findOne({ userId });
if (profile) {
await redis.hmset(cacheKey, profile.toObject());
await redis.expire(cacheKey, 1800); // 30分钟过期
}
res.json(profile || {});
});
HTTP缓存头设置
Express可以方便地设置HTTP缓存头,控制浏览器和CDN的缓存行为。常见的缓存头包括Cache-Control
、ETag
和Last-Modified
。
app.get('/static/js/app.js', (req, res) => {
const filePath = path.join(__dirname, 'public/js/app.js');
const fileStats = fs.statSync(filePath);
// 设置强缓存(1年)
res.set('Cache-Control', 'public, max-age=31536000');
// 设置弱缓存验证头
res.set('ETag', fileStats.mtimeMs.toString());
res.set('Last-Modified', fileStats.mtime.toUTCString());
res.sendFile(filePath);
});
app.get('/api/dynamic-data', (req, res) => {
// 禁止缓存动态数据
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
res.set('Pragma', 'no-cache');
res.set('Expires', '0');
res.json({ data: '实时数据', timestamp: Date.now() });
});
对于条件请求(If-None-Match/If-Modified-Since)的处理:
app.get('/api/articles/:id', async (req, res) => {
const article = await ArticleModel.findById(req.params.id);
if (!article) return res.status(404).send();
const etag = crypto.createHash('md5').update(JSON.stringify(article)).digest('hex');
res.set('ETag', etag);
// 检查客户端缓存是否有效
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.json(article);
});
缓存失效策略
合理的缓存失效机制是保证数据一致性的关键。常见的失效策略包括:
- 时间失效(TTL):设置固定过期时间
- 事件驱动失效:数据变更时主动清除相关缓存
- 写穿透:更新数据时同步更新缓存
- 延迟加载:仅在读取时填充缓存
// 事件驱动缓存失效示例
app.post('/api/articles', async (req, res) => {
const newArticle = await ArticleModel.create(req.body);
// 清除相关缓存
await redis.del('all_articles');
await redis.del(`article_${newArticle.id}`);
// 发布缓存失效事件
eventEmitter.emit('article:created', newArticle.id);
res.status(201).json(newArticle);
});
// 写穿透示例
app.put('/api/users/:id', async (req, res) => {
const userId = req.params.id;
const updatedUser = await UserModel.findByIdAndUpdate(userId, req.body, { new: true });
// 同步更新缓存
await redis.setex(`user_${userId}`, 3600, JSON.stringify(updatedUser));
res.json(updatedUser);
});
对于复杂的关联数据,可以使用通配符批量删除缓存:
// 删除用户所有相关缓存
async function invalidateUserCache(userId) {
const keys = await redis.keys(`user:${userId}:*`);
if (keys.length > 0) {
await redis.del(keys);
}
}
多级缓存架构
在高并发场景下,可以采用多级缓存架构:
- 浏览器缓存:通过HTTP头控制
- CDN缓存:缓存静态资源和API响应
- 应用内存缓存:快速响应热点数据
- 分布式缓存:共享数据,如Redis
- 数据库缓存:如MySQL查询缓存
// 多级缓存实现示例
async function getProductWithMultiCache(productId) {
// 1. 检查内存缓存
const memoryCacheKey = `product_${productId}`;
const memoryCached = myCache.get(memoryCacheKey);
if (memoryCached) return memoryCached;
// 2. 检查Redis缓存
const redisCacheKey = `product:${productId}`;
const redisCached = await redis.get(redisCacheKey);
if (redisCached) {
const product = JSON.parse(redisCached);
myCache.set(memoryCacheKey, product); // 回填内存缓存
return product;
}
// 3. 查询数据库
const product = await ProductModel.findById(productId);
if (product) {
await redis.setex(redisCacheKey, 3600, JSON.stringify(product));
myCache.set(memoryCacheKey, product);
}
return product;
}
缓存击穿与雪崩防护
缓存失效可能导致严重性能问题:
- 缓存击穿:热点key过期瞬间大量请求直达数据库
- 缓存雪崩:大量key同时过期导致数据库压力激增
解决方案:
// 使用互斥锁防止缓存击穿
async function getProductSafe(productId) {
const cacheKey = `product:${productId}`;
const lockKey = `lock:${cacheKey}`;
let data = await redis.get(cacheKey);
if (data) return JSON.parse(data);
// 获取分布式锁
const locked = await redis.set(lockKey, '1', 'EX', 10, 'NX');
if (!locked) {
// 未获取到锁,短暂等待后重试
await new Promise(resolve => setTimeout(resolve, 100));
return getProductSafe(productId);
}
try {
// 再次检查缓存(可能在等待期间已被其他进程填充)
data = await redis.get(cacheKey);
if (data) return JSON.parse(data);
// 查询数据库
const product = await ProductModel.findById(productId);
if (product) {
await redis.setex(cacheKey, 3600, JSON.stringify(product));
}
return product;
} finally {
// 释放锁
await redis.del(lockKey);
}
}
防止缓存雪崩可以通过随机过期时间:
// 设置缓存时添加随机抖动
function setCacheWithJitter(key, value, baseTTL = 3600) {
const jitter = Math.floor(Math.random() * 600); // 0-10分钟随机抖动
const actualTTL = baseTTL + jitter;
return redis.setex(key, actualTTL, JSON.stringify(value));
}
缓存监控与调优
有效的缓存系统需要持续监控和调优:
- 命中率监控:记录缓存命中/未命中次数
- 内存使用监控:防止内存溢出
- 性能指标:缓存操作耗时
// 简单的缓存统计中间件
const cacheStats = {
hits: 0,
misses: 0,
errors: 0
};
app.use((req, res, next) => {
const start = Date.now();
const originalJson = res.json;
res.json = function(body) {
const duration = Date.now() - start;
if (req.cacheHit) {
cacheStats.hits++;
} else if (req.cacheMiss) {
cacheStats.misses++;
}
// 记录慢查询
if (duration > 500) {
console.warn(`Slow response: ${req.method} ${req.url} - ${duration}ms`);
}
originalJson.call(this, body);
};
next();
});
// 暴露统计端点
app.get('/cache-stats', (req, res) => {
res.json({
...cacheStats,
hitRate: cacheStats.hits / (cacheStats.hits + cacheStats.misses) || 0
});
});
对于Redis可以使用INFO命令获取详细指标:
app.get('/redis-stats', async (req, res) => {
const info = await redis.info();
const stats = {
used_memory: info.match(/used_memory:\d+/)[0].split(':')[1],
hits: info.match(/keyspace_hits:\d+/)[0].split(':')[1],
misses: info.match(/keyspace_misses:\d+/)[0].split(':')[1]
};
res.json(stats);
});