缓存穿透、缓存击穿、缓存雪崩

一、缓存架构图

 

(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值

当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;

但是这种方法会存在三个问题:

  1. 缓存NULL的时间太长NULL数据长时间得不到更新,会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
  2. 缓存NULL的时间太短达不到防止缓存击穿的效果
  3. 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;

(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)数据预热

  数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。

  1. 在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间(缓存数据的过期时间设置随机),让缓存失效的时间点尽量均匀,防止同一时间大量数据过期现象发生。
  2. 设置热点数据永远不过期

缓存穿透、击穿、雪崩总结

  1. 缓存穿透:查询一个不存在的值时,未命中缓存,直接落到了数据库。解决方案:接口校验、缓存NULL值、Bloom Filter。
  2. 缓存击穿:热点key,缓存过期,直击数据库。解决方案:设置永不过期、加锁互斥获取数据。
  3. 缓存雪崩:大量的缓存key在同一时间失效,导致大量请求落到数据库上。解决方案:redis高可用、限流降级、数据预热(随机过期时间、设置用不过期)。
  1. 在没有引入缓存的时候,我们请求的数据都是上数据库直接查询了。
  2. 引入缓存之后,我们在获取数据时会先去缓存看看有没有缓存数据,有直接返回,没有上数据库进行查询,然后设置到缓存中,再进行返回。
  3. 并不是所有的数据都要放到缓存中了:访问频率低的、读少写多的、一致性要求高的,这些就不要缓存了。
  1. 穿透不存在
  2. 击穿热点key
  3. 雪崩缓存挂了或者大量key失效
原文地址:https://www.cnblogs.com/mjtabu/p/12916332.html