残月的技术日志

残月的技术日志

Redis中的缓存穿透、击穿、雪崩问题

2024-10-18

缓存穿透、击穿、雪崩,是缓存中非常常见的三个问题(面试也很爱问)。

在介绍之前,我们先来看下,当一个系统中引入Redis作为缓存,数据是怎么查询的。

通常就长图中这样,我们来看:

  1. 首先 我们的服务会先向缓存(Redis)查询,如果缓存命中(也就是缓存里面有),就直接在缓存中返回

  2. 要是缓存没命中,就去向数据库查询

  3. 数据库将数据返回,并将数据缓存到Redis中

OK 很完美 这就像硬盘的缓存一样,加速了热点数据的查询效率

缓存穿透

缓存穿透: 指的是当查询一个缓存中不存在的数据时,此时查询的操作就会打到数据库中,由于数据查询的结果为空,且此时数据库查询到的数据不会被缓存

这就导致了什么问题,欸,这个缓存有跟没有一样,每次的请求都会查询数据库。

一个两个倒还好,要是请求达到一定的量,就有可能导致数据库服务宕机

那什么情况会出现缓存穿透,举个例子:

当一个电商系统,我查询一个不存在的商品的ID,这时候,数据库中就不存在这个商品,也就返回空值

为什么会出现查询一个不存在的商品的这种不正常的情况呢:

  • 可能是由于开发人员的疏忽,导致发出错误的请求

  • 或者有可能是因为数据更新的不及时,导致已经被删除的ID又被查询

  • 最大的可能就是遭受恶意攻击

当有心之人获取到你的接口地址,得知了接口的参数,就可以通过某些脚本,向接口发送了大量的带着错误的参数的请求,哦吼

解决方法

有没有办法解决,有!

缓存空对象:

这个是网上讲的最多的方法,也就是当数据库返回空数据时,照样对空数据进行缓存

但有个问题,要是有心之人传递的参数是随机值,那缓存中还是不会命中,且大量的缓存也会浪费内存

即使不是恶意攻击,要是在第一次查询缓存空对象后,数据库中插入了对应的数据,那就会导致数据不一致

所以,我不建议对缓存中的空对象设置太长的过期时间

布隆过滤器:

什么是布隆过滤器:布隆过滤器通常采用一种空间效率极高数据结构存储,用于测试一个元素是否在集合中

对于缓存穿透问题,我们可以在缓存预热阶段,将ID存储在布隆过滤器中

在对Redis进行查询之前,先查询布隆过滤器,若布隆过滤器不存在这个ID,就直接返回Null,不往下走

这不仅仅环节了数据库的压力,也减低了Redis的压力,真好

布隆过滤器的事项方法有很多,例如guava库中就有相应的实现,本文不深入讨论

我们来讲讲缺点:首先,布隆过滤器的实现是会比缓存空对象要复杂一点的

而且,布隆过滤器本身是存在误算的(当然也容许误算的存在),我这拿黑马课程中的一张图举例

id1和id2通过多个哈希函数进行计算,得到多个位置索引,但id3(数据库中不存在)多次计算后的结果与id1、id2对上了,也就是说,在布隆过滤器中,会判断id3存在

对于这种问题,通常可以通过加大数组的大小来缓解(大了就不会那么巧对上了),但随之而来的也就内存消耗的增加(现实中的数组不会像示意图这么小),在一些库中,通常误算率可以设置

当然方法不止这些,还有些通用的方法我放到最后

缓存击穿

缓存击穿: 缓存击穿是指一个热点Key在缓存中过期的一瞬间,在缓存重建的这个间隙,同时有大量的请求访问这个Key,由于缓存过期,这些请求都会打到数据库上,从而可能导致数据库压力剧增,甚至崩溃

击穿也就是像手中的盾突然间碎了,缓存起不到对数据库"保护"的作用

解决方法

互斥锁:

解决方法其实也很明显,大量请求,我只让一个线程去访问数据库,其他的等缓存有了再拿缓存不就好了

欸,互斥锁就能保证只有一个线程去查询数据库

来了个线程一,看到Redis中没有缓存,就添加一个互斥锁,然后向数据库中查询数据

此时来了个线程二,Redis中还没缓存,它也去向数据库查询,但发现互斥锁没有解锁,那就等着,一段时间看看Redis中有缓存了没有

等线程一重建了缓存,线程二重试的时候就拿到了,也就不会去访问数据库了

缺点也很明显,当缓存过期时,其他线程要等待缓存重建,比较适合对数据有强一致性的场景

逻辑过期:

逻辑过期,那就不能和之前一样再Redis中设置过期时间了

我们可以在Value中多存储一个属性,表示过期时间

这时候线程去访问Redis,就肯定拿得到数据(Key对的话),拿到数据后,判断过期时间是否小于当前系统时间

要是小于,就去向数据库发起请求,拿到新的数据并更新缓存,新数据的过期时间肯定也是一个全新的啦

欸,发现个问题,此时要是多个线程同时拿到的数据都是过期的,拿岂不是又要大量的请求数据库

所以,逻辑过期需要配合互斥锁一起使用,好处是其他线程可以不等待锁释放,可以返回拿到的过期的数据

缺点也很明显,线程返回的数据可能不是实时的数据,这种策略通常用在高可用的场景中,性能好

部分热点数据永不过期:

这个有点狠,对于部分不会变动的热点数据,我们设置他永不过期,通常很少用这种

缓存雪崩

缓存雪崩:是指缓存中大量数据同时过期,或者缓存服务宕机,导致大量请求直接落到数据库上,使数据库压力过大,甚至崩溃。

都说雪崩时,没有一篇雪花是无辜的,所以对于这些不同Key的缓存丢失,我们都要照顾到

解决方法

设置不同的过期时间:

针对同时过期,我们可以想方法然他们岔开

这么做,很简单,在原有TTL上加上一个随机数,具体多少以业务位准

嘿,就这么简单,既不对原有过期时间的设计产生太大影响,也岔开了这些数据的过期时间,完美

Redis集群部署:

一台挂了,可能性比较大,集群中多太都挂了,这概率就小不少,这我就不多说了,以后要是想起来的话单独再写篇文章

还有一些通用的方法

其实仔细看刚刚的三个问题,都有个共同点,大量的请求

对于这种高并发场景,也不一定要从Redis上下手

我们可以从接口的角度思考,用Sentinel给接口限流、熔断,用hystrix做降级,都是解决思路,不妨融合些其他技术,或许有新的解决方案。

The End

参考资料:

https://www.bilibili.com/video/BV1yT411H7YKhttps://blog.csdn.net/qq_41125219/article/details/119982158https://www.cnblogs.com/liyulong1982/p/6013002.html

  • 0