Redis 缓存问题:穿透、击穿、雪崩是什么及其解决方案

思维导图

本文的思维导图如下:

一、缓存穿透

(一)什么是缓存穿透?

缓存穿透针对的是查询数据库和缓存中都不存在的数据,即请求的数据本身就是无效的。

这种情况下,每次请求都会直接打到数据库,而数据库查询不到结果,导致缓存中也不会存储该数据。

**比如:**有一个 GET 请求:api/news/getById/1

数据库里没有 key = 1 的数据,却有请求 key = 1 的数据,这样 Redis 差不多,再查到 DB。

(二)如何解决缓存穿透?

方案一:缓存空数据

缓存空数据:如果查询到数据库没有该数据,我们就缓存并返回空结果。

优点: 实现简单

缺点:

  • 消耗内存,内存的压力会很大
  • 可能发送缓存于数据库不一致的问题,比如:
    • 一开始有数据库里没有 ID = 1 的数据,为了解决缓存穿透,我们把缓存里 ID = 1 的数据设置为 null;
    • 后面真来了一个 ID = 1 的数据,虽然数据库里没有这个数据,缓存却有这个数据。

方案二:布隆过滤器

在预热缓存的同时,也预热布隆过滤器(往布隆过滤器里面添加数据)。

当有请求发来的时候,先让布隆过滤器判断一下数据是否存在:

  • 存在则放行到 Reids 和 DB
  • 不存在就返回

**比如:**有一个 GET 请求:api/news/getById/1

先查询布隆过滤器里面有没有 ID = 1 这个数据:

  • 如果有,则往后查询并返回数据
  • 如果没有,则直接返回

布隆过滤器的底层结构 是一个位数组 (bit array),并配合多个哈希函数

每个哈希函数会对给定的元素生成一个哈希值,这个哈希值对应位数组中的一个位置,然后将该位置的位设置为1。

什么是位数组?

相当于一个以 位(bit) 为单位的数组,数组中每个单元只能存储二进制数 0 或 1.

作用: 布隆过滤器可以用于检索一个元素是否在一个集合中。

添加元素时,每个哈希函数都会计算出一个哈希值,并将对应的位数组位置设为1。

检查一个元素是否可能存在于集合中时,同样的哈希函数会被用来计算哈希值,如果所有计算出的位位置都是1,则认为该元素可能存在于集合中。

误判

但是,由于哈希冲突的存在,布隆过滤器可能会产生误判(false positives),即错误地认为一个元素存在于集合中,即使它实际上并不存在。

比如:id = 3 的数据是不存在的,但是经过三次 hash 计算后,Redis 发现计算的数组位置都为 1,则误判 id = 3 数据存在。

误判率的影响因素:数组大小

  • 数组越大,误判率越小,但内存消耗更大
  • 数组越小,误判率越大,但内存消耗低了

布隆过滤器的实现方案:

  • Redisson
  • Guava

我们一般在项目里会设置误判率 0.05:

**优点:**内存占用较少,没有多余 key

缺点: 实现复杂,存在误判

二、缓存击穿

(一)什么是缓存击穿?

缓存击穿针对的是**热点数据**,通常是多个客户端几乎同时查询一条缓存中没有的数据。

这种情况下,缓存中没有该数据,所有请求都会直接打到数据库上,造成数据库压力骤增。

举例:

比如双十一的时候,淘宝给某一个热门商品的 key 设置了有效时间 20: 00-24: 00(四小时)

当晚上 24:00的时候,这个 key 过期失效了,但是这个时间点还有大量并发请求来读取它。

这些请求在 Redis 里面查不到数据,就会直接打到数据库 → 造成数据库响应不及时,然后挂掉

(二)如何解决缓存击穿?

方案一:互斥锁(分布式锁)

大量的用户去 Redis 里面请求数据

  • Redis 如果有的话就会返回给用户,
  • Redis 如果没有的话,就会请求数据库去查询数据

我们就在"请求数据库"这一步给它上锁:

  • 只有一个线程可以抢到这个锁,所以也就只有一个线程可以操作这个数据库
  • 当它查询到数据之后,再把缓存重新写到 Redis 里面
  • 其他没有抢到锁的线程,让它们先睡眠几毫秒,然后再重新去 Redis 里面查询数据。

因为只有一个线程可以拿到互斥锁,进行缓存重建,其他线程需要等待。

优点: 保证数据的强一致性

缺点: 性能差

使用场景: 金融行业,需要安全保险

方案二:逻辑过期

线程 1 查到 Redis,发现数据已过期,就获取互斥锁,然后再开一个线程 2 来缓存重建:

  • 线程 2 会到数据库里面查询数据,把数据写入 Redis,并重设逻辑过期时间,然后释放锁
  • 在线程 2 进行缓存重建的过程中(没释放锁之前):其他线程(比如线程 3)来到 Redis 查询数据,发现逻辑时间过期,获取互斥锁失败,会返回一个过期数据回去。

线程 2 缓存重建完成后,其他线程(比如线程4)就可以在 Redis 里面命中缓存了。


优点:

  • 高可用(别管结果可不可用,先返回结果再说)
  • 性能优

缺点: 不能保证数据绝对一致性

使用场景: 互联网行业,强调用户体验感

三、缓存雪崩

(一)什么是缓存雪崩?

1. 什么是缓存雪崩

缓存雪崩就是:

  • 同一时间段内 Redis 中大量的 key 集体同时失效
  • 或者 Redis 服务宕机,导致大量的请求到达数据库,给数据库带来巨大压力。

2. 缓存雪崩和缓存击穿的区别

不同点:

  • 缓存击穿 是针对单个热点数据,而缓存雪崩是针对大量数据。
  • 缓存击穿通常是由于缓存策略或数据热度不均导致的,缓存雪崩则是由于缓存数据的过期策略导致的集体失效。

共同点:

  • 缓存击穿和缓存雪崩都会导致在缓存中找不到数据,大量请求直接打到数据库,可能导致数据库压力过大。

(二)如何解决缓存雪崩?

1. 随机初始化 TTL:

可以为每个缓存项设置一个随机的过期时间,避免它们同时过期。

2. 搭建 Redis 集群

针对 Redis 服务宕机,可以搭建 Redis 集群 ,让这些 key 平均地分布在不同的 Redis 节点上,提高服务的可用性

3. 添加降级限流策略

降级和限流是应对缓存问题的有效手段。

  • 降级 是指在后端服务不可用时,++主动降低服务的处理级别++ 或++关闭非核心功能++。
  • 限流是限制单位时间内的请求数量,防止过多的请求同时到达后端服务。

针对缓存雪崩问题,我们也可以给缓存业务添加降级限流策略,比如:

复制代码
1. 单体服务,可以在 Nginx 中设置限流规则
2. 微服务,可以在 Spring Cloud Gateway 中设置限流规则

注意:这些策略不仅适用于缓存雪崩,也适用于缓存击穿和缓存穿透场景。

4. 多级缓存策略

给业务添加多级缓存(本地缓存 + 远程缓存),提高缓存命中率,减少对数据库的压力。

例如:可以将本地缓存(Guava 或者 Caffeine)作为一级缓存,Redis 作为二级缓存。

四、相关面试题

1. 什么是缓存穿透?怎么解决?

缓存穿透,大概率是遭受了恶意攻击,有人不断查询一个数据库和缓存中都不存在的数据。

这种情况下,每次请求都会直接打到数据库,数据库也查不到数据,也不会将数据写入缓存。

这个不存在的数据每次请求都要到 DB 去查询,可能会导致数据库压力过重、挂掉。

解决方案的话,我们通常都会用布隆过滤器来解决它。

2. 介绍一下布隆过滤器

布隆过滤器主要的作用就是检索一个元素是否在一个集合中

它的底层主要就是一个位数组 ,里面用来存放二进制 0 或 1,以及多个哈希函数

位数组里一开始都是 0,当一个 key 来了之后经过多次 hash 计算出数据在数组中的位置,

然后就把数组中原来的 0 改为 1.

这样的话,在查找该 key 的过程中,如果数组中的这些位置都为 1,就能表该 key 存在。

它的缺点是: 由于哈希冲突,布隆过滤器可能会产生一定的误判。

这个误判是必然存在的,降低误判率就是增加数组的长度。

我们一般也可以通过代码设置误判率,大概不会超过 5%,5% 以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。

3. 什么是缓存击穿?怎么解决?

缓存击穿,针对的是热点数据

通常是 Redis 里的某个 key 在某个时间点过期的时候,恰好这个时间点又有对这个 key

有大量的并发请求过来,这些请求发现缓存过期就会都直接打到数据库上,造成数据库压力骤增,可能会瞬间把数据库压垮。

解决方案有两种方式:

  • 第一是使用互斥锁: 当缓存失效时,不立即去 load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db 的操作并回设缓存,否则重试 get 缓存的方法
  • 第二是可以设置当前 key 逻辑过期 ,大概是思路如下:
    • ① 在设置 key 的时候,设置一个过期时间字段一块存入缓存中,不给当前 key 设置过期时间
    • ② 当查询的时候,从 redis 取出数据后判断时间是否过期
    • ③ 如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新

当然两种方案各有利弊:

  • 如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,也有可能产生死锁的问题。
  • 如果选择 key 的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据同步这块做不到强一致。

4. 什么是缓存雪崩?怎么解决?

缓存雪崩,就是设置缓存时采用了相同的过期时间,导致大量 key 在某一时刻集体失效,

或者是 Redis 服务宕机,导致大量请求全部打到数据库,导致数据库的瞬时压力过重雪崩。

缓存雪崩与缓存击穿的区别:

  • 雪崩是很多 key同一时刻大面积失效;
  • 击穿是针对某一个热点数据的 key

解决思路就是将这些 key 的缓存失效时间分散开。

  • 比如可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
  • 或者搭建 Reids 集群,让这些 key 平均地分布在不同的 Redis 节点上。
  • 此外,还可以给缓存业务添加降级限流策略、多级缓存。

这就是缓存雪崩的一些解决方案。

相关推荐
喜欢流萤吖~2 小时前
Lambda 表达式
java
Dragon online2 小时前
数据分析师成长之路--从SQL恐惧到数据掌控者的蜕变
数据库·sql
ZouZou老师2 小时前
C++设计模式之适配器模式:以家具生产为例
java·设计模式·适配器模式
曼巴UE53 小时前
UE5 C++ 动态多播
java·开发语言
VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue音乐管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
一招定胜负3 小时前
navicat连接数据库&mysql常见语句及操作
数据库·mysql
程序员鱼皮3 小时前
刚刚,IDEA 免费版发布!终于不用破解了
java·程序员·jetbrains
阿海5743 小时前
备份mysql数据的shell脚本
mysql
热心市民蟹不肉3 小时前
黑盒漏洞扫描(三)
数据库·redis·安全·缓存