缓存策略
欢迎来到缓存策略知识库!
💾 缓存简介
缓存是将频繁访问的数据存储在更快的存储介质中,以提高系统性能、减少数据库压力、降低响应时间的技术手段。
🎯 为什么需要缓存
性能对比
| 存储层级 | 访问时间 | 容量 |
|---|---|---|
| CPU L1 缓存 | 0.5 ns | KB 级 |
| CPU L2 缓存 | 7 ns | MB 级 |
| 内存 | 100 ns | GB 级 |
| SSD | 150 μs | TB 级 |
| 机械硬盘 | 10 ms | TB 级 |
| 网络 | 150 ms | - |
Redis (内存) vs MySQL (磁盘):
- Redis: 10w+ QPS
- MySQL: 1k-2k QPS
缓存的好处
- 提高性能 - 内存访问速度远超磁盘
- 降低数据库负载 - 减少数据库查询
- 提升用户体验 - 快速响应
- 节省成本 - 减少服务器资源消耗
🎯 缓存类型
1. 本地缓存
存储在应用进程内存中。
优点:
- 访问速度最快
- 无网络开销
- 实现简单
缺点:
- 不能跨服务器共享
- 内存受限
- 缓存更新复杂
适用场景:
- 配置信息
- 字典数据
- 不常变化的数据
实现:
javascript
// Node.js - Memory Cache
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 });
// 设置缓存
cache.set('key', 'value', 10000);
// 获取缓存
const value = cache.get('key');
// 删除缓存
cache.del('key');2. 分布式缓存
存储在独立的缓存服务器。
优点:
- 多服务器共享
- 容量大
- 可扩展
缺点:
- 网络开销
- 维护成本高
适用场景:
- 用户会话
- 热点数据
- 分布式系统
实现:
- Redis
- Memcached
3. CDN 缓存
内容分发网络,缓存静态资源。
优点:
- 就近访问
- 减轻源站压力
- 加速访问
适用场景:
- 图片、视频
- JS、CSS 文件
- 静态 HTML
4. 浏览器缓存
存储在浏览器中。
实现:
- HTTP 缓存头 (Cache-Control)
- LocalStorage / SessionStorage
📋 缓存读写策略
1. Cache-Aside (旁路缓存)
最常用的策略
读取流程
1. 查询缓存
├─ 命中 -> 返回数据
└─ 未命中 -> 2. 查询数据库
3. 写入缓存
4. 返回数据更新流程
1. 更新数据库
2. 删除缓存 (而不是更新缓存)为什么删除而不是更新?
- 避免并发问题
- 减少无效更新(如果更新后没有读取)
代码示例
javascript
// 读取
async function getUser(userId) {
// 1. 查询缓存
let user = await redis.get(`user:${userId}`);
if (user) {
return JSON.parse(user);
}
// 2. 查询数据库
user = await db.users.findById(userId);
if (user) {
// 3. 写入缓存
await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
}
return user;
}
// 更新
async function updateUser(userId, data) {
// 1. 更新数据库
await db.users.update(userId, data);
// 2. 删除缓存
await redis.del(`user:${userId}`);
}2. Read-Through (读穿透)
缓存层负责读取数据库。
应用 -> 缓存
└─> 数据库 (缓存未命中时)javascript
class CacheService {
async get(key, loader) {
let value = await redis.get(key);
if (!value) {
value = await loader(); // 加载器函数
await redis.setex(key, 3600, value);
}
return value;
}
}
// 使用
const user = await cacheService.get(`user:${userId}`, () => {
return db.users.findById(userId);
});3. Write-Through (写穿透)
先写缓存,缓存再写数据库。
应用 -> 缓存 -> 数据库优点: 数据一致性好 缺点: 写入慢
4. Write-Behind (写回)
先写缓存,异步写数据库。
应用 -> 缓存
└─> 异步 -> 数据库优点: 写入快 缺点: 可能丢失数据
🚨 缓存问题及解决方案
1. 缓存穿透
问题: 查询不存在的数据,缓存和数据库都没有。
大量请求 key=999999
├─> 缓存未命中
└─> 数据库查询(无数据)
└─> 每次都查数据库 ❌解决方案:
方案一:缓存空值
javascript
async function getUser(userId) {
let user = await redis.get(`user:${userId}`);
if (user === 'null') {
return null; // 缓存的空值
}
if (!user) {
user = await db.users.findById(userId);
if (user) {
await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
} else {
// 缓存空值,设置较短过期时间
await redis.setex(`user:${userId}`, 60, 'null');
}
}
return user === 'null' ? null : JSON.parse(user);
}方案二:布隆过滤器
javascript
const BloomFilter = require('bloom-filters').BloomFilter;
// 初始化布隆过滤器
const bf = BloomFilter.create(10000, 0.01);
// 将所有存在的 ID 加入过滤器
users.forEach(user => bf.add(user.id.toString()));
async function getUser(userId) {
// 先用布隆过滤器判断
if (!bf.has(userId.toString())) {
return null; // 一定不存在
}
// 可能存在,继续查询缓存和数据库
// ...
}2. 缓存击穿
问题: 热点数据过期,大量请求同时打到数据库。
热点 key 过期
└─> 大量请求同时查询数据库 ❌解决方案:
方案一:互斥锁
javascript
async function getUser(userId) {
let user = await redis.get(`user:${userId}`);
if (!user) {
const lockKey = `lock:user:${userId}`;
// 尝试获取锁
const acquired = await redis.set(lockKey, '1', 'EX', 10, 'NX');
if (acquired) {
try {
// 获取到锁,查询数据库
user = await db.users.findById(userId);
if (user) {
await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
}
} finally {
// 释放锁
await redis.del(lockKey);
}
} else {
// 未获取到锁,等待后重试
await sleep(50);
return getUser(userId);
}
}
return user ? JSON.parse(user) : null;
}方案二:永不过期
javascript
// 逻辑过期
const data = {
value: user,
expireTime: Date.now() + 3600000 // 1小时后
};
await redis.set(`user:${userId}`, JSON.stringify(data));
// 读取时判断
async function getUser(userId) {
const dataStr = await redis.get(`user:${userId}`);
if (!dataStr) {
// 缓存miss,查询数据库
return loadFromDB(userId);
}
const data = JSON.parse(dataStr);
if (Date.now() > data.expireTime) {
// 逻辑过期,异步刷新
refreshCache(userId); // 异步执行,不阻塞
}
return data.value;
}3. 缓存雪崩
问题: 大量缓存同时过期,请求全部打到数据库。
大量 key 同时过期 (如都是 12:00 过期)
└─> 数据库压力激增 ❌解决方案:
方案一:过期时间随机化
javascript
// 避免同时过期
const baseExpire = 3600;
const randomExpire = Math.floor(Math.random() * 300); // 0-5分钟
const expire = baseExpire + randomExpire;
await redis.setex(`user:${userId}`, expire, JSON.stringify(user));方案二:多级缓存
javascript
// L1: 本地缓存 (1分钟)
// L2: Redis (1小时)
// L3: 数据库
async function getUser(userId) {
// 1. 查询本地缓存
let user = localCache.get(`user:${userId}`);
if (user) return user;
// 2. 查询 Redis
user = await redis.get(`user:${userId}`);
if (user) {
localCache.set(`user:${userId}`, user, 60);
return JSON.parse(user);
}
// 3. 查询数据库
user = await db.users.findById(userId);
if (user) {
await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
localCache.set(`user:${userId}`, user, 60);
}
return user;
}方案三:Redis 集群 + 持久化
- 主从复制 + 哨兵
- Redis 集群
- 开启持久化(RDB + AOF)
4. 缓存一致性
问题: 缓存和数据库数据不一致。
解决方案:
方案一:先更新数据库,再删除缓存
javascript
async function updateUser(userId, data) {
// 1. 更新数据库
await db.users.update(userId, data);
// 2. 删除缓存
await redis.del(`user:${userId}`);
}方案二:延迟双删
javascript
async function updateUser(userId, data) {
// 1. 删除缓存
await redis.del(`user:${userId}`);
// 2. 更新数据库
await db.users.update(userId, data);
// 3. 延迟后再删除缓存
setTimeout(async () => {
await redis.del(`user:${userId}`);
}, 1000);
}方案三:Canal 监听 binlog
MySQL binlog -> Canal -> 删除缓存💡 最佳实践
1. 缓存设计
javascript
// ✅ 好的缓存键设计
const key = `user:${userId}:profile`;
const key = `article:${articleId}:views`;
// ❌ 不好的缓存键设计
const key = userId; // 太简单,可能冲突
const key = `user_${userId}_profile_data`; // 使用下划线,不规范2. 设置合理的过期时间
javascript
// 根据数据特点设置
await redis.setex('config', 86400, data); // 配置:1天
await redis.setex('user:123', 3600, user); // 用户:1小时
await redis.setex('article:456', 600, article); // 文章:10分钟
await redis.setex('hotspot', 60, data); // 热点:1分钟3. 序列化
javascript
// JSON 序列化
await redis.set('user:123', JSON.stringify(user));
const user = JSON.parse(await redis.get('user:123'));
// 压缩(大对象)
const compressed = zlib.gzipSync(JSON.stringify(data));
await redis.set('large:data', compressed);4. 监控告警
- 命中率 - 高于 80% 为宜
- 内存使用率 - 低于 80%
- 慢查询 - 监控慢查询
- 过期key数量 - 定期清理
📖 学习资源
推荐文章
推荐书籍
- 《Redis 设计与实现》
- 《Redis 深度历险》
准备好了吗?开始优化你的系统缓存!