跳到主要内容

Redis 缓存穿透,击穿,雪崩

· 阅读需 5 分钟

穿透、击穿、雪崩,是用 Redis 做缓存时绕不开的三类问题。名字相近,成因和应对却各不相同,这篇把它们逐个说清楚。

缓存穿透

缓存穿透是指用户请求的数据在缓存中不存在即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍,然后返回空。

如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至击垮数据库系统。

解决方案

1)布隆过滤器

布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数,可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好得多,缺点是有一定的误识别率,且删除困难。

如果想判断一个元素是不是在一个集合里,一般的思路是把所有元素保存起来,再通过比较确定,链表、树等数据结构都是这种思路。但随着集合中元素的增加,需要的存储空间越来越大,检索速度也越来越慢(O(n) 或 O(log n))。而散列表(哈希表)可以通过一个 Hash 函数把元素映射到位阵列(Bit array)中的一个点,这样只要看这个点是不是 1,就能知道集合中有没有它——这就是布隆过滤器的基本思想。

2)返回空对象

当缓存未命中、查询数据库也为空时,可以把返回的空对象写进缓存,这样下次请求该 key 时直接从缓存返回空对象,请求不会落到数据库。为了避免存储过多空对象,通常会给空对象设置一个过期时间。

这种方法存在两个问题:

  1. 如果有大量的 key 穿透,缓存空对象会占用内存空间。
  2. 在 key 过期之前,这段时间内可能存在缓存和数据库数据不一致的场景。

缓存击穿

当某个热点 key 在过期的瞬间,有大量请求同时访问这个 key 的数据。由于缓存刚好失效,这些请求会同时打到持久化的数据库去查询,并把数据回写到缓存,导致数据库瞬间压力过大,这就是击穿。

此处可以理解 击穿穿透 的区别:

击穿,是一个 key 非常热点,大量的访问都打在这个 key 上面,在 key 失效的瞬间,所有请求打在数据库上,就打出一个洞,击穿了。而穿透更多的是访问的数据不存在的情况,大量的请求访问的都是不存在的数据。

解决方案

1)使用互斥锁

让第一个访问的线程去查库并回写缓存,其他线程等待回写完成后重新读取缓存即可。

2)设置热点数据永不过期

对访问极高的热点数据不设置过期时间,改由业务侧异步更新缓存内容。

缓存雪崩

缓存雪崩是指缓存中大批量的热点 key 同时到达过期时间,而查询数据量巨大,请求直接落到数据库上,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到,从而都去查数据库。

解决方案

1)均匀过期

设置不同的过期时间,让缓存失效的时间点均匀分布。通常可以为有效期增加随机值,或者统一规划有效期。

2)加互斥锁

跟缓存击穿的解决思路一致,同一时间只让一个线程构建缓存,其他线程阻塞排队。

3)缓存永不过期

跟缓存击穿的解决思路一致,缓存在物理上永远不过期,用一个异步线程更新缓存。

4)双层缓存策略

使用主备两层缓存:

  • 主缓存:有效期按照经验值设置,作为主读取的缓存,主缓存失效后从数据库加载最新值。
  • 备份缓存:有效期长,获取锁失败时读取的缓存,主缓存更新时需要同步更新备份缓存。

小结

三个问题的本质都是请求绕过缓存直接打到数据库:穿透是查不存在的数据,击穿是单个热点 key 失效,雪崩是大批 key 同时失效。应对思路也有共性——要么在缓存层把无效请求拦下来(布隆过滤器、空对象),要么控制回源的并发(互斥锁),要么让失效时间可控(均匀过期、永不过期、双层缓存)。实际使用时按数据的热点程度和一致性要求组合选用即可。

评论 / COMMENTS