缓存穿透、缓存雪崩、缓存击穿三大问题介绍及解决思路

缓存穿透

概念

​ 我们知道缓存的应用场景之一是一部分数据经常被访问,如果这部分数据只存放在数据库中的话,每次访问都要向数据库发送请求,大量并发下数据库压力就会非常大,可能导致查询效率低甚至是数据库崩溃。而缓存的使用相当于将这部分经常访问的数据放在缓存中,这样就会客户端请求就会直接指向缓存,大大减轻数据库的压力,当然这种效果只是缓存使用的一部分原因,缓存还有很多用处。

​ 缓存穿透是指客户端请求的数据在缓存中和数据库中均不存在,首先缓存永远不会生效,这些请求就会交给数据库处理,而数据库也没有这些数据,也就无法将数据放到缓存中,缓存就无法起到作用了。这些请求相当于穿透了缓存,直接打向了数据库,这就是缓存穿透。

解决方案

  1. 缓存空对象:当我们客户端访问不存在的数据时,哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,即value设为null,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了。

    ​ 这种方式实现起来比较简单,缺点是容易造成数据短期的不一致,因为可能数据库中有相关数据了,但是缓存中还存储的null,但是我们在向缓存中设置数据的时候是会设置过期时间的,等过期时间到了,数据也就更新到正常值了。

  2. 布隆过滤:布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,假设布隆过滤器判断这个数据不存在,则直接返回。

    ​ 这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要是哈希思想,就可能存在哈希冲突。

缓存雪崩

概念

     缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。什么叫大量缓存key同时失效呢?简单来说,就是大量key的过期时间同时到达,即缓存中相应数据已经删除,这时候大量请求就会略过缓存直达数据库,使数据库瞬间面临巨大压力,就像发生雪崩了一样。

解决方案

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿

概念

​ 缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

解决方案

  1. 互斥锁

    ​ 因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行。

    ​ 假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。

  2. 逻辑过期

    ​ 我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。

    ​ 我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程会开启一个 线程去进行以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。

    ​ 换句话说,用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。

    ​ 这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。