1. 缓存问题概述
Redis 作为高性能缓存中间件,能够有效提高数据访问速度,但在实际使用中,可能会遇到 缓存击穿、缓存穿透、缓存雪崩 三大问题。这些问题可能会导致 缓存失效、数据库压力骤增、系统崩溃,因此必须采取合理的应对策略。
2. 缓存击穿、穿透、雪崩的区别
问题 | 定义 | 导致的后果 | 典型场景 |
---|---|---|---|
缓存穿透(Cache Penetration) | 查询 缓存和数据库中都不存在的数据,导致每次请求都要访问数据库。 | 数据库压力骤增,影响系统性能。 | 攻击者恶意请求不存在的 key,例如查询 id=-1 。 |
缓存击穿(Cache Breakdown) | 某个热点数据在缓存过期的瞬间,大量请求涌入数据库。 | 数据库瞬时负载过高,可能导致系统崩溃。 | 促销活动商品 ID 缓存过期时,流量暴增。 |
缓存雪崩(Cache Avalanche) | 大量缓存同时过期,导致大量请求打到数据库。 | 数据库承受不了瞬时高并发,可能宕机。 | 设定相同过期时间的大量缓存同时失效。 |
3. 具体案例分析与解决方案
3.1 缓存穿透
案例
ini
java
复制编辑
// 伪代码示例:查询用户信息
String key = "user:1001";
String user = redis.get(key);
if (user == null) { // Redis 中没有数据
user = database.query("SELECT * FROM users WHERE id = 1001"); // 查询数据库
redis.setex(key, 3600, user); // 写入缓存
}
return user;
问题 :如果用户 id=9999
不存在,Redis 没有缓存,每次查询都会打到数据库,造成高并发压力。
解决方案
方案 | 具体措施 | 优缺点 |
---|---|---|
布隆过滤器 | 使用 布隆过滤器(Bloom Filter) 维护一个所有合法 key 的集合,拦截非法请求。 | 低内存占用,误判率较低,但不能删除数据。 |
缓存空值 | 若查询数据库后发现数据不存在,将 null 存入 Redis,并设置短 TTL(如 60s)。 |
有效防止短时间内的重复查询,但可能会缓存无用数据。 |
接口层拦截 | 在应用层限制 ID 规则,如 ID 需大于 0,防止非法访问。 | 适用于特定业务规则。 |
布隆过滤器示例(Java 实现)
javascript
java
复制编辑
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 100000);
bloomFilter.put("user:1001"); // 添加合法 key
if (!bloomFilter.mightContain("user:9999")) {
return null; // 直接拦截
}
3.2 缓存击穿
案例
ini
java
复制编辑
// 高并发访问某个热点 key
String key = "hot-item:123";
String item = redis.get(key);
if (item == null) { // 缓存过期
item = database.query("SELECT * FROM items WHERE id = 123"); // 直接访问数据库
redis.setex(key, 3600, item); // 重新缓存
}
问题 :当 "hot-item:123" 这个热门 key 过期的瞬间,大量请求直接冲向数据库,造成高并发压力。
解决方案
方案 | 具体措施 | 优缺点 |
---|---|---|
互斥锁(Mutex) | 缓存过期时,只允许一个线程查询数据库,其他线程等待。 | 避免数据库短时间高并发,但有一定等待时间。 |
设置热点数据永不过期 | 设置较长 TTL,并使用异步更新机制,减少过期瞬间的冲击。 | 适用于超热点数据,但可能导致数据不一致。 |
提前更新缓存 | 主动刷新缓存,在即将过期前预加载数据,确保缓存持续有效。 | 适用于可预测的缓存更新场景。 |
互斥锁示例
ini
java
复制编辑
String key = "hot-item:123";
String item = redis.get(key);
if (item == null) {
if (redis.setnx("lock:hot-item:123", "1")) { // 获取锁
redis.expire("lock:hot-item:123", 30); // 设置锁过期时间
item = database.query("SELECT * FROM items WHERE id = 123");
redis.setex(key, 3600, item);
redis.del("lock:hot-item:123"); // 释放锁
} else {
Thread.sleep(100); // 休眠后重试
}
}
3.3 缓存雪崩
案例
如果我们在 00:00
统一设置大量缓存的 TTL=3600s
,那么 01:00
这些缓存会同时过期,导致数据库压力激增。
解决方案
方案 | 具体措施 | 优缺点 |
---|---|---|
随机过期时间 | 给每个 key 设置不同的过期时间 (如 3600 ± 600 秒),避免集中过期。 |
实现简单,避免缓存同时失效。 |
分批加载 | 使用双层缓存(L1+L2) ,当 Redis 失效时,先访问本地缓存(如 Guava Cache)。 | 适用于允许短时间一致性的业务。 |
自动重建缓存 | 使用后台线程异步刷新缓存,防止大规模过期后查询数据库。 | 适用于数据较稳定的场景。 |
随机过期时间示例
scss
java
复制编辑
int ttl = 3600 + new Random().nextInt(600); // 设置 3600 ~ 4200 秒随机过期
redis.setex("user:1001", ttl, user);
本地缓存 + Redis
vbnet
java
复制编辑
LoadingCache<String, String> localCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<String, String>() {
public String load(String key) throws Exception {
return redis.get(key);
}
});
4. 结论
问题 | 核心原因 | 最佳解决方案 |
---|---|---|
缓存穿透 | 访问缓存和数据库都不存在的 key | 布隆过滤器 + 缓存空值 |
缓存击穿 | 热点 key 过期瞬间,大量请求打到数据库 | 互斥锁 + 提前更新缓存 |
缓存雪崩 | 大量 key 同时过期,数据库负载骤增 | 随机 TTL + 分批加载 |
🔹 企业级应用中,通常结合多种方案 ,例如 布隆过滤器 + 互斥锁 + 随机过期时间,来优化 Redis 缓存架构,确保高并发下的稳定性。