一文告诉你面试的时候要怎么回答缓存击穿、穿透、雪崩问题

缓存击穿 缓存穿透 缓存雪崩

1.缓存击穿是什么?怎么解决

分析:

如果缓存中的某个热点数据过期了、此时大量的请求访问了该热点数据、就无法从缓存中读取、直接访问数据库,数据库很容易就被高并发的请求冲垮。

缓存击穿的关键是热点数据不存在于缓存中、因为如果是普通数据、那就是正常的缓存未命中、反之热点数据不在缓存中了、容易导致大量请求落到数据库上。

缓存击穿解决手段:

  1. 设置热点数据的热度时间窗口:对于热点数据、可以设置一个热度时间窗口、在这个时间窗口内、如果一个数据被频繁访问、就将其缓存时间延长、避免频繁刷新缓存导致缓存击穿。Redis滑动窗口

  2. 使用互斥锁或分布式锁:在缓存失效时、只允许一个线程去查询数据库、其他线程等待查询结果。可以使用互斥锁或分布式锁来实现、确保只有一个线程能够查询数据库、其他线程等待结果、避免多个线程同时查询数据库造成数据库压力过大。

  3. 异步更新缓存:在缓存失效时、可以异步地去更新缓存、而不是同步地去查询数据库并刷新缓存。这样可以减少对数据库的直接访问、并且不会阻塞其他请求的响应。

  4. 缓存预热:通过在缓存失效前、提前更新缓存、避免缓存失效时多个请求访问数据库。

  5. 缓存永不过期或延迟失效:对于某些重要数据、可以设置永不过期的缓存或延迟过期时间来减少缓存击穿

  6. 缓存数据本身永不过期 但 在缓存中存储一个逻辑过期时间(而非物理过期时间)。当读取缓存时、通过逻辑时间判断是否需要异步更新数据。

逻辑过期 是一种缓存设计策略,它的核心思想是:缓存数据本身不设置物理过期时间(即永不过期)、但在缓存中存储一个逻辑过期时间字段。通过判断逻辑过期时间来决定是否需要更新数据、而不是依赖缓存系统的自动过期机制。

  1. 缓存数据时、存储数据 + 逻辑过期时间(例如 expire_time)。

  2. 读取缓存时:

    1. 若当前时间 < expire_time:直接返回缓存数据。

    2. 若当前时间 ≥ expire_time:触发异步更新(如通过消息队列或后台线程) 立即返回旧数据 避免阻塞用户请求。

  3. 异步更新成功后,更新缓存数据和逻辑过期时间。

补充知识:

什么是逻辑过期

逻辑过期是一种缓存设计策略,它与传统的物理过期不同。

  • 物理过期: 缓存系统(如 Redis)为每个缓存项设置一个过期时间。一旦到达这个时间、缓存系统会自动删除该缓存项。

  • 逻辑过期: 缓存系统不设置过期时间(或设置一个很长的过期时间)、缓存项永远不会被自动删除。但是缓存项中会包含一个额外的字段、表示该数据的逻辑过期时间。

逻辑过期如何工作?

1.缓存数据时:

  • 将数据存储到缓存中。
  • 同时存储一个逻辑过期时间(expire_time)、表示该数据被认为是有效的截止时间。

读取缓存时:

  • 检查缓存中是否存在该数据。
  • 如果存在、比较当前时间与 expire_time
  • 当前时间 < expire_time 说明数据仍然有效、直接返回缓存数据。
  • 当前时间 >= expire_time 说明数据已经逻辑过期、但仍然返回旧数据。 同时触发一个异步更新操作、去数据库中获取最新的数据并更新缓存。

2.步更新:

  • 异步更新操作(例如通过消息队列或后台线程)从数据库中获取最新的数据。
  • 更新缓存中的数据、并更新 expire_time 为新的逻辑过期时间。

逻辑过期如何解决缓存击穿

  • 避免大量请求同时访问数据库: 当缓存逻辑过期时、仍然返回旧数据、避免了大量请求同时穿透到数据库。

  • 保证可用性: 即使缓存数据已经逻辑过期、仍然可以提供服务、保证了系统的可用性。

  • 异步更新保证最终一致性: 通过异步更新、最终会保证缓存中的数据与数据库中的数据一致。

逻辑过期的优点:

  • 高可用性: 即使缓存过期、仍然可以提供服务。

  • 避免缓存击穿: 减少了对数据库的直接访问。

  • 平滑过渡: 异步更新可以平滑地更新缓存、避免了突然的性能下降。

逻辑过期的缺点:

  • 数据不一致: 在异步更新完成之前、缓存中的数据可能不是最新的。

  • 实现复杂: 需要额外的逻辑来处理过期时间和异步更新。

  • 需要考虑异步更新失败的情况: 需要设计重试机制或补偿机制来处理异步更新失败的情况。

举例说明:

假设有一个商品信息、逻辑过期时间设置为 1 小时。

  1. 初始状态: 商品信息被缓存、expire_time 设置为当前时间 + 1 小时。

  2. 1 小时后: 当有请求访问该商品信息时、发现当前时间已经超过了 expire_time

  3. 逻辑过期处理:

  • 立即返回旧数据: 仍然返回缓存中的商品信息、保证用户可以立即看到数据。
  • 触发异步更新: 启动一个异步任务、从数据库中获取最新的商品信息。
  • 异步更新完成: 异步任务从数据库中获取到最新的商品信息、并更新缓存中的数据、同时将 expire_time 设置为当前时间 + 1 小时。

缓存击穿解决方案:

  • 事前预防 (Proactive):

    • 缓存预热 (Cache Pre-warming): 在系统启动或缓存失效前、提前加载热点数据到缓存中。

    • 缓存永不过期或延迟失效 (Eternal/Delayed Expiry):对于非常重要的热点数据、可以设置永不过期或延迟过期时间。

    • 逻辑过期 (Logical Expiry): 缓存数据本身永不过期、但在缓存中存储一个逻辑过期时间。

    • 当读取缓存时通过逻辑时间判断是否需要异步更新数据。

  • 事中处理 (Reactive):

    • 互斥锁分布式锁 (Mutex/Distributed Lock): 当缓存失效时、只允许一个线程去查询数据库并更新缓存,其他线程等待结果。

    • 异步更新缓存 (Asynchronous Cache Refresh): 在缓存失效时、异步地去更新缓存、而不是同步地查询数据库并刷新缓存。

    • 热度时间窗口 (Hot Data Time Window): 动态调整热点数据的缓存过期时间、延长频繁访问数据的缓存时间。



2.缓存击穿面试参考回答

面试官问:缓存击穿是什么? 解决方案是什么?

缓存击穿就是如果缓存中的某个热点数据过期了、此时大量的请求访问了该热点数据、就无法从缓存中读取、直接访问数据库、就会造成数据库的压力过大。解决办法有事前预防 事后处理的方案

像事前预防可以实现缓存预热、将热点的数据提前加载到缓存中。

也可以设置一个逻辑过期时间、也就是说缓存数据本身不设置物理过期时间、缓存项永远不会被自动删除但在缓存中存储一个逻辑过期时间字段、在读取缓存时检查缓存中是否存在该数据。如果存在则比较当前时间与

逻辑过期时间、若当前时间 < 逻辑过期时间 说明数据仍然有效、直接返回缓存数据。若当前时间 >= 逻辑过期时间 说明数据已经逻辑过期、但仍然返回旧数据。 同时触发一个异步更新操作、可以使用一个异步任务去数据库中获取最新的数据并更新缓存。

然后在事后处理、请求已经到达了数据库、我们可以使用互斥锁、在缓存失效的时候只允许同一个线程去查询数据库数据并更新缓存、其他线程等待结果。当这个线程结束了之后锁释放了、其他线程就读取缓存、避免了缓存对数据库造成压力。

3.缓存穿透是什么、怎么解决

分析:

当用户访问的数据、既不在缓存中、也不在数据库中、导致请求在访问缓存时、发现缓存缺失、再去访问数据库时、发现数据库中也没有要访问的数据、没办法构建缓存数据、来服务后续的请求。

那么当有大量这样的请求到来时、数据库的压力骤增、这就是缓存穿透的问题。

最常见的场景就是有攻击者伪造了大量的请求、请求某个不存在的数据。这会造成两个后果:

  • 缓存里面没有对应的数据、所以查询会落到数据库上。

  • 数据库也没有数据、所以没有办法回写缓存、下一次请求同样的数据、请求还是会落到数据库上。

缓存穿透解决手段:

  1. 回写特殊值:在缓存未命中、而且数据库里也没有的情况下、往缓存里写入一个特殊的值。这个值就是标记数据不存在、那么下一次查询请求过来的时候、看到这个特殊值、就知道没有必要再去数据库里查询了。但如果如果攻击者每次都用不同的且都不存在的 key来请求数据、那么回写特殊值这种手段会丧失它的效果、而且因为要回写特殊值、会浪费不少 Redis 的内存。这可能会进一步引起另外一个问题、就是 Redis 在内存不足、执行淘汰的时候、把其他有用的数据淘汰掉、而更好的就是考虑使用布隆过滤器。

  2. 解决缓存穿透的通用方案是: 给所有指定的 key 预先设定一个默认值、比如空字符串Null、当返回这个空字符串Null时,我们可以认为这是一个不存在的 key、在业务代码中、就可以判断是否取消查询数据库的操作、或者等待一段时间再请求这个 key。如果此时取到的值不再是Null、我们就可以认为缓存中对应的 key 有值了,这就避免出现请求访问到数据库的情况,从而把大量的类似请求挡在了缓存之中。

  3. 使用布隆过滤器:布隆过滤器是一种快速判断元素是否存在的数据结构、它可以在很小的内存占用下、快速判断一个元素是否在一个集合中。将所有可能存在的数据哈希到一个足够大的位数组中、当一个请求过来时、可以先通过查询布隆过滤器快速判断数据是否存在、如果不存在、则直接返回、避免对数据库的查询。

  4. 限流策略:针对频繁请求的特定数据,可以设置限流策略,例如使用令牌桶算法或漏桶算法,限制对这些数据的请求频率,减轻数据库的压力。

缓存穿透事前事中解决方案

一、缓存穿透解决方案:

  • 事前预防 (Proactive):

    • 布隆过滤器 (Bloom Filter): 这是最典型的事前预防方案。在请求到达缓存之前、先通过布隆过滤器判断 key 是否可能存在。如果布隆过滤器判断不存在、则直接返回、避免访问缓存和数据库。

    • 缓存预热 (Cache Pre-warming): 虽然主要用于解决缓存击穿、但也可以一定程度上预防缓存穿透。如果预先加载了所有可能的 key 到缓存中,就能减少穿透的发生。

  • 事中处理 (Reactive):

    • 回写特殊值 (Null Value Caching): 当缓存和数据库都未命中时、将一个特殊值(例如 null 或空字符串)写入缓存。后续请求可以直接从缓存中获取该特殊值、避免再次查询数据库。

    • 通用默认值 (Default Value): 预先为所有指定的 key 设置一个默认值、例如空字符串Null。当返回这个空字符串Nul”时,我们可以认为这是一个不存在的 key、在业务代码中,就可以判断是否取消查询数据库的操作,或者等待一段时间再请求这个 key。

    • 限流策略 (Rate Limiting): 限制对特定 key 的请求频率、防止恶意攻击或大量无效请求冲击数据库。可以使用令牌桶、漏桶等算法。

以下是几种处理查到缓存失效时情况的策略

返回旧值 (Stale-While-Revalidate):

  • 原理: 当缓存失效时、立即返回缓存中的旧值、同时启动异步任务更新缓存。 客户端先拿到旧数据、稍后缓存更新后、下次请求就能拿到新数据。

  • 永不过期数据+逻辑过期 (Logical Expiry) + 异步更新:

  • 原理: 缓存数据本身永不过期、但在缓存中存储一个逻辑过期时间。 当读取缓存时、通过逻辑时间判断是否需要异步更新数据。

4.缓存击穿面试参考回答:

面试官问:缓存穿透是什么?解决方案是什么? 参考回答:缓存穿透就是在查询数据的时候既不在缓存中、也不在数据库中、导致请求在访问缓存时、发现缓存缺失、再去访问数据库时、数据库也没有数据无法构建缓存、那么当有大量这样的请求到来时、数据库的压力骤增。

解决缓存穿透、我们可以从事前预防和事后处理两个方面入手。

事前预防可以使用布隆过滤器、在请求到达缓存之前、我们使用布隆过滤器来快速判断请求的 key 是否可能存在于数据库中。如果布隆过滤器判断 key 一定不存在、那么就直接返回、避免了对缓存和数据库的无效访问。

然后事中处理的方案就是缓存空对象。当缓存和数据库都未命中时、我们不是直接返回错误、而是将一个空对象(例如 null 或一个特殊标记)写入缓存。第一次查询缓存和数据库中发现这个key不存在、那就以这个 key 存入缓存、value可以为null。这样后续请求就可以从缓存中获取到这个空对象、避免了每次都穿透到数据库。这个值就是标记数据不存在、那么下一次查询请求过来的时候、看到这个特殊值、就知道没有必要再去数据库里查询了。为了避免缓存中存在过多的空对象、我们可以给空对象设置一个较短的过期时间。

5.布隆过滤器是怎么工作的

布隆过滤器由初始值都为0的位图数组和N个哈希函数两部分组成。在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库中,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。

  • 第一步:使用N个哈希函数分别对数据做哈希计算,得到N个哈希值。

  • 第二步:将第一步得到的N个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。

  • 第三步:将每个哈希值在位图数组的对应位置的值设置为1。

举个例子,假设有一个位图数组长度为8,哈希函数数3个的布隆过滤器。

在数据库写入数据x后,把数据x标记在布隆过滤器时,数据x会被3个哈希函数分别计算出3个哈希值,然后在对这3个哈希值对8取模,假设取模的结果为1、4、6,然后把位图数组的第1、4、6位置的值设置为1。当应用要查询数据x是否数据库时,通过布隆过滤器只要查到位图数组的第1、4、6位置的值是否全为1,只要有一个为0,就认为数据x不在数据库中。

布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据x和数据y可能都落在第1、4、6位置,而事实上,可能数据库中并不存在数据y,存在误判的情况。

所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据。

布隆过滤器之所以会产生误判、是因为多个不同的元素经过哈希函数计算后,可能会映射到位数组中的相同位置,导致查询时误认为该元素存在。 误判率取决于位数组的大小、哈希函数的数量以及添加的元素数量。 可以通过增加位数组的大小和哈希函数的数量来降低误判率,但同时也会增加空间复杂度和计算复杂度。

正确的降低误判率的方法:

  • 增加位数组的大小 (m): 更大的位数组可以减少哈希冲突的概率。

  • 增加哈希函数的数量 (k): 更多的哈希函数可以更均匀地将元素映射到位数组中。

6.缓存雪崩是什么?怎么解决

分析:

缓存雪崩一般有两个常见场景、当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时、如果此时有大量的用户请求、都无法在 Redis 中处理、于是全部请求都直接访问数据库、从而导致数据库的压力骤增、严重的会造成数据库宕机、从而形成一系列连锁反应、造成整个系统崩溃、这就是缓存雪崩的问题。

场景1:缓存集中失效。比如为了双十一、将一批商品放入到缓存中、设置两小时过期、凌晨2点过期了、导致对这一批商品的访问都落到了数据库、数据库就会产生压力波峰。

场景2:当然缓存雪崩一个最严重特殊情况是、在流量高峰、一个缓存节点直接出现问题、甚至扩大到缓存集群出现问题、那么就会导致本应该访问缓存的流量透传到数据库上。

大量数据同时过期的解决手段:

  • 设置缓存数据的随机过期时间:在设置缓存数据的过期时间时、上一个随机值、使得不同的缓存数据在过期时刻不一致。这样可以避免大量数据同时过期、减轻数据库负荷。

  • 分布式锁或互斥锁:在缓存失效时、使用分布式锁或互斥锁来保证只有一个请求可以重新加载缓存。其他请求等待该请求完成后、直接从缓存中获取数据。这样可以避免多个请求同时访问数据库。

  • 数据预热:在系统启动或者非高峰期、提前将热点数据加载到缓存中、预热缓存。这样即使在高并发时、也能够从缓存中获取到数据,减轻数据库的压力。

  • 后台更新缓存:业务线程不再负责更新缓存、缓存也不设置有效期、而是让缓存永久有效、并将更新缓存的工作交由后台线程定时更新。

  • 数据库优化:对于缓存雪崩问题、除了缓存层面的应对策略、还可以从数据库层面进行优化、如提升数据库性能、增加数据库的容量等,以应对大量请求导致的数据库压力。

Redis故障宕机的解决手段:

  • 服务熔断或请求限流机制:启动服务熔断机制、暂停业务应用对缓存服务的访问、直接返回错误、所以不用再继续访问数据库、保证数据库系统的正常运行、等到 Redis 恢复正常后、再允许业务应用访问缓存服务。服务熔断机制是保护数据库的正常允许、但是暂停了业务应用访问缓存服系统、全部业务都无法正常工作。也可以启用请求限流机制、只将少部分请求发送到数据库进行处理、再多的请求就在入口直接拒绝服务。

  • 提高缓存本身的可用性:通过主从节点的方式构建 Redis 缓存高可靠集群、如果 Redis 缓存的主节点故障宕机、从节点可以切换成为主节点、继续提供缓存服务、避免了由于 Redis 故障宕机而导致的缓存雪崩问题。

7.缓存雪崩面试参考回答:

面试官问:缓存雪崩是什么?怎么解决?

缓存雪崩一般有两个常见场景、当大量缓存数据在同一时间过期或者 Redis 故障宕机时、如果此时有大量的用户请求、都无法在 Redis 中处理、于是全部请求都直接访问数据库、从而导致数据库的压力骤增、可能会造成系统崩溃。

缓存雪崩通常有两种场景:大量 key 同时过期Redis 宕机。采取不同的解决方案:

针对大量 key 同时过期:在系统启动的时候我们可以提前将热点数据加载到缓存中、这样即使在高并发时也能够从缓存中获取到数据、减轻数据库的压力。还可以设置缓存的随机过期时间、加上一个随机值、避免大量 key 在同一时刻过期从而减轻数据库压力。

针对 Redis 宕机:启动服务熔断机制、暂停业务应用对缓存服务的访问、直接返回错误、所以不用再继续访问数据库、保证数据库系统的正常运行、等到 Redis 恢复正常后、再允许业务应用访问缓存服务。也可以通过主从复制、哨兵模式或 Redis Cluster 等方式构建Redis 集群、如果 Redis 缓存的主节点故障宕机、从节点可以切换成为主节点、继续提供缓存服务、避免了由于 Redis 故障宕机而导致的缓存雪崩问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值