用大白话聊聊 Redis 缓存“失灵”:击穿、穿透、雪崩,一文讲透!
- 缓存击穿:热点数据在缓存过期时,高并发请求同时涌入导致数据库压力过大。解决方案包括设置较长的过期时间并定期刷新缓存,或使用互斥锁防止多个请求同时访问数据库。
- 缓存穿透:查询不存在的数据,导致每次请求都直接访问数据库。可以通过缓存空值(设置较短过期时间)或使用布隆过滤器预先判断数据是否存在来解决。
- 缓存雪崩:大量缓存在同一时间点失效,所有请求涌向数据库。建议为不同缓存设置随机过期时间以分散失效时间点,或者采用多级缓存策略减轻数据库压力。
认识 Redis 缓存:为什么会失效?
在高性能的应用中,我们经常使用 Redis 作为缓存来减轻数据库的压力。当我们查询数据时,优先从 Redis 中获取,如果 Redis 中没有,再去查询数据库,然后将数据放入 Redis,这个过程被称为 缓存命中。

但是,缓存并非万无一失。当缓存中的数据失效时,就可能引发一系列问题。我们通常将这些问题归纳为三种常见的“失效”场景:缓存击穿、缓存穿透和缓存雪崩。
别担心,这些名字听起来有点吓人,但理解起来其实很简单。我们可以用一个生动的比喻来解释它们。
缓存击穿:高并发的“致命一击”
概念:
想象一下,你家楼下有一家超火的奶茶店。他们把最受欢迎的“招牌奶茶”提前做好了几杯,放在柜台里,顾客一来就能直接拿走(这就像缓存)。
但是,每杯奶茶都有保质期(缓存过期时间)。当最后一杯招牌奶茶刚好卖完,而此刻大量顾客又同时涌进来,都想买这杯奶茶。店员不得不临时去后厨(数据库)现做,但做一杯奶茶需要时间,后面排队的人越来越多,所有人都只能干等。
这就是缓存击穿。它指的是某个热点数据(招牌奶茶)在缓存过期的一瞬间,同时有大量高并发请求涌入,这些请求会绕过缓存,直接访问数据库,导致数据库瞬间压力过大,甚至宕机。
解决方案:
- 设置永不过期:
- 将热点数据设置一个较长的过期时间。
- 在后台定时刷新缓存,或者在数据更新时主动删除缓存。
- 这就像,奶茶店把招牌奶茶保质期延长到很长,并且店员会定期检查,快过期了就主动换新的。
- 加互斥锁(推荐):
- 当第一个请求去查询数据库时,先给这个数据加锁。
- 其他所有后续请求来了,发现这个数据正在被处理,就会在原地等待。
- 等第一个请求处理完毕,将数据放入缓存后,其他请求就能直接从缓存中获取,避免了大量请求同时访问数据库。
代码示例:
下面是一个使用 Java 锁来防止缓存击穿的简单代码示例:
public String getData(String key) {
// 1. 先从缓存获取
String value = redisClient.get(key);
if (value != null) {
return value;
}
// 2. 缓存中没有,加锁
synchronized (this) {
// 3. 再次从缓存获取(双重检查)
value = redisClient.get(key);
if (value != null) {
return value;
}
// 4. 缓存中还是没有,去数据库查询
value = databaseClient.get(key);
// 5. 查到数据,放入缓存
if (value != null) {
redisClient.set(key, value, 60); // 设置过期时间
}
return value;
}
}
缓存穿透:查不到的“白忙活”
概念:
继续奶茶店的比喻。如果一个顾客,每次来都点一杯根本不存在的“月球奶茶”。店员每次都得去后厨(数据库)翻找,结果自然是“查无此茶”。如果有很多顾客都被恶意引导,都来点“月球奶茶”,店员们就会一直白忙活,不断地去后厨查询,数据库的压力会越来越大。
这就是缓存穿透。它指的是查询一个根本不存在的数据。由于缓存中本身就没有这个数据,所以每次请求都会穿过缓存,直接打到数据库上。如果恶意攻击者利用这个漏洞,不断发起查询不存在数据的请求,就会导致数据库负载过高。
解决方案:
- 缓存空值:
- 当数据库查询结果为空时,将这个空结果也缓存起来,并设置一个较短的过期时间。
- 这样,下次再有对这个“不存在数据”的请求,就能直接从缓存中获取到空值,而不用再去访问数据库。
- 就像,店员告诉顾客“没有月球奶茶”,并把这个信息记下来,下次再有人问,就直接回答,不用再进后厨了。
- 布隆过滤器(Bloom Filter):
- 布隆过滤器是一个非常高效的数据结构,它能快速判断一个数据是否存在。
- 在数据写入数据库时,同时将数据的摘要信息放入布隆过滤器。
- 当有请求过来时,先用布隆过滤器判断这个数据是否存在。如果过滤器说**“不存在”,那这个数据就肯定不存在,直接返回,连缓存都不用查。如果过滤器说“可能存在”**,再去查询缓存和数据库。
- 这就像,奶茶店门口有一本厚厚的“菜单”,上面记录了所有存在的奶茶。顾客点单时,先查这本菜单,如果查不到,就直接告诉他没有,省去了去后厨的时间。
代码示例:
public String getData(String key) {
String value = redisClient.get(key);
// 1. 如果缓存中存在,直接返回
if (value != null) {
return value;
}
// 2. 缓存中没有,去数据库查询
value = databaseClient.get(key);
// 3. 数据库查询结果不为空,放入缓存
if (value != null) {
redisClient.set(key, value, 60);
} else {
// 4. 数据库查询结果为空,也放入缓存,并设置较短的过期时间
redisClient.set(key, "null", 5);
}
return value;
}
缓存空值的代码实现非常简单:
缓存雪崩:一起“集体阵亡”
概念:
回到奶茶店。如果店里所有的奶茶,不管招牌的还是普通的,保质期都一样,比如都是上午10点过期。到了10点,所有的奶茶都不能卖了。而此时,刚好是午餐高峰期,大量顾客涌入,所有的请求都因为找不到缓存(奶茶),而涌向了后厨(数据库),导致后厨工作量瞬间暴增,瘫痪了。
这就是缓存雪崩。它指的是在某一个时间点,大量的缓存同时失效。由于这些请求无法命中缓存,导致所有请求都涌向数据库,在瞬间对数据库造成极大的冲击。
解决方案:
- 设置随机过期时间(推荐):
- 给不同的缓存数据设置不同的过期时间。
- 比如,给缓存A设置5分钟过期,给缓存B设置5分10秒过期,给缓存C设置4分50秒过期。
- 这样,即使到了某个时间点,也只有少量缓存会失效,而不是全部失效,从而将数据库压力分散开来。
- 多级缓存:
- 使用多级缓存,比如使用 Ehcache 或 Caffeine 等本地缓存。
- 当 Redis 缓存失效时,请求先尝试从本地缓存中获取,如果本地缓存中还有,就可以直接返回。
- 只有本地缓存也失效时,才去访问数据库。
代码示例:
设置随机过期时间非常简单,只需要在设置缓存时稍作修改:
// 正常设置过期时间
redisClient.set(key, value, 60);
// 设置随机过期时间,在60-120秒之间
// 假设 rand是一个随机数生成器,rand.nextInt(60) 生成0-59的随机数
int randomExpiration = 60 + new Random().nextInt(60);
redisClient.set(key, value, randomExpiration);
总结
| 问题名称 | 发生原因 | 解决方案 |
|---|---|---|
| 缓存击穿 | 热点数据失效,高并发请求同时涌入 | 互斥锁、永不过期 |
| 缓存穿透 | 查询不存在的数据,请求穿透缓存 | 缓存空值、布隆过滤器 |
| 缓存雪崩 | 大量缓存在同一时间失效 | 随机过期时间、多级缓存 |