Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?

Sherwin.Wei Lv7

Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?

回答重点

缓存击穿:指某个热点数据在缓存中失效,导致大量请求直接访问数据库。此时,由于瞬间的高并发,可能导致数据库崩溃。

缓存穿透:指查询一个不存在的数据,缓存中没有相应的记录,每次请求都会去数据库查询,造成数据库负担加重。

缓存雪崩:指多个缓存数据在同一时间过期,导致大量请求同时访问数据库,从而造成数据库瞬间负载激增。

解决方案

缓存击穿

  • 使用互斥锁,确保同一时间只有一个请求可以去数据库查询并更新缓存。
  • 热点数据永不过期。

缓存穿透

  • 使用布隆过滤器,过滤掉不存在的请求,避免直接访问数据库。
  • 对查询结果进行缓存,即使是不存在的数据,也可以缓存一个标识,以减少对数据库的请求。

缓存雪崩

  • 采用随机过期时间策略,避免多个数据同时过期。
  • 使用双缓存策略,将数据同时存储在两层缓存中,减少数据库直接请求。

扩展知识

缓存雪崩解析

Snipaste_2024-06-11_14-36-10.png

缓存雪崩是指在某个时间点,大量缓存同时失效或被清空,导致大量请求直接打到数据库或后端系统,造成系统负载激增,甚至引发系统崩溃。

这通常是由于缓存中的大量数据在同一时间失效引起的。

想象一个电商系统,用户量很大,假设某一系列的商品缓存突然同一时间失效,那就会造成我们的缓存雪崩,导致服务全部打到了数据库,引发严重后果。

解决办法

缓存键同时失效:

1)过期时间随机化:设置缓存的过期时间,加上一个随机值,避免同一时间大量缓存失效。

2)使用多级缓存:引入多级缓存机制,如本地缓存和分布式缓存相结合,减少单点故障风险。

3)缓存预热:系统启动时提前加载缓存数据,避免大量请求落到冷启动状态下的数据库。

4)加互斥锁:使得没缓存或缓存失效的情况下,同一时间只有一个请求来构建缓存,防止数据库压力过大。

缓存中间件故障:

1)服务熔断:暂停业务的返回数据,直接返回错误。

2)构建集群:构建多个 Redis 集群保证其高可用。

缓存击穿解析

Snipaste_2024-06-11_14-36-32.png

缓存击穿指的是某一热点数据缓存失效,使得大量请求直接打到了数据库,增加数据库负载。

想象一下大家都在抢茅台,但在某一时刻茅台的缓存失效了,大家的请求打到了数据库中,这就是缓存击穿。

缓存雪崩是多个 key,大量数据同时过期,而缓存击穿是某个热点 key 崩溃,可以认为缓存击穿是缓存雪崩的子集。

解决办法

1)加互斥锁:保证同一时间只有一个请求来构建缓存,跟缓存雪崩相同。

2)热点数据永不过期:不要给热点数据设置过期时间,在后台异步更新缓存。

缓存穿透解析

Snipaste_2024-06-11_14-36-38.png

缓存穿透是指查询一个不存在的数据,由于缓存中肯定不存在,导致每次请求都直接访问数据库,增加数据库负载。

攻击者可以通过构造不存在的 key 发起大量请求,对数据库造成很大的压力,可能会造成系统宕机。

解决办法

1)防止非法请求:检查非法请求,封禁其 IP 以及账号,防止它再次为非作歹。

2)缓存空值:将数据库中不存在的结果(例如空值)也缓存起来,并设置一个较短的过期时间,避免频繁查询数据库。

3)使用布隆过滤器:使用布隆过滤器来快速判断一个请求的数据是否存在,如果布隆过滤器判断数据不存在,则直接返回,避免查询数据库。

互斥锁实现示例(Java)

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
public class CacheService {
private Jedis jedis = new Jedis("localhost");
private Lock lock = new ReentrantLock();

public String getData(String key) {
// 尝试从缓存获取数据
String value = jedis.get(key);

// 如果缓存不存在
if (value == null) {
// 加锁以防止并发请求
lock.lock();
try {
// 再次检查缓存,避免重复查询
value = jedis.get(key);
if (value == null) {
// 查询数据库
value = queryDatabase(key);
// 将结果放入缓存
jedis.set(key, value);
}
} finally {
// 释放锁
lock.unlock();
}
}
return value;
}
}

说明

  1. 缓存查询:首先尝试从 Redis 中获取数据。
  2. 加锁:如果缓存中没有数据,使用 ReentrantLock 加锁,确保只有一个线程可以查询数据库。
  3. 二次检查:在加锁后再次检查缓存,避免重复查询。
  4. 数据库查询:如果缓存仍然没有数据,查询数据库并将结果存入缓存。
  5. 释放锁:确保锁在查询结束后被释放,以防止死锁。

这种方式有效地防止了缓存击穿,因为即使在高并发的情况下,只有一个请求会去数据库查询数据,其他请求则会等待锁释放。如果后端是多实例部署,一般实例数量也不多,即使使用本地锁也行,因为并发也不高。

布隆过滤器

Comments