Redis 穿透、击穿、雪崩

本文将介绍 Redis 中可能出现的穿透、击穿、雪崩及其解决方案。

一、正常流程

在加入缓存的情况下,正常工作流程是这样的:

  • 接收查询请求
  • 尝试从缓存中获取数据
  • 如果缓存中有,直接返回
  • 如果缓存中没有,则从数据库中获取,将其缓存,返回结果

二、缓存穿透

1. 什么是缓存穿透?

在正常查询流程中,如果某项数据未在缓存中找到,则会尝试从数据库中获取,将结果返回并缓存。

存在这样一个场景:某个查询的结果注定为空,后端接收请求后尝试从缓存中获取,发现缓存中没有后向数据库查询,数据库查询后返回 null,Redis 的一般策略决定了它不会缓存此空数据。因此,每一次相同查询都需要访问数据库。

在这种情况下,每次对 “空数据” 的查询都会 “撬开” 缓存、访问数据库,这便是缓存穿透。

2. 解决方案

(1) 缓存空对象

即使查询结果为空,也将其缓存。

(2) 增加校验

对参数做校验,直接拒绝非法请求。

1
2
3
···/list?pageNum=-1&pageSize=10

// err pageNum不得小于1

(3) 布隆过滤器

具体请看:

什么是布隆过滤器? - 知乎

布隆过滤器是一个数据结构,能够用于 “预估” 元素是否存在,它的优点是高效、快速,缺点是并不完全准确。

具体来说,

  • 布隆过滤器维护了一个位数组
  • 当向其中放入元素时,通过若干个哈希函数计算出若干个索引值,将位数组中的对应位置赋值为 1
  • 当判断元素是否存在时,通过若干个哈希函数计算出若干个索引值,判断这些索引值对应的数组项是否全部为 1

使用布隆过滤器来应对缓存穿透时,有两个核心操作:

  • 数据预热: 预先将所有可能的查询 key 加入布隆过滤器中
  • 存在性校验:接收到查询请求时,首先判断 key 是否存在,存在时才允许查询

数据预热:

将所有的 id 存入布隆过滤器中

1
2
3
4
List<User> users = userMapper.selectList(userQueryWrapper);
for (User user:users) {
bloomFilter.put(user.getId());
}

存在性校验:

1
2
3
4
5
if (bloomFilter.mightContain(id)) {
···查询···
} else {
···拒绝···
}

三、缓存击穿

1. 什么是缓存击穿?

缓存击穿通常发生在高并发的场景下的热点数据过期失效时。

当请求发现数据不存在于缓存时,会去数据库查询数据,将结果放入缓存并返回。向数据库查询需要一定的时间,这段时间内,缓存中的数据始终是缺失的,如果瞬时有大量请求,这些请求都会去数据库查询,给数据库带来巨大的压力。

2. 解决方案

(1) 永不过期

对于不轻易改变且访问量较大的数据,可以设为永不过期。

(2) 加锁

向数据库查询之前,需要先获取锁,从而确保只有一个请求落到数据库。

四、缓存雪崩 - 大量击穿

1. 原因说明

即大量缓存击穿同时发生,通常发生在并发量较大且多个缓存同时过期的情况下。

2. 解决方案

(1) 永不过期

(2) 加锁

(3) 分散过期时间

通过手动设置或随机设置的方式,分散过期时间,避免缓存大量同时过期。

(4) 服务降级

暂缓对非核心服务的处理,给核心服务腾出服务器资源。

  • 如果用户请求的是非核心数据,拒绝访问,返回预定义信息 / 空信息 / 错误信息
  • 如果用户请求的是核心数据,正常处理

五、缓存雪崩 - 缓存宕机

1. 原因说明

如果 Redis 实例发生故障宕机,就会导致大量请求堆积到数据库。

2. 解决方案

(1) 搭建集群

可以通过搭建 Redis 集群的方式,避免单一 Redis 节点宕机影响缓存的正常工作。

(2) 服务熔断

为了避免继而引发数据库的崩溃,暂停服务。

在实际场景中,可以检测 Redis 和数据库所在服务器的负载指标,当发现 Redis 宕机且数据库所有服务器负载增加时,启用服务熔断机制,暂停应用对数据的访问。

(3) 请求限流

请求熔断虽然可以最大程度上保护数据库,但是也极大地影响了应用的正常运行。

还可以使用请求限流,在保护数据库的同时尽量维持应用的正常运行,具体做法是:在请求入口处控制请求量,只允许一定量的请求进入系统。

参考