Redis 缓存穿透、击穿与雪崩防护指南:布隆过滤器、互斥锁与多级缓存实战

声明:本文部分内容使用AI辅助生成,经人工编辑、审核和补充个人经验。

更新说明:技术栈版本信息基于 Redis 7.x / Java 17 / Spring Boot 3.x。

引言

在高并发系统中,Redis 缓存是提升性能的关键组件。然而,当缓存遇到异常流量或设计缺陷时,可能会出现缓存穿透、缓存击穿和缓存雪崩等问题,导致数据库压力剧增甚至系统崩溃。这里深入分析这三种缓存问题的成因,并提供布隆过滤器、互斥锁、多级缓存等多种防护方案的实战代码。

缓存问题分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────────────────┐
│ Redis 缓存问题分类 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 缓存穿透 (Penetration) │
│ ├── 定义:查询不存在的数据,缓存和数据库都无结果 │
│ ├── 危害:每次请求都打到数据库 │
│ └── 场景:恶意攻击、非法参数 │
│ │
│ 缓存击穿 (Breakdown) │
│ ├── 定义:热点 Key 过期瞬间,大量请求打到数据库 │
│ ├── 危害:数据库压力突增 │
│ └── 场景:秒杀商品、热门文章 │
│ │
│ 缓存雪崩 (Avalanche) │
│ ├── 定义:大量 Key 同时过期,所有请求打到数据库 │
│ ├── 危害:数据库崩溃,系统不可用 │
│ └── 场景:缓存重建失败、定时任务导致集中过期 │
│ │
└─────────────────────────────────────────────────────────────────────┘

缓存穿透防护

问题分析

缓存穿透是指查询一个一定不存在的数据,由于缓存不命中且数据库也无结果,导致每次请求都要访问数据库。攻击者可以利用这一点发起大量非法请求,压垮数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────────────────┐
│ 缓存穿透示意图 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 攻击者 应用层 缓存层 数据库 │
│ │ │ │ │ │
│ │ GET /user/-1 │ │ │ │
│ │ ───────────────►│ │ │ │
│ │ │ 查询缓存 │ │ │
│ │ │ ────────────────►│ │ │
│ │ │ 未命中(null) │ │ │
│ │ │ ◄───────────────│ │ │
│ │ │ │ │ │
│ │ │ 查询数据库 │ │ │
│ │ │ ───────────────────────────────►│ │
│ │ │ 无结果 │ │ │
│ │ │ ◄───────────────────────────────│ │
│ │ │ │ │ │
│ │ 返回 404 │ │ │ │
│ │ ◄───────────────│ │ │ │
│ │ │ │ │ │
│ │ 【重复上述流程,每次请求都打到数据库】 │
│ │
└─────────────────────────────────────────────────────────────────────┘

方案一:缓存空对象

最简单的方案是将空结果也缓存起来,设置较短的过期时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Node.js + ioredis 示例
const Redis = require('ioredis');
const redis = new Redis();

async function getUser(userId) {
// 1. 查询缓存
const cacheKey = `user:${userId}`;
const cached = await redis.get(cacheKey);

// 2. 缓存命中(包括空值标记)
if (cached !== null) {
// 空值标记判断
if (cached === '__NULL__') {
return null;
}
return JSON.parse(cached);
}

// 3. 查询数据库
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);

// 4. 写入缓存(包括空值)
if (user) {
await redis.setex(cacheKey, 3600, JSON.stringify(user));
} else {
// 空值缓存 5 分钟
await redis.setex(cacheKey, 300, '__NULL__');
}

return user;
}
优点 缺点
实现简单 占用额外内存
效果显著 可能缓存大量无效 Key
无额外依赖 短暂的数据不一致

方案二:布隆过滤器

布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─────────────────────────────────────────────────────────────────────┐
│ 布隆过滤器原理 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 初始状态:位数组全部置为 0 │
│ ┌─────────────────────────────────────────┐ │
│ │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 添加元素 "user:1001": │
│ • Hash1("user:1001") % 10 = 2 → 位置 2 置 1 │
│ • Hash2("user:1001") % 10 = 5 → 位置 5 置 1 │
│ • Hash3("user:1001") % 10 = 8 → 位置 8 置 1 │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 0 │ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 查询 "user:9999"(不存在): │
│ • Hash1("user:9999") % 10 = 2 → 位置 2 为 1 ✓ │
│ • Hash2("user:9999") % 10 = 6 → 位置 6 为 0 ✗ │
│ │
│ 【结论:"user:9999" 一定不存在】 │
│ │
│ 查询 "user:1001"(存在): │
│ • 所有 Hash 位置都为 1 │
│ 【结论:"user:1001" 可能存在(有一定误判率)】 │
│ │
└─────────────────────────────────────────────────────────────────────┘

Node.js 布隆过滤器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 使用 bloom-filters 库
const { BloomFilter } = require('bloom-filters');

// 创建布隆过滤器
// 参数:预计元素数量 100000,误判率 0.01
const filter = new BloomFilter(100000, 0.01);

// 初始化:加载所有有效用户 ID
async function initBloomFilter() {
const userIds = await db.query('SELECT id FROM users');
userIds.forEach(user => {
filter.add(`user:${user.id}`);
});

// 将过滤器数据保存到 Redis
const exported = filter.export();
await redis.set('bloom:user', JSON.stringify(exported));
}

// 查询用户
async function getUserWithBloom(userId) {
const cacheKey = `user:${userId}`;

// 1. 布隆过滤器检查
if (!filter.has(cacheKey)) {
// 一定不存在,直接返回
return null;
}

// 2. 查询缓存
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}

// 3. 查询数据库
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);

// 4. 写入缓存
if (user) {
await redis.setex(cacheKey, 3600, JSON.stringify(user));
}

return user;
}

Redis 4.0+ RedisBloom 模块

1
2
# 加载 RedisBloom 模块
redis-server --loadmodule /path/to/redisbloom.so
1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建布隆过滤器
BF.RESERVE user_filter 0.01 100000

# 添加元素
BF.ADD user_filter user:1001
BF.ADD user_filter user:1002

# 批量添加
BF.MADD user_filter user:1003 user:1004 user:1005

# 检查元素是否存在
BF.EXISTS user_filter user:1001 # 返回 1(可能存在)
BF.EXISTS user_filter user:9999 # 返回 0(一定不存在)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Node.js 使用 RedisBloom
async function getUserWithRedisBloom(userId) {
const cacheKey = `user:${userId}`;

// 1. 使用 RedisBloom 检查
const exists = await redis.call('BF.EXISTS', 'user_filter', cacheKey);
if (!exists) {
return null; // 一定不存在
}

// 2. 查询缓存
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}

// 3. 查询数据库
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);

// 4. 写入缓存
if (user) {
await redis.setex(cacheKey, 3600, JSON.stringify(user));
}

return user;
}
方案 内存占用 准确率 复杂度
缓存空对象 较高 100%
布隆过滤器 极低 99%+

方案三:参数校验与限流

在应用层进行参数校验,拦截明显非法的请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Express 中间件示例
const rateLimit = require('express-rate-limit');

// 限流:每分钟最多 100 次请求
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
message: '请求过于频繁,请稍后重试'
});

// 参数校验
function validateUserId(req, res, next) {
const userId = parseInt(req.params.id);

// 校验 ID 范围
if (isNaN(userId) || userId <= 0 || userId > 10000000) {
return res.status(400).json({ error: 'Invalid user ID' });
}

next();
}

// 路由应用
app.get('/api/user/:id', apiLimiter, validateUserId, async (req, res) => {
const user = await getUser(req.params.id);
res.json(user);
});

缓存击穿防护

问题分析

缓存击穿是指某个热点 Key 在过期的瞬间,有大量并发请求同时访问,所有请求都打到数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────────────────┐
│ 缓存击穿示意图 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 时间轴 ───────────────────────────────────────────────────────► │
│ │
│ T0 T1 T2 T3 │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──┐ ┌──┐ ┌──┐ ┌──┐ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │DB│ │ │ │
│ └──┘ └──┘ └──┘ └──┘ │
│ 缓存命中 缓存命中 缓存过期 缓存重建 │
│ 大量请求打到DB │
│ │
│ 【热点 Key 过期瞬间,所有请求穿透到数据库】 │
│ │
└─────────────────────────────────────────────────────────────────────┘

方案一:互斥锁(Mutex)

只允许一个线程查询数据库并重建缓存,其他线程等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const Redlock = require('redlock');

const redlock = new Redlock([redis], {
driftFactor: 0.01,
retryCount: 10,
retryDelay: 200,
retryJitter: 200
});

async function getUserWithLock(userId) {
const cacheKey = `user:${userId}`;
const lockKey = `lock:${cacheKey}`;

// 1. 查询缓存
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}

// 2. 获取分布式锁
let lock;
try {
lock = await redlock.lock(lockKey, 1000);

// 3. 双重检查(其他线程可能已重建缓存)
const cached2 = await redis.get(cacheKey);
if (cached2) {
return JSON.parse(cached2);
}

// 4. 查询数据库
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);

// 5. 写入缓存
if (user) {
await redis.setex(cacheKey, 3600, JSON.stringify(user));
}

return user;
} catch (err) {
// 获取锁失败,短暂等待后重试
await sleep(100);
return getUserWithLock(userId);
} finally {
// 6. 释放锁
if (lock) {
await lock.unlock().catch(() => {});
}
}
}

方案二:逻辑过期(永不过期)

不设置 TTL,而是在数据中添加过期时间字段,由应用层判断是否过期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
async function getUserWithLogicalExpire(userId) {
const cacheKey = `user:${userId}`;

// 1. 查询缓存
const cached = await redis.get(cacheKey);

if (cached) {
const data = JSON.parse(cached);
const now = Date.now();

// 2. 检查逻辑过期时间
if (data.expireTime > now) {
// 未过期,直接返回
return data.value;
}

// 3. 已过期,尝试获取锁重建
const lockKey = `lock:${cacheKey}`;
const acquired = await redis.set(lockKey, '1', 'EX', 10, 'NX');

if (acquired) {
// 获取锁成功,异步重建缓存
rebuildCache(userId, cacheKey).catch(console.error);
}

// 4. 返回过期数据(保证可用性)
return data.value;
}

// 5. 缓存不存在,查询数据库
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);

if (user) {
const cacheData = {
value: user,
expireTime: Date.now() + 3600 * 1000 // 1 小时后过期
};
await redis.set(cacheKey, JSON.stringify(cacheData));
}

return user;
}

async function rebuildCache(userId, cacheKey) {
try {
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
if (user) {
const cacheData = {
value: user,
expireTime: Date.now() + 3600 * 1000
};
await redis.set(cacheKey, JSON.stringify(cacheData));
}
} finally {
await redis.del(`lock:${cacheKey}`);
}
}
方案 优点 缺点
互斥锁 数据一致性好 可能产生等待延迟
逻辑过期 无等待延迟 可能返回旧数据

缓存雪崩防护

问题分析

缓存雪崩是指大量 Key 同时过期,或者 Redis 集群故障,导致所有请求直接访问数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────────────────┐
│ 缓存雪崩示意图 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ T0: 正常状态 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Key A: │ │ Key B: │ │ Key C: │ │
│ │ Exp: T+1 │ │ Exp: T+1 │ │ Exp: T+1 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ T1: 集体过期 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Key A: │ │ Key B: │ │ Key C: │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┴─────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Database │ │
│ │ 💀 崩溃 │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

方案一:随机过期时间

为缓存的过期时间添加随机偏移,避免同时过期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function setWithRandomExpire(key, value, baseExpireSeconds) {
// 添加 0~600 秒的随机偏移
const randomOffset = Math.floor(Math.random() * 600);
const expireSeconds = baseExpireSeconds + randomOffset;

await redis.setex(key, expireSeconds, value);
}

// 使用示例
async function cacheUser(user) {
const key = `user:${user.id}`;
const value = JSON.stringify(user);

// 基础过期时间 3600 秒,实际 3600~4200 秒
await setWithRandomExpire(key, value, 3600);
}

方案二:多级缓存

使用本地缓存(Caffeine/Guava)作为一级缓存,Redis 作为二级缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const NodeCache = require('node-cache');

// 本地缓存(一级)
const localCache = new NodeCache({
stdTTL: 60, // 本地缓存 60 秒
checkperiod: 120
});

async function getUserWithMultiLevel(userId) {
const cacheKey = `user:${userId}`;

// 1. 查询本地缓存
const localData = localCache.get(cacheKey);
if (localData) {
return localData;
}

// 2. 查询 Redis(二级缓存)
const redisData = await redis.get(cacheKey);
if (redisData) {
const user = JSON.parse(redisData);
// 回填本地缓存
localCache.set(cacheKey, user);
return user;
}

// 3. 查询数据库
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);

if (user) {
// 回填两级缓存
localCache.set(cacheKey, user);
await redis.setex(cacheKey, 3600, JSON.stringify(user));
}

return user;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
┌─────────────────────────────────────────────────────────────────────┐
│ 多级缓存架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 请求 │
│ │ │
│ ▼ │
│ ┌──────────────┐ 命中 ┌──────────────┐ │
│ │ Local Cache │◄──────────│ 应用层 │ │
│ │ (一级缓存) │ │ │ │
│ └──────┬───────┘ │ Node.js │ │
│ │ 未命中 │ │ │
│ ▼ └──────┬───────┘ │
│ ┌──────────────┐ 命中 │ │
│ │ Redis │◄────────────────┘ │
│ │ (二级缓存) │ │
│ └──────┬───────┘ │
│ │ 未命中 │
│ ▼ │
│ ┌──────────────┐ │
│ │ Database │ │
│ │ (数据库) │ │
│ └──────────────┘ │
│ │
│ 优点: │
│ • 本地缓存访问速度极快(微秒级) │
│ • Redis 故障时,本地缓存仍可提供服务 │
│ • 减少 Redis 网络开销 │
│ │
└─────────────────────────────────────────────────────────────────────┘

方案三:缓存预热与高可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 缓存预热:系统启动时加载热点数据
async function warmupCache() {
// 加载热门商品
const hotProducts = await db.query(
'SELECT * FROM products WHERE is_hot = 1 LIMIT 1000'
);

for (const product of hotProducts) {
const key = `product:${product.id}`;
await redis.setex(key, 7200, JSON.stringify(product));
}

console.log(`预加载了 ${hotProducts.length} 个热点商品`);
}

// 定时刷新即将过期的热点数据
async function refreshHotData() {
const hotKeys = await redis.keys('product:*');

for (const key of hotKeys.slice(0, 100)) {
const ttl = await redis.ttl(key);

// TTL 小于 300 秒的热点数据提前刷新
if (ttl > 0 && ttl < 300) {
const productId = key.replace('product:', '');
const product = await db.query(
'SELECT * FROM products WHERE id = ?',
[productId]
);

if (product) {
await redis.setex(key, 7200, JSON.stringify(product));
console.log(`刷新热点数据: ${key}`);
}
}
}
}

// 定时任务
setInterval(refreshHotData, 60 * 1000); // 每分钟检查一次

综合防护方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// 综合缓存工具类
class CacheManager {
constructor(redis, localCache) {
this.redis = redis;
this.localCache = localCache;
this.bloomFilter = null; // 布隆过滤器实例
}

/**
* 获取数据(综合防护)
*/
async get(key, queryDB, options = {}) {
const {
useBloom = false, // 是否使用布隆过滤器
useLock = false, // 是否使用互斥锁
useLocal = false, // 是否使用本地缓存
expireSeconds = 3600, // 过期时间
nullExpireSeconds = 60 // 空值过期时间
} = options;

// 1. 布隆过滤器检查
if (useBloom && this.bloomFilter) {
if (!this.bloomFilter.has(key)) {
return null;
}
}

// 2. 本地缓存
if (useLocal && this.localCache) {
const localData = this.localCache.get(key);
if (localData !== undefined) {
return localData;
}
}

// 3. Redis 缓存
const cached = await this.redis.get(key);
if (cached !== null) {
if (cached === '__NULL__') {
return null;
}
const data = JSON.parse(cached);
if (useLocal) {
this.localCache.set(key, data, 60);
}
return data;
}

// 4. 数据库查询(可能加锁)
let data;
if (useLock) {
data = await this.getWithLock(key, queryDB, expireSeconds);
} else {
data = await queryDB();
await this.set(key, data, expireSeconds, nullExpireSeconds);
}

// 5. 回填本地缓存
if (useLocal && data !== null) {
this.localCache.set(key, data, 60);
}

return data;
}

/**
* 设置缓存(带随机偏移)
*/
async set(key, value, expireSeconds, nullExpireSeconds = 60) {
if (value === null) {
await this.redis.setex(key, nullExpireSeconds, '__NULL__');
} else {
// 添加随机偏移,防止雪崩
const randomOffset = Math.floor(Math.random() * 600);
await this.redis.setex(
key,
expireSeconds + randomOffset,
JSON.stringify(value)
);
}
}

/**
* 带互斥锁的查询
*/
async getWithLock(key, queryDB, expireSeconds) {
const lockKey = `lock:${key}`;

// 尝试获取锁
const acquired = await this.redis.set(lockKey, '1', 'EX', 10, 'NX');

if (!acquired) {
// 获取锁失败,等待后重试
await sleep(100);
const cached = await this.redis.get(key);
if (cached && cached !== '__NULL__') {
return JSON.parse(cached);
}
return this.getWithLock(key, queryDB, expireSeconds);
}

try {
// 双重检查
const cached = await this.redis.get(key);
if (cached && cached !== '__NULL__') {
return JSON.parse(cached);
}

// 查询数据库
const data = await queryDB();
await this.set(key, data, expireSeconds);
return data;
} finally {
await this.redis.del(lockKey);
}
}
}

// 使用示例
const cacheManager = new CacheManager(redis, localCache);

// 普通查询(缓存空对象 + 随机过期)
const user = await cacheManager.get(
`user:${userId}`,
() => db.query('SELECT * FROM users WHERE id = ?', [userId]),
{ expireSeconds: 3600 }
);

// 热点数据查询(布隆过滤器 + 本地缓存 + 互斥锁)
const product = await cacheManager.get(
`product:${productId}`,
() => db.query('SELECT * FROM products WHERE id = ?', [productId]),
{
useBloom: true,
useLock: true,
useLocal: true,
expireSeconds: 7200
}
);

总结

Redis 缓存防护的核心要点:

  1. 缓存穿透:使用布隆过滤器拦截非法请求,或缓存空对象减少数据库访问
  2. 缓存击穿:使用互斥锁或逻辑过期保护热点 Key,避免并发重建
  3. 缓存雪崩:随机过期时间打散过期点,多级缓存提供冗余保护
  4. 综合策略:根据业务特点组合使用多种防护手段
  5. 监控告警:建立缓存命中率、数据库 QPS 监控,及时发现问题

通过合理的防护设计,可以让 Redis 缓存系统在高并发场景下稳定运行,为业务提供可靠的性能保障。