【面试题】缓存穿透、缓存击穿、缓存雪崩是什么?如何解决?

视频文稿
要解答这个问题,首先我们必须先理解缓存中间件的一般使用逻辑。
缓存的使用逻辑,主要就是在访问数据库前,先在缓存中查询。缓存中不存在的才需要去数据库查找。那么这样的好处就是:(1)可以缓解数据库的压力;(2)缓存性能优于数据库的话,可以提高我们的查询速度。
缓存穿透
那么什么是缓存穿透(Cache Penetration)?缓存穿透主要是指我们查询一个缓存和数据库中都不存在的数据,而系统设计时不做任何特别设计的话,将会导致每次查询都将穿透缓存打在数据库上。而因为数据库中也不存在,最后返回的结果将是空。但整个过程都将消耗数据库的资源,这其实是没有必要的。当用户对这种不存在的数据大量查询时,就很可能最终因为数据库作为性能瓶颈而导致服务崩溃。
缓存穿透的关键就是指出了请求数据不存在时的潜在风险。
解决方案
一般是两种:(1)缓存空对象;(2)布隆过滤器
缓存空对象好理解,其实就是第一次查询结果为空的时候,我们就在缓存中把该结果缓存下来。这样后续的访问就将在缓存层直接返回空结果,而不会打到数据库上。
该方案需要注意的就是:(1)最好给这种空结果缓存设置过期时间,否则大量请求不存在的数据会使得我们缓存的内存空间被它们的空结果缓存占用且得不到释放。(2)当数据库对这些原本不存在的数据进行记录时,需要记得更新缓存或者清除空结果缓存,否则会导致缓存不一致。
第二个方案是布隆过滤器。布隆过滤器这里先简单介绍一下。它的作用主要就是判断一个数据在它内部是否存储过:如果存在,它一定会返回存在。如果不存在,它大概率返回不存在,但有一定概率会误判返回存在,且这个概率随着数据增加而增大。大概原理可以理解成在布隆过滤器中有一个数组(其实也就是位图),初始值都为 0。任意一个数据,我们存储时会把用多个 hash 函数把它计算出多个 hash 值,并把那些 hash 值对应索引位置置 1。那么判断存在时,就是去同样多个索引处看是否全部为 1 即可。也正因为此,存在一定概率这些索引是被其他元素置 1 的,导致误判存在。简单一句话概括,布隆过滤器可以理解成一个不精确的 Set,不精确在于说存在不一定存在,说不存在一定不存在。
我们布隆过滤器的解决方案,其实就是在查询缓存层前引入一个布隆过滤器。这样我们就可以在查询前就通过布隆过滤器来判断数据的存在性了。而布隆过滤器占用的空间会比一般的 Set 小,就相比之下更适合这个场景。
但这个方案需要注意的就是:(1)布隆过滤器也存在误判的可能,所以少量特定数据的请求依然会穿透。只是这个概率在数据少的时候可以忽略不计。一旦数据大时,我们就必须扩大布隆过滤器的位图大小和增加 hash 函数的数量,否则误判率就会急剧上升。(2)传统的布隆过滤器不支持删除(因为是基于位图实现),所以在缓存数据会被删除场景下就需要使用变种(比如基于计数器实现的布隆过滤器,但占用空间就更大了),或者放弃该解决方案。
缓存击穿
那么什么是缓存击穿(Cache Breakdown)呢?缓存击穿的重点是指缓存中的热点数据过期时的问题。当热点数据过期时,大量请求将直接穿过缓存打在数据库上。因为本身是热点数据,访问量大,这样极可能导致数据库的压力突然暴增,导致性能瓶颈甚至服务崩溃。
解决方案
也是两种:(1)对于热点数据,令其永不过期;(2)用锁确保数据缓存重新加载时只有一个请求进入数据库。
不过期的实现方式可以是直接不设置过期时间,也可以是在过期前主动去更新缓存。
确保数据缓存重新加载时只有一个请求进入数据库,其实就是确保在第一个请求使得热点数据缓存重新加载前其他请求不进入数据库。单机通过 synchronized 或 Lock 来处理,分布式环境采用分布式锁。这样第一个请求完成后,其他请求就可以在缓存中直接读取结果。这样也就避免了大量请求同时打到数据库的情况。
缓存雪崩
缓存雪崩(Cache Avalanche)主要是指缓存中大量的 key 在同一时刻过期,或者缓存中间件直接挂掉,导致大量请求直接到达数据库,导致数据库查询压力突增,从而形成一系列的连锁反应,造成系统崩溃等情况。
解决方案
对于大量 key 同时过期的情况,我们可以在设置过期时间时加上一个随机数,这样把过期时间打散,使其不在同一时间失效。
而第二种缓存中间件故障的情况,就需要注意保证其高可用性。可以采用集群、主从、哨兵等各种方式提高缓存可用性。服务层也可以引入服务熔断、限流、降级等措施。