一文了解缓存雪崩,缓存穿透,缓存击穿

一文了解缓存雪崩,缓存穿透,缓存击穿在之前的文章 无处不在的缓存 我们对缓存系统进行了初步的介绍 并简要提及了缓存系统使用过程中可能存在的风险和注意事项 本文我们就来深入了解下这些问题 缓存雪崩定义 缓存雪崩 是指大量缓存同时过期 导致大量请求不命中缓存而是直接查询数据库

大家好,欢迎来到IT知识分享网。

在之前的文章《无处不在的缓存》,我们对缓存系统进行了初步的介绍,并简要提及了缓存系统使用过程中可能存在的风险和注意事项。本文我们就来深入了解下这些问题。

缓存雪崩

定义

“缓存雪崩”是指大量缓存同时过期,导致大量请求不命中缓存而是直接查询数据库。这会给数据库带来很大的压力,并可能导致数据库崩溃。

解决方案

解决方案1:随机化过期时间

在介绍缓存预留模式(Cache Aside)时,我们为缓存统一设置了过期时间 60 秒,并在服务发布时预先初始化了热点key的缓存。

这样,在 60 秒后,热key的缓存将会同时过期,大部分流量会进入加载 DB 数据到缓存的逻辑。因此,我们可以尝试在缓存过期时间内设置一个随机数,比如60~120秒。这样,一些缓存就稍后点过期,从而分担一些将数据加载到DB的压力。

cacheService.set(key, data, 60 + new Random().nextInt(60));

随机时间范围的选择应根据业务实际情况而定。一般来说,如果过期时间设置得比较分散,可以明显降低缓存雪崩的概率。

解决方案2:热key数据不过期

由于缓存雪崩问题是由于缓存数据统一过期造成的,因此在更新缓存时,可以省略过期时间,改为更新DB时删除缓存的操作来更新缓存。但热key的整个更新操作需要加锁,否则可能会出现前面提到的数据不一致问题。

有人可能会问,如果DB更新成功,但是缓存更新失败,导致后续查询返回不到最新数据怎么办?

这里可以设计一个定时任务,比如每小时通过查询DB来刷新所有数据的缓存,保证之前更新失败的影响不会很大。

或者,当缓存更新失败时,可以发送异步消息,通过消费该消息来重试缓存。

不过,这种方式只适合部分企业。如果缓存数据过多,就会消耗大量的缓存资源。可以适当地进行一些选择。

缓存穿透

定义

当之前热点的缓存过期,导致大量并发请求时,就会发生缓存穿透。如果没有并发控制,它们都可能绕过if(data == null)判断条件,进入将DB数据加载到缓存的逻辑,给DB带来巨大 压力,甚至导致DB崩溃。

解决方案

缓存穿透实际上可以理解为缓存雪崩的一个特例,因此提出的方案可以在缓存雪崩的解决方案的基础上进一步优化。

解决方案1:加锁和排队

随机化过期时间可以很大程度上降低不同缓存key同时过期的概率。基于该方案,可以在某个key读取缓存失败后加载DB的逻辑中加入并发控制,可以更好的解决缓存雪崩和缓存穿透的问题。

这里通过一个例子来说明并发控制的逻辑。

假设某个key有1000个并发请求,其中有一个肯定会先获得锁,然后去DB加载数据并更新到缓存。剩下的999个请求会被锁阻塞,但不会直接报错,而是会设置一个等待时间,比如2s秒。

当请求1更新缓存并释放锁时,剩下的999个请求将一一排队获取锁。当请求2获得锁并进入加载逻辑时,由于请求1已经更新了缓存,所以实际上可以再次查询缓存。如果找到查询,可以直接返回,无需再次加载数据。

然而,在2s秒的等待时间内,只能处理199个请求。剩下的800个请求会进入获取锁失败的过程。事实上,请求1已经更新了缓存,所以获取锁失败的过程就是再次查询缓存。如果有数据则返回,否则报错提示系统忙。

为了更好地理解代码,以下是参考示例:

public class DataCacheManager { public Data query(Long id) { String key = "keyPrefix:" + id; //查询缓存 Data data = cacheService.get(key); if(data == null) { try { cacheService.lock(key, 2); //先检查缓存 data = cacheService.get(key); if(data != null) { return data; } //查询DB data = dataDao.get(id); //更新缓存 cacheService.set(key, data); } catch (tryLockTimeoutException e) { //超时后,再次检查缓存,如果存在则返回,否则会出现异常。 data = cacheService.get(key); if (data != null) { return data; } throw e; } finally { cacheService.unLock(key); } } return data; } }

解决方案2:热点数据永不过期

同样,缓存穿透问题也是由缓存过期引起的,可以通过将热点数据的缓存设置为永不过期来避免。

缓存击穿

定义

缓存击穿是指既不在缓存中又不在数据库中的数据。但用户仍然发起大量的请求,导致每个请求都去请求数据库,从而导致数据库不堪重负。

一般情况下,如果按照标准的页面操作,不太可能会出现这种情况。例如,你在电商首页或列表页点击的商品必须是存在的。但有人可能会伪造请求,将goodsId改为 0,然后发起大量请求,目的就是搞垮我们的系统,所以要提前预防悲剧的发生。

解决方案

解决方案1:参数验证

对客户端发送的请求进行参数校验,如果不满足校验条件,则直接拦截。例如,产品ID不会小于0,以防止请求到达下游。

该解决方案不能完全防止渗透。如果传递的是正常的数字,比如goodsId=,而该商品不存在,则无法完全防止渗透。

解决方案2:缓存空值

由于请求的是DB中不存在的数据,因此通过设置一个值(例如“null”)并确保其与正常数据不一致,将此类数据存储在缓存中就足够了。在后续查询中,如果标识为“null”,则可以简单地返回null。

不过最好设置合理的过期时间,因为key对应的数据不一定总是为null,也会占用缓存空间。

解决方案3:布隆过滤器

以前的文章我们介绍过布隆过滤器,原理很容易理解。它是一个使用很少的内存来确定大量数据“肯定不存在还是可能存在”的数据结构。

我们可以将缓存穿透的查询数据条件哈希到足够大的布隆过滤器上。该请求首先会被布隆过滤器拦截。不存在的数据将被直接拦截并返回,从而避免下一步对DB的压力。可能存在的数据可以到DB进行实际查询,但概率很小,所以影响很小。

备份解决方案:数据库速率限制和降级

归根结底,缓存雪崩、穿透、击穿这三个问题,都归结为数据库压力过大。因此,核心问题在于如何保证数据库不会过载。

从数据库的角度来看,它无法控制确切的请求数量。它肯定不会完全相信你做出的优化保证。毕竟,如果你编写的代码中存在错误怎么办?最安全的办法就是给自己设置保护,这个保护就是限速。

假设你设置了 1 秒内通过 1,000 个请求的速率限制。如果此时有2000个请求进来,前1000个会正常执行,接下来的1000个会触发限速。你可以实现配置的降级逻辑,例如返回一些配置的默认值或给出友好的错误消息。这里最好有针对性的限速,比如对热表的读请求有单独的限速配置,保证其他正常请求不会受到限制。

热点数据及淘汰策略

服务器使用的缓存大多通常将数据存储在内存中,以保证缓存的执行速度。然而,由于硬件和成本的限制,内存容量无法像磁盘一样近乎无限地扩展。当实际数据量非常大,无法存入缓存时,我们需要保证缓存中有限的数据命中尽可能多的请求。换句话说,缓存应该存储热点数据。

这里就有一个必须面对的问题:当数据量很大,而缓存容量有限时,如果容量满了怎么办?

做出适当的牺牲!

在实现缓存时,必须有一种机制来保证内存中的数据不会无限增加,即数据淘汰机制。数据淘汰机制是一个成熟的缓存系统必须具备的基本能力。需要澄清的是,数据淘汰策略和数据过期是两个概念。

  • 数据过期:数据过期是缓存系统的标准逻辑,也是满足业务期望的数据删除机制。也就是说,设置了过期日期的缓存数据在过期日期之后将从缓存中删除。
  • 数据淘汰:数据淘汰是缓存系统的一种“有损自保”降级策略,是一种超出业务预期的数据删除方式。是指当存储的数据尚未达到过期时间,但缓存空间已满,想要添加新数据到缓存时,缓存模块需要实施的一种应对策略。

我们可以把缓存想象成一个容器。那么,当这个容器已经满了,我们还想往里面放东西时,我们该怎么办呢?只有两个办法:

第一种方法是直接拒绝,因为容器已经满了,无法容纳更多的东西。

一文了解缓存雪崩,缓存穿透,缓存击穿

第二种方法是扔掉容器中的一些现有内容,为新内容腾出空间。

一文了解缓存雪崩,缓存穿透,缓存击穿

进一步的考虑表明,当决定首先从容器中删除一些现有内容时,必须做出一个新的决定:应该删除哪些内容?实践中,常用的解决方案有以下几种:

  • 随机选择:一切听天由命,容器中已有的一些内容会被随机移除。
  • 按需求排序,保留经常使用的项:基于LRU(最近最少使用)策略,去除最长时间未使用的数据。
  • 提前过期并淘汰:对于一些有过期时间的记录,按照过期时间排序,剔除最近即将过期的数据(类似提前过期)。

除了上述常见的策略外,在实现缓存时还可以根据业务场景构建自定义的淘汰策略。例如,基于创建日期、上次修改日期、优先级、访问次数等。

一些主流缓存中间件的淘汰机制大多遵循上述方案。例如,Redis提供了多达6种不同的数据淘汰机制供用户根据需要进行选择,利用有限的空间只存储热点数据,最大化缓存的价值。如下:

一文了解缓存雪崩,缓存穿透,缓存击穿

从上图可以看出,Redis更加精准地实现了随机淘汰和LRU策略,支持淘汰目标范围为所有数据以及有过期时间的数据。这种策略相对来说比较合理,因为有过期时间的数据本质上是可移除的,直接剔除不会对业务产生逻辑上的影响。相比之下,没有过期时间的数据通常需要驻留在内存中,并且往往是一些配置数据或需要作为白名单的数据(例如用户信息,如果用户信息不在缓存中,则意味着该用户不存在)。如果强行删除这些数据,可能会导致业务层面的一些逻辑异常。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/171014.html

(0)
上一篇 2025-02-21 11:26
下一篇 2025-02-21 11:33

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信