想象你每天必喝的咖啡馆,店员发现你固定点冰美式后,提前把配方贴在收银台背后。这本是为提升效率的好意,但如果遇到三种情况就会翻车:
- 有人非要喝不存在的「珍珠奶茶」(店员每次都查配方本却发现没有)
- 今天唯一会做冰美式的店员请假(其他店员挤在操作台前不知所措)
- 所有咖啡配方表同时被风吹走(整个店铺陷入混乱)
这就是缓存穿透、击穿和雪崩的生动写照。我们来看看Node.js中如何用代码应对这些场景。
1. 缓存穿透:如何应对「找不存在」的请求
1.1 现象诊断
当查询数据库中不存在的数据时,每次请求都穿透缓存直达数据库,就像不断问店员要不存在商品的顾客。
1.2 解决方案示例
(Node.js + Redis)
方案一:布隆过滤器把关
// 初始化布隆过滤器
const { BloomFilter } = require('bloom-filters');
const filter = BloomFilter.create(1000000, 0.01); // 百万数据量,1%误判率
// 预热阶段加载有效ID
const validIds = await db.query('SELECT id FROM products');
validIds.forEach(id => filter.add(id));
// 查询拦截
app.get('/product/:id', async (req, res) => {
const id = req.params.id;
// 优先检查布隆过滤器
if (!filter.has(id)) {
return res.status(404).json({ error: '商品不存在' });
}
// 后续查询流程...
});
方案二:缓存空对象
const redis = require('ioredis');
const client = new redis();
app.get('/product/:id', async (req, res) => {
const id = req.params.id;
let data = await client.get(`product:${id}`);
// 缓存命中特殊空值
if (data === 'NULL') return res.status(404).send();
if (data) return res.json(JSON.parse(data));
// 数据库查询
const product = await db.query('SELECT * FROM products WHERE id = ?', [id]);
if (!product) {
// 缓存空值5分钟
await client.setex(`product:${id}`, 300, 'NULL');
return res.status(404).send();
}
// 正常缓存数据
await client.setex(`product:${id}`, 3600, JSON.stringify(product));
res.json(product);
});
2. 缓存击穿:热点数据失效的连锁反应
2.1 危机时刻
就像网红奶茶店突然排起长队,而此时收银系统故障,所有顾客都要手动登记订单。
2.2 互斥锁实现
(Node.js + Redis)
const acquireLock = async (key, ttl = 10) => {
const result = await client.set(key, 'LOCK', 'EX', ttl, 'NX');
return result === 'OK';
};
app.get('/hot-product/:id', async (req, res) => {
const cacheKey = `product:${req.params.id}`;
let data = await client.get(cacheKey);
if (data) return res.json(JSON.parse(data));
// 尝试获取分布式锁
const lockKey = `${cacheKey}:lock`;
const isLocked = await acquireLock(lockKey);
if (!isLocked) {
// 未获得锁时的降级策略
await new Promise(resolve => setTimeout(resolve, 50));
return this.get('/hot-product/' + req.params.id)(req, res);
}
try {
// 二次检查缓存(可能在等待锁期间数据已重建)
data = await client.get(cacheKey);
if (data) return res.json(JSON.parse(data));
// 数据库查询
const product = await db.query('SELECT * FROM products WHERE id = ?', [id]);
await client.setex(cacheKey, 3600, JSON.stringify(product));
res.json(product);
} finally {
await client.del(lockKey);
}
});
3. 缓存雪崩:当集体失效引发灾难
3.1 预防策略
为不同的缓存键设置随机过期时间,避免同时失效:
// 设置缓存时加入随机抖动
const setCacheWithJitter = async (key, value, baseTTL = 3600) => {
const jitter = Math.floor(Math.random() * 600); // 0-10分钟随机值
await client.setex(key, baseTTL + jitter, value);
};
// 缓存预热时使用
await setCacheWithJitter('product:1001', productData);
4. 混合战术:实战中的组合拳
4.1 多级缓存架构示例
// 第一层:内存缓存
const LRU = require('lru-cache');
const memoryCache = new LRU({ max: 500 });
// 第二层:Redis集群
const getProduct = async (id) => {
// 优先读取内存缓存
if (memoryCache.has(id)) return memoryCache.get(id);
// 读取Redis缓存
const redisData = await client.get(`product:${id}`);
if (redisData) {
memoryCache.set(id, redisData);
return redisData;
}
// 数据库查询并回填
const dbData = await fetchFromDB(id);
setCacheWithJitter(`product:${id}`, dbData);
memoryCache.set(id, dbData);
return dbData;
};
5. 应用场景与选型指南
5.1 典型场景对比
场景特征 | 适用方案 |
---|---|
频繁查询不存在ID | 布隆过滤器 + 空值缓存 |
秒杀活动热点商品 | 互斥锁 + 逻辑过期时间 |
大促期间全站缓存刷新 | 多级缓存 + 随机过期时间 |
6. 经验总结:缓存管理四要诀
- 容量规划:Redis内存使用不超过70%,设置淘汰策略
- 监控报警:关注缓存命中率、穿透率等核心指标
- 降级预案:在缓存集群故障时快速切到本地缓存
- 版本管理:缓存键包含版本号以便灰度更新