Redis 缓存击穿、穿透与雪崩治理¶
在以 Redis 为核心缓存层的高并发分布式系统中,缓存承担了 95% 以上的读流量,有效保护了后端脆弱的关系型数据库(如 MySQL / PostgreSQL)。
然而,当系统遭遇海量恶意黑客扫描、超大热点 Key 突发过期、或缓存集群大面积宕机时,流量会瞬间穿透防御层直击底层数据库,引发**数据库连接池打满、CPU 100%、服务雪崩瘫痪**。本文将针对缓存领域的三大经典灾难提供工业级完整治理方案。
一、三大缓存灾难诱因与防御策略矩阵¶
graph TD
subgraph Penetration [1. 缓存穿透 (Penetration)]
QueryNonExist["查询根本不存在的恶意 Key (如 id = -999)"] --> RedisMiss1[Redis 缓存未命中]
RedisMiss1 --> DBMiss1[直打 DB,DB 亦无此数据,无法回填缓存,每次请求都穿透 DB!]
end
subgraph Breakdown [2. 缓存击穿 (Breakdown)]
HotKeyExpire["单个超级热点 Key (如爆款秒杀商品) 瞬间过期"] --> MassiveReq[数十万 QPS 瞬间穿透到达 DB 重建缓存,拖垮 DB!]
end
subgraph Avalanche [3. 缓存雪崩 (Avalanche)]
BatchExpire[大量 Key 在同一秒集体到期 或 Redis 节点宕机] --> AllToDB[全量读流量瞬间排山倒海压垮整个数据库集群!]
end | 灾难类型 | 发生场景与特征 | 危害程度 | 核心推荐治理方案 |
|---|---|---|---|
| 缓存穿透 | 查询数据库中**压根不存在**的数据(如黑客恶意扫描不存在的 ID) | 持续消耗 DB 连接与 CPU 资源 | 布隆过滤器 (Bloom Filter) 前置拦截 + 空对象短期缓存 (setex key 30 "") |
| 缓存击穿 | 单点极高并发的热点 Key 在某一瞬间恰好缓存过期 | 瞬间数万 QPS 冲击数据库单行记录 | 互斥锁 (SETNX) 重建 或 逻辑过期 (Logical Expiration) 异步刷新 |
| 缓存雪崩 | 大量不同 Key 恰好设置了相同的过期时间,或 Redis 全局瘫痪 | 整个数据库集群瞬间宕机 | 过期时间增加随机扰动 (Jitter) + 本地内存二级缓存 (Caffeine) 多级分流 |
二、缓存穿透防御:Redis 布隆过滤器实战¶
布隆过滤器由一个长二进制位数组(Bit Array)与多个独立的哈希函数组成,具备极高的空间效率。若布隆过滤器判定某 Key 不存在,则该 Key 100% 不存在:
// services/BloomFilterProtector.ts
import { Redis } from 'ioredis';
const redis = new Redis();
export class ProductCacheService {
/**
* 采用布隆过滤器 + 空值缓存双重防御缓存穿透
*/
async getProductDetail(productId: string): Promise<any> {
const cacheKey = `product:detail:${productId}`;
// 1. 第一道防线:布隆过滤器快速预检
// 利用 Redis 的 BF.EXISTS 命令 (RedisBloom 模块)
const exists = await redis.send_command('BF.EXISTS', 'bf_products', productId);
if (exists === 0) {
console.warn(`[布隆拦截] 商品 ID ${productId} 绝不存在,直接阻断!`);
return null;
}
// 2. 查询 Redis 缓存
const cachedData = await redis.get(cacheKey);
if (cachedData !== null) {
if (cachedData === '__NULL_PLACEHOLDER__') return null; // 命中空值缓存
return JSON.parse(cachedData);
}
// 3. 穿透至数据库查询
const product = await db.product.findUnique({ where: { id: productId } });
if (!product) {
// 数据库中不存在:写入短暂的空值占位缓存 (60秒),防止同 ID 反复穿透
await redis.set(cacheKey, '__NULL_PLACEHOLDER__', 'EX', 60);
return null;
}
// 数据库存在:正常写入缓存 (30分钟)
await redis.set(cacheKey, JSON.stringify(product), 'EX', 1800);
return product;
}
}
三、缓存击穿防御:逻辑过期 (Logical Expiration) 异步构建¶
对于微博热搜、大促秒杀等超级热点,物理上永不过期,而在 Value JSON 中嵌入 expireTime 逻辑过期字段。一旦发现逻辑过期,通过分布式互斥锁仅由一个异步 Worker 刷新,其余请求直接返回旧数据(保证高可用):
// services/HotKeyBreakerGuard.ts
interface CacheData<T> {
data: T;
logicalExpireTime: number; // 毫秒时间戳
}
export async function getHotProductWithLogicalExpire<T>(
key: string,
dbFallback: () => Promise<T>,
ttlSeconds = 60
): Promise<T> {
const raw = await redis.get(key);
if (!raw) {
// 首次全空冷启动,同步回源
const val = await dbFallback();
const payload: CacheData<T> = { data: val, logicalExpireTime: Date.now() + ttlSeconds * 1000 };
await redis.set(key, JSON.stringify(payload));
return val;
}
const cacheObj: CacheData<T> = JSON.parse(raw);
const isExpired = Date.now() > cacheObj.logicalExpireTime;
if (!isExpired) {
return cacheObj.data; // 仍在逻辑有效期内,直接返回
}
// 已逻辑过期:尝试获取分布式锁进行异步重建
const lockKey = `lock:rebuild:${key}`;
const acquired = await redis.set(lockKey, '1', 'EX', 10, 'NX');
if (acquired === 'OK') {
// 成功抢到锁:开启异步线程执行耗时 DB 重建,不阻塞当前响应
(async () => {
try {
console.log(`[热点重建] 正在后台异步刷新缓存: ${key}`);
const freshData = await dbFallback();
const newPayload: CacheData<T> = {
data: freshData,
logicalExpireTime: Date.now() + ttlSeconds * 1000,
};
await redis.set(key, JSON.stringify(newPayload));
} finally {
await redis.del(lockKey); // 释放锁
}
})();
}
// 未抢到锁或正在重建中:直接返回旧数据 (Stale-While-Revalidate 理念)
return cacheObj.data;
}
四、缓存雪崩防御:过期时间 Jitter 随机打散¶
为避免成千上万个 Key 在整点同时失效,设置过期时间时强制附加随机扰动偏移量: