缓存击穿 缓存穿透 缓存雪崩
1.缓存击穿是什么?怎么解决
分析:
如果缓存中的某个热点数据过期了、此时大量的请求访问了该热点数据、就无法从缓存中读取、直接访问数据库,数据库很容易就被高并发的请求冲垮。
缓存击穿的关键是热点数据不存在于缓存中、因为如果是普通数据、那就是正常的缓存未命中、反之热点数据不在缓存中了、容易导致大量请求落到数据库上。
缓存击穿解决手段:
-
设置热点数据的热度时间窗口:对于热点数据、可以设置一个热度时间窗口、在这个时间窗口内、如果一个数据被频繁访问、就将其缓存时间延长、避免频繁刷新缓存导致缓存击穿。Redis滑动窗口
-
使用互斥锁或分布式锁:在缓存失效时、只允许一个线程去查询数据库、其他线程等待查询结果。可以使用互斥锁或分布式锁来实现、确保只有一个线程能够查询数据库、其他线程等待结果、避免多个线程同时查询数据库造成数据库压力过大。
-
异步更新缓存:在缓存失效时、可以异步地去更新缓存、而不是同步地去查询数据库并刷新缓存。这样可以减少对数据库的直接访问、并且不会阻塞其他请求的响应。
-
缓存预热:通过在缓存失效前、提前更新缓存、避免缓存失效时多个请求访问数据库。
-
缓存永不过期或延迟失效:对于某些重要数据、可以设置永不过期的缓存或延迟过期时间来减少缓存击穿
-
缓存数据本身永不过期 但 在缓存中存储一个逻辑过期时间(而非物理过期时间)。当读取缓存时、通过逻辑时间判断是否需要异步更新数据。
逻辑过期 是一种缓存设计策略,它的核心思想是:缓存数据本身不设置物理过期时间(即永不过期)、但在缓存中存储一个逻辑过期时间字段。通过判断逻辑过期时间来决定是否需要更新数据、而不是依赖缓存系统的自动过期机制。
-
缓存数据时、存储数据 + 逻辑过期时间(例如
expire_time
)。 -
读取缓存时:
-
若当前时间 <
expire_time
:直接返回缓存数据。 -
若当前时间 ≥
expire_time
:触发异步更新(如通过消息队列或后台线程) 立即返回旧数据 避免阻塞用户请求。
-
-
异步更新成功后,更新缓存数据和逻辑过期时间。
补充知识:
什么是逻辑过期
逻辑过期是一种缓存设计策略,它与传统的物理过期不同。
-
物理过期: 缓存系统(如 Redis)为每个缓存项设置一个过期时间。一旦到达这个时间、缓存系统会自动删除该缓存项。
-
逻辑过期: 缓存系统不设置过期时间(或设置一个很长的过期时间)、缓存项永远不会被自动删除。但是缓存项中会包含一个额外的字段、表示该数据的逻辑过期时间。
逻辑过期如何工作?
1.缓存数据时:
- 将数据存储到缓存中。
- 同时存储一个逻辑过期时间(
expire_time
)、表示该数据被认为是有效的截止时间。
读取缓存时:
- 检查缓存中是否存在该数据。
- 如果存在、比较当前时间与
expire_time
: - 当前时间 <
expire_time
: 说明数据仍然有效、直接返回缓存数据。 - 当前时间 >=
expire_time
: 说明数据已经逻辑过期、但仍然返回旧数据。 同时触发一个异步更新操作、去数据库中获取最新的数据并更新缓存。
2.步更新:
- 异步更新操作(例如通过消息队列或后台线程)从数据库中获取最新的数据。
- 更新缓存中的数据、并更新
expire_time
为新的逻辑过期时间。
逻辑过期如何解决缓存击穿
-
避免大量请求同时访问数据库: 当缓存逻辑过期时、仍然返回旧数据、避免了大量请求同时穿透到数据库。
-
保证可用性: 即使缓存数据已经逻辑过期、仍然可以提供服务、保证了系统的可用性。
-
异步更新保证最终一致性: 通过异步更新、最终会保证缓存中的数据与数据库中的数据一致。
逻辑过期的优点:
-
高可用性: 即使缓存过期、仍然可以提供服务。
-
避免缓存击穿: 减少了对数据库的直接访问。
-
平滑过渡: 异步更新可以平滑地更新缓存、避免了突然的性能下降。
逻辑过期的缺点:
-
数据不一致: 在异步更新完成之前、缓存中的数据可能不是最新的。
-
实现复杂: 需要额外的逻辑来处理过期时间和异步更新。
-
需要考虑异步更新失败的情况: 需要设计重试机制或补偿机制来处理异步更新失败的情况。
举例说明:
假设有一个商品信息、逻辑过期时间设置为 1 小时。
-
初始状态: 商品信息被缓存、
expire_time
设置为当前时间 + 1 小时。 -
1 小时后: 当有请求访问该商品信息时、发现当前时间已经超过了
expire_time
。 -
逻辑过期处理:
- 立即返回旧数据: 仍然返回缓存中的商品信息、保证用户可以立即看到数据。
- 触发异步更新: 启动一个异步任务、从数据库中获取最新的商品信息。
- 异步更新完成: 异步任务从数据库中获取到最新的商品信息、并更新缓存中的数据、同时将
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.缓存穿透是什么、怎么解决
分析:
当用户访问的数据、既不在缓存中、也不在数据库中、导致请求在访问缓存时、发现缓存缺失、再去访问数据库时、发现数据库中也没有要访问的数据、没办法构建缓存数据、来服务后续的请求。
那么当有大量这样的请求到来时、数据库的压力骤增、这就是缓存穿透的问题。
最常见的场景就是有攻击者伪造了大量的请求、请求某个不存在的数据。这会造成两个后果:
-
缓存里面没有对应的数据、所以查询会落到数据库上。
-
数据库也没有数据、所以没有办法回写缓存、下一次请求同样的数据、请求还是会落到数据库上。
缓存穿透解决手段:
-
回写特殊值:在缓存未命中、而且数据库里也没有的情况下、往缓存里写入一个特殊的值。这个值就是标记数据不存在、那么下一次查询请求过来的时候、看到这个特殊值、就知道没有必要再去数据库里查询了。但如果如果攻击者每次都用不同的且都不存在的 key来请求数据、那么回写特殊值这种手段会丧失它的效果、而且因为要回写特殊值、会浪费不少 Redis 的内存。这可能会进一步引起另外一个问题、就是 Redis 在内存不足、执行淘汰的时候、把其他有用的数据淘汰掉、而更好的就是考虑使用布隆过滤器。
-
解决缓存穿透的通用方案是: 给所有指定的 key 预先设定一个默认值、比如空字符串Null、当返回这个空字符串Null时,我们可以认为这是一个不存在的 key、在业务代码中、就可以判断是否取消查询数据库的操作、或者等待一段时间再请求这个 key。如果此时取到的值不再是Null、我们就可以认为缓存中对应的 key 有值了,这就避免出现请求访问到数据库的情况,从而把大量的类似请求挡在了缓存之中。
-
使用布隆过滤器:布隆过滤器是一种快速判断元素是否存在的数据结构、它可以在很小的内存占用下、快速判断一个元素是否在一个集合中。将所有可能存在的数据哈希到一个足够大的位数组中、当一个请求过来时、可以先通过查询布隆过滤器快速判断数据是否存在、如果不存在、则直接返回、避免对数据库的查询。
-
限流策略:针对频繁请求的特定数据,可以设置限流策略,例如使用令牌桶算法或漏桶算法,限制对这些数据的请求频率,减轻数据库的压力。
缓存穿透事前事中解决方案
一、缓存穿透解决方案:
-
事前预防 (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 故障宕机而导致的缓存雪崩问题。