缓存穿透、缓存击穿、缓存雪崩
一、缓存架构图
(1)在没有引入缓存的时候,我们请求的数据都是上数据库直接查询了。
(2)引入缓存之后,我们在获取数据时会先去缓存看看有没有缓存数据,有直接返回,没有上数据库进行查询,然后设置到缓存中,再进行返回。
(3)并不是所有的数据都要放到缓存中了:访问频率低的、读少写多的、一致性要求高的,这些就不要缓存了。
二、缓存穿透、击穿、雪崩
2.1 缓存穿透
缓存穿透:用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
通俗来讲:请求查询一个不存在的值,缓存中没有数据,导致请求直接命中数据库,数据库中也没有数据,这就是缓存穿透。
2.2 缓存击穿
缓存击穿:缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
通俗的讲:请求查询一个存在的值(热点key),缓存中有数据,缓存中数据的key刚好过期,导致请求直接命中数据库。(热点key,缓存过期,直击数据库)
常见于:秒杀(秒杀中的key就是热点key。)
一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
热点key在高并发下刚好过期,这些秒级请求直接打到数据库,如果数据库处理不过来,那就挂了。
2.3 缓存雪崩
我们先理解下雪崩的意思,大量雪体崩塌,这种自然想象就是雪崩。
大量雪对应缓存中的就是大量的key,崩塌就是缓存过期失效了或者缓存层出现了错误(挂了),这就造成缓存雪崩了,直接请求到数据库了。
缓存雪崩:缓存层出现了错误(可能是大量的key同一时间失效),不能正常工作了。于是所有的请求都会达到存储层,存储层的调用量会暴增(大量请求直接命中数据库),可能会造成存储层也会挂掉的情况。
Ps:击穿与雪崩的区别即在于击穿是对于某一特定的热点数据来说,而雪崩是全部数据。
三、缓存穿透、击穿、雪崩进阶理解
3.1 理解一
当“无key”的时候,也就是key不存在,这时候频繁请求,就会越过缓存,造成缓存穿透。
当“有一key”且很“火热”的时候,也就是热点key,这时候频繁请求,碰巧缓存过期了,就会造成缓存击穿。
当“有多key”的时候,也就是说大量的key,在同一时间失效了,欧侯,这时候就都请求到数据库了,这就会造成缓存雪崩了。
3.2 理解二
我们也可以这么理解,上面缓存的三种情况,就是在缓存在高并发下的并发问题。
高并发下,不停的访问一个不存在的key,造成请求绕过了缓存系统,请求落到了数据库,就造成了缓存穿透。
高并发下,不停的访问一个热点key,在缓存失效的时候,请求在此时落到了数据库,就造成了缓存击穿(缓存击穿,本来是有人挡着的,但是利用了缓存失效的间隙,进行了攻击)。
高并发下,大批量缓存的key同时失效,此时所有请求落到了数据库,造成数据库压力过大,这就是缓存雪崩。
四、缓存穿透、击穿、雪崩如何解决?
4.1 缓存穿透如何解决?
(1)在接口层增加校验
比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接return。
(2)缓存NULL值
但是这种方法会存在三个问题:
- 缓存NULL的时间太长,NULL数据长时间得不到更新,会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
- 缓存NULL的时间太短,达不到防止缓存击穿的效果。
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
![](https://img2020.cnblogs.com/blog/1751422/202005/1751422-20200519135447176-1401251862.png)
(3)布隆过滤器(Bloom Filter)
类似于哈希表的一种算法,用所有可能的查询条件生成一个bitmap,在进行数据库查询之前会使用这个bitmap进行过滤,如果不在其中则直接过滤,从而减轻数据库层面的压力。
4.2 缓存击穿如何解决?
(1)设置热点数据永远不过期。
(2)使用加互斥锁:对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//假设设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
4.3 缓存雪崩如何解决?
(1)redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
(2)限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
(3)数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。
- 在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间(缓存数据的过期时间设置随机),让缓存失效的时间点尽量均匀,防止同一时间大量数据过期现象发生。
- 设置热点数据永远不过期。
缓存穿透、击穿、雪崩总结
- 缓存穿透:查询一个不存在的值时,未命中缓存,直接落到了数据库。解决方案:接口校验、缓存NULL值、Bloom Filter。
- 缓存击穿:热点key,缓存过期,直击数据库。解决方案:设置永不过期、加锁互斥获取数据。
- 缓存雪崩:大量的缓存key在同一时间失效,导致大量请求落到数据库上。解决方案:redis高可用、限流降级、数据预热(随机过期时间、设置永不过期)。
- 在没有引入缓存的时候,我们请求的数据都是上数据库直接查询了。
- 引入缓存之后,我们在获取数据时会先去缓存看看有没有缓存数据,有直接返回,没有上数据库进行查询,然后设置到缓存中,再进行返回。
- 并不是所有的数据都要放到缓存中了:访问频率低的、读少写多的、一致性要求高的,这些就不要缓存了。
- 穿透不存在
- 击穿热点key
- 雪崩缓存挂了或者大量key失效