缓存策略
缓存策略的基本概念
缓存策略是提升应用性能的关键手段之一,通过将频繁访问的数据存储在高速存储介质中,减少对慢速数据源(如数据库或远程API)的依赖。在Node.js中,缓存可以显著降低I/O操作的开销,特别是在处理高并发请求时。
常见的缓存类型包括:
- 内存缓存(如Node.js的
Map
对象或专用库) - 分布式缓存(如Redis)
- 浏览器缓存(通过HTTP头控制)
- CDN缓存(针对静态资源)
内存缓存实现
Node.js中最简单的缓存实现是使用内存存储。以下是一个基于Map
的基础缓存示例:
class SimpleCache {
constructor() {
this.cache = new Map();
this.ttl = 60000; // 默认1分钟过期时间
}
set(key, value, ttl = this.ttl) {
const expiresAt = Date.now() + ttl;
this.cache.set(key, { value, expiresAt });
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expiresAt) {
this.cache.delete(key);
return null;
}
return item.value;
}
delete(key) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
}
使用示例:
const cache = new SimpleCache();
cache.set('user:123', { name: 'Alice', role: 'admin' });
// 5秒后获取
setTimeout(() => {
console.log(cache.get('user:123')); // 返回缓存对象
}, 5000);
// 2分钟后获取
setTimeout(() => {
console.log(cache.get('user:123')); // 返回null,已过期
}, 120000);
缓存失效策略
合理的缓存失效机制对保证数据一致性至关重要。常见策略包括:
-
时间过期(TTL)
- 设置固定过期时间
- 适合变化不频繁的数据
-
写时失效
- 数据变更时主动清除缓存
- 适合需要强一致性的场景
-
LRU(最近最少使用)
- 当缓存达到上限时,淘汰最久未使用的条目
- Node.js实现示例:
class LRUCache {
constructor(capacity = 100) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return null;
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// 删除最久未使用的
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
}
多级缓存架构
对于高性能应用,可以采用多级缓存策略:
graph TD
A[客户端] -->|请求| B[浏览器缓存]
B -->|未命中| C[CDN缓存]
C -->|未命中| D[应用内存缓存]
D -->|未命中| E[Redis缓存]
E -->|未命中| F[数据库]
Node.js中实现多级缓存的代码结构:
async function getWithMultiLevelCache(key) {
// 1. 检查内存缓存
let data = memoryCache.get(key);
if (data) return data;
// 2. 检查Redis缓存
data = await redisClient.get(key);
if (data) {
memoryCache.set(key, data); // 回填内存缓存
return data;
}
// 3. 查询数据库
data = await db.query('SELECT * FROM data WHERE key = ?', [key]);
// 写入各级缓存
await redisClient.setex(key, 3600, data); // Redis缓存1小时
memoryCache.set(key, data); // 内存缓存
return data;
}
缓存击穿与雪崩防护
缓存击穿解决方案
当热点key过期时,大量请求直接打到数据库:
async function getProduct(id) {
const cacheKey = `product:${id}`;
let product = await redis.get(cacheKey);
if (!product) {
// 使用互斥锁防止并发重建
const lockKey = `lock:${cacheKey}`;
const lock = await redis.set(lockKey, '1', 'EX', 10, 'NX');
if (lock) {
try {
product = await db.getProduct(id);
await redis.setex(cacheKey, 3600, product);
} finally {
await redis.del(lockKey);
}
} else {
// 等待其他进程重建缓存
await new Promise(resolve => setTimeout(resolve, 100));
return getProduct(id); // 重试
}
}
return product;
}
缓存雪崩防护
大量key同时过期导致数据库压力激增:
- 随机化过期时间:
function setWithRandomTtl(key, value, baseTtl) {
const ttl = baseTtl + Math.floor(Math.random() * 60 * 1000); // 添加随机0-60秒
redis.setex(key, ttl, value);
}
- 永不过期+后台更新:
// 设置永不过期的key
redis.set('hot:data', data);
// 后台定时更新
setInterval(async () => {
const newData = await fetchLatestData();
redis.set('hot:data', newData);
}, 5 * 60 * 1000); // 每5分钟更新
HTTP缓存策略
Node.js中实现HTTP缓存头控制:
const express = require('express');
const app = express();
// 静态资源缓存
app.use('/static', express.static('public', {
maxAge: '1y', // 浏览器缓存1年
immutable: true // 内容不变
}));
// API响应缓存
app.get('/api/data', async (req, res) => {
const data = await getData();
// 设置缓存头
res.set({
'Cache-Control': 'public, max-age=300', // 5分钟
'ETag': generateETag(data) // 基于内容生成ETag
});
res.json(data);
});
// 条件GET请求处理
app.get('/api/data', (req, res) => {
const ifNoneMatch = req.get('If-None-Match');
const currentETag = generateETag(data);
if (ifNoneMatch === currentETag) {
return res.status(304).end(); // 未修改
}
// ...返回新数据
});
缓存监控与调优
有效的缓存系统需要持续监控:
-
关键指标监控:
- 缓存命中率
- 平均响应时间
- 内存使用情况
-
Node.js实现简单监控:
class MonitoredCache extends SimpleCache {
constructor() {
super();
this.stats = {
hits: 0,
misses: 0,
size: 0
};
}
get(key) {
const value = super.get(key);
if (value) {
this.stats.hits++;
} else {
this.stats.misses++;
}
this.stats.size = this.cache.size;
return value;
}
getHitRate() {
const total = this.stats.hits + this.stats.misses;
return total > 0 ? (this.stats.hits / total) : 0;
}
}
- 使用Redis监控命令:
# 查看内存使用
redis-cli info memory
# 查看键空间统计
redis-cli info keyspace
# 监控命中率
redis-cli info stats | grep -E '(keyspace_hits|keyspace_misses)'
缓存模式实践
写穿透模式
async function writeThrough(key, value) {
// 1. 先更新数据库
await db.update(key, value);
// 2. 更新缓存
await cache.set(key, value);
}
写回模式
async function writeBack(key, value) {
// 1. 只更新缓存
await cache.set(key, value);
// 2. 异步批量更新数据库
if (!this.batchUpdate) {
this.batchUpdate = setTimeout(async () => {
const dirtyKeys = cache.getDirtyKeys();
await db.bulkUpdate(dirtyKeys);
this.batchUpdate = null;
}, 5000); // 每5秒批量更新
}
}
缓存预热
启动时加载热点数据:
async function warmUpCache() {
const hotProducts = await db.query(`
SELECT * FROM products
WHERE views > 1000
ORDER BY views DESC
LIMIT 100
`);
await Promise.all(
hotProducts.map(p =>
redis.setex(`product:${p.id}`, 86400, JSON.stringify(p))
)
);
}
// 服务启动时
app.listen(3000, () => {
warmUpCache().then(() => {
console.log('缓存预热完成');
});
});
特殊场景处理
分页查询缓存
async function getProductsPage(page, size) {
const cacheKey = `products:page:${page}:size:${size}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const products = await db.query(
'SELECT * FROM products LIMIT ? OFFSET ?',
[size, (page - 1) * size]
);
// 设置较短过期时间,因为分页数据变化较快
await redis.setex(cacheKey, 300, JSON.stringify(products));
return products;
}
关联数据缓存
处理关联数据时,可以采用延迟加载策略:
async function getUserWithPosts(userId) {
const userKey = `user:${userId}`;
const postsKey = `user:${userId}:posts`;
const [user, posts] = await Promise.all([
redis.get(userKey),
redis.get(postsKey)
]);
if (!user) {
const userData = await db.getUser(userId);
await redis.setex(userKey, 3600, JSON.stringify(userData));
user = userData;
} else {
user = JSON.parse(user);
}
if (!posts) {
const postsData = await db.getUserPosts(userId);
await redis.setex(postsKey, 1800, JSON.stringify(postsData));
posts = postsData;
} else {
posts = JSON.parse(posts);
}
return { ...user, posts };
}
性能优化技巧
- 批量操作:
// 批量获取缓存
async function batchGet(keys) {
if (redis) {
return redis.mget(keys);
}
// 内存缓存实现
return keys.map(key => this.cache.get(key));
}
- 压缩缓存数据:
const zlib = require('zlib');
async function setCompressed(key, value) {
const compressed = await new Promise((resolve, reject) => {
zlib.deflate(JSON.stringify(value), (err, buffer) => {
err ? reject(err) : resolve(buffer);
});
});
await redis.set(key, compressed);
}
async function getCompressed(key) {
const compressed = await redis.getBuffer(key);
return new Promise((resolve, reject) => {
zlib.inflate(compressed, (err, buffer) => {
if (err) return reject(err);
resolve(JSON.parse(buffer.toString()));
});
});
}
- 缓存分区:
// 按业务分片
const SHARD_COUNT = 16;
function getShard(key) {
const hash = simpleHash(key);
return `cache:shard:${hash % SHARD_COUNT}`;
}
function simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0; // 转换为32位整数
}
return Math.abs(hash);
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn