为什么需要缓存?
缓存是提升系统性能的重要手段。通过缓存热点数据,可以减少数据库压力,降低响应时间。Redis 作为高性能的内存数据库,是最常用的缓存方案之一。
常见缓存策略
1. Cache Aside(旁路缓存)
最常用的缓存模式:
async function getUser(id) {
// 1. 先查缓存
const cached = await redis.get(`user:${id}`);
if (cached) {
return JSON.parse(cached);
}
// 2. 缓存未命中,查数据库
const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
// 3. 写入缓存
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
return user;
}2. Read/Write Through
应用只与缓存交互,缓存负责与数据库同步:
class CacheThrough {
async get(key) {
const value = await redis.get(key);
if (!value) {
const dbValue = await db.get(key);
await redis.set(key, dbValue);
return dbValue;
}
return value;
}
async set(key, value) {
await redis.set(key, value);
await db.set(key, value);
}
}3. Write Behind(异步写入)
先更新缓存,异步更新数据库:
async function updateUser(id, data) {
// 1. 立即更新缓存
await redis.set(`user:${id}`, JSON.stringify(data));
// 2. 异步更新数据库
setTimeout(async () => {
await db.update('users', { id }, data);
}, 0);
}缓存三大问题
1. 缓存穿透
**问题**:查询一个不存在的数据,缓存未命中,每次都要查数据库。
**解决方案**:
async function getUser(id) {
const cached = await redis.get(`user:${id}`);
// 缓存空值,防止穿透
if (cached === 'NULL') {
return null;
}
if (cached) {
return JSON.parse(cached);
}
const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
if (!user) {
// 缓存空值,设置较短的过期时间
await redis.set(`user:${id}`, 'NULL', 'EX', 60);
return null;
}
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
return user;
}2. 缓存雪崩
**问题**:大量缓存同时失效,导致数据库压力骤增。
**解决方案**:
// 1. 过期时间加随机值
const ttl = 3600 + Math.floor(Math.random() * 300);
await redis.set(key, value, 'EX', ttl);
// 2. 使用永不过期 + 主动更新
async function getWithRefresh(key, fetchFn) {
const cached = await redis.get(key);
if (cached) {
// 检查是否需要刷新
const refreshTime = await redis.get(`${key}:refresh`);
if (!refreshTime || Date.now() > parseInt(refreshTime)) {
// 异步刷新
refreshCache(key, fetchFn);
}
return JSON.parse(cached);
}
return refreshCache(key, fetchFn);
}
async function refreshCache(key, fetchFn) {
const value = await fetchFn();
await redis.set(key, JSON.stringify(value));
await redis.set(`${key}:refresh`, Date.now() + 3600 * 1000);
return value;
}3. 缓存击穿
**问题**:热点 key 失效瞬间,大量请求打到数据库。
**解决方案**:
// 使用互斥锁
async function getUserWithLock(id) {
const cached = await redis.get(`user:${id}`);
if (cached) {
return JSON.parse(cached);
}
// 尝试获取锁
const lockKey = `lock:user:${id}`;
const locked = await redis.set(lockKey, '1', 'EX', 10, 'NX');
if (locked) {
try {
// 再次检查缓存(可能已被其他线程更新)
const doubleCheck = await redis.get(`user:${id}`);
if (doubleCheck) {
return JSON.parse(doubleCheck);
}
// 查数据库
const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
return user;
} finally {
await redis.del(lockKey);
}
} else {
// 等待其他线程更新缓存
await new Promise(resolve => setTimeout(resolve, 100));
return getUserWithLock(id);
}
}缓存更新策略
1. 主动更新
数据变更时立即更新缓存:
async function updateUser(id, data) {
await db.update('users', { id }, data);
await redis.del(`user:${id}`);
}2. 延迟双删
async function updateUser(id, data) {
// 1. 先删除缓存
await redis.del(`user:${id}`);
// 2. 更新数据库
await db.update('users', { id }, data);
// 3. 延迟再次删除缓存
setTimeout(async () => {
await redis.del(`user:${id}`);
}, 500);
}监控与告警
// 监控缓存命中率
const hits = await redis.info('keyspace_hits');
const misses = await redis.info('keyspace_misses');
const hitRate = hits / (hits + misses);
if (hitRate < 0.8) {
// 发送告警
alert('缓存命中率过低');
}总结
Redis 缓存的关键要点:
记住,缓存不是万能的,需要根据实际业务场景合理使用。