Redis 缓存穿透,缓存击穿,缓存雪崩

在日常开发工作中,缓存技术被广泛利用以增强系统性能和减轻数据库的访问压力。但是,在使用缓存的过程中,我们可能会碰到几个典型问题,比如缓存穿透、缓存击穿和缓存雪崩。这些问题不仅在实际的应用中需要解决,而且它们也经常在面试中被问到。

​ 然后,大部分的小可爱在准备阶段可能已经熟悉了这些概念,但在面试的紧张氛围下,往往会出现混淆,无法清晰区分缓存穿透、缓存击穿和缓存雪崩。为了帮助大家深入理解并记住这三种缓存问题,以便在面试或实际工作中能够清晰地识别和解决问题,本文将详细解析这三个缓存问题,并提供一些记忆技巧,确保你不再对它们感到困惑,达到拨云见日、茅塞顿开之功效。

一、缓存穿透
1、什么是缓存穿透?

​ 缓存穿透是指用户请求的数据在缓存中不存在即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍。如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至导致数据库承受不住而宕机崩溃。

那么,我们该怎么记忆缓存穿透呢?

我们可以这么想,缓存穿透其实就是恶意攻击,就是有人想“穿”过你的防护网(缓存)来“偷”你的家(数据库),这也就是穿透。或者你这样想,有人想恶意攻击你的数据库,这是不是不好的行为,是不是小偷的行为!这样透和偷,不就联系起来了。那么下次面试官问你缓存穿透是什么的时候。你只要想到透就是偷,就是不好的行为,也就能想到缓存穿透的意思就是别人恶意攻击,故意频繁访问一个不存在的key,来压垮你的数据库。

2、问题分析

​ 缓存穿透现象的核心问题在于请求中使用的key在Redis缓存中无法找到相应的值。这与缓存击穿的情形有本质的不同,后者通常涉及一个有效key的过期失效。在缓存穿透的情况下,传入的key在Redis中根本就不存在。如果黑客故意发送大量不存在的key的请求,这会导致数据库遭受巨大的查询压力,可能会严重威胁到系统的正常运行。因此,在日常开发实践中,对请求参数进行严格的校验是至关重要的。对于那些非法或明显不可能存在的key,系统应该立即返回一个错误提示,而不是让这些请求到达数据库层面。这样不仅可以提升系统的安全性,还能够维护数据库的稳定性和性能。

3、解决方案
  • 缓存空数据

    当出现Redis查不到数据,数据库也查不到数据的情况,我们就把这个key保存到Redis中,设置value=“null”,并设置其一个较短的过期时间,后面再出现查询这个key的请求的时候,直接返回null,就不需要再查询数据库了。

img

优点实现简单

缺点:1)耗费内存并且会有失效的情况。在Redis中缓存大量空值不仅会消耗宝贵的内存资源,而且如果攻击者持续使用随机键进行攻击,这种防御策略就会失效。在这种情况下,不仅数据库可能因过载而崩溃,Redis服务也可能由于内存耗尽而出现拒绝写操作的现象。这样,你的正常需要写入redis的业务也就会跟着受到影响。

​ 2)数据不一致。虽然在缓存空值时我们设定了较短的过期时间,但仍存在一种情况:在缓存的空值尚未过期的这段短时间内,数据库中的实际数据可能已经更新,而该键值在数据库中存在了数据。这导致在缓存中的空值仍然被返回,而没有反回真实的数据,从而造成缓存与数据库之间的数据不一致现象。

  • 布隆过滤器

​ 布隆过滤器提供一种高效的概率型检测机制,用于判断一个元素是否可能在一个集合内。它的工作原理是,当布隆过滤器断言某个键(key)不存在时,这个结论是绝对可靠的;但当它认为某个键存在时,只表示有很高的可能性确实存在,尽管有一定的误判几率。为了缓解缓存穿透的问题,我们可以在Redis缓存层之前部署一道布隆过滤器的防线。将数据库中所有的键导入布隆过滤器中,这样在任何查询到达Redis之前,系统会首先检查该查询的键是否在布隆过滤器中。如果键在布隆过滤器中不存在,那么查询将不会继续前往数据库,而是直接返回结果,以此避免对数据库的不必要访问和潜在的查询压力。通过这样的布隆过滤器前置筛查,我们不仅保护了数据库免受不存在的键的查询压力,还确保了整体系统的性能稳定,即使在高并发查询的环境下也能有效地运作。

img

优点占用内存小。布隆过滤器不需要存储具体的数据项,只需要存储数据的哈希值,因此相比存储实际数据,它占用的内存更少

缺点: 1)**存在一定的误判情况。**布隆过滤器存在一定的误判率,即它可能会错误地认为某个不存在的元素存在(假阳性),尽管可以通过调整参数来降低误判率,但无法完全消除。

​ 2)**不可逆性。**布隆过滤器不需要存储具体的数据项,只需要存储数据的哈希值,因此相比存储实际数据,它占用的内存更少。

​ 3)**数据不一致。**为了维护数据的准确性和一致性,理想情况下,当数据库中的数据发生更新时,布隆过滤器也应当进行相应的更新以反映这些变更。然而,布隆过滤器与数据库是两个独立的数据管理实体。所以可能出现的一种情形是,在数据库成功执行了数据更新之后,当尝试更新布隆过滤器时,网络异常发生,导致新增的数据未能及时写入布隆过滤器中。在这种状况下,后续针对这个新加入数据的查询请求将会被布隆过滤器拒绝,因为该数据的键尚未存在于过滤器中。尽管这是一个合理的查询请求,它却被“错误地”拦截了。

​ 在这里,其实我们可以意识到并不存在一个完美无缺的解决方案。最终选择哪种方案必须依据我们的业务场景来定。例如,虽然引入布隆过滤器可以解决缓存穿透导致的内存占用问题,但它同时可能带来其它问题。这也正是我们需要培养的思维方式:每当考虑引入新的技术方栈时,我们必须思考它能够解决哪些问题,以及可能会带来哪些新问题,并要提前规划好如何应对这些潜在的问题**。**

那么,到底该如何解决这个问题?

​ 在这里,关键的考量因素是我们平台能够承载的并发水平。我们可以根据我们平台的特点去考量平台最大的并发量。然后,再依据最大并发量进行限流,确保即使在极端的高并发情况下,也不会对数据库造成过大压力导致其崩溃。其次,再经过限流后,我们再去考虑并发度高低,并发度低的我们可以简单的使用缓存空值的方案来解决缓存穿透的问题,并发度高的情况下我们还是最好使用布隆过滤器的方案解决缓存穿透问题。

​ 当然,除了上面提到的布隆过滤器和缓存空数据的方案之外,我们还可以通过实施参数校验拉黑恶意攻击者的IP来增强对缓存穿透问题的防护。

二、缓存击穿
1、什么是缓存击穿?

​ 缓存击穿通常发生在高并发系统中,当某个被频繁访问的数据(热点key)在缓存中的有效期过了,恰好在这个时候有大量的并发请求需要访问这个数据。由于缓存中的数据已经失效,这些并发请求就会直接转发到数据库上,如果数据库的处理能力不足以应对这种突然增加的压力,就可能导致系统响应缓慢甚至崩溃。

那么,我们该怎么记忆缓存击穿呢?

​ 首先,让我们参考百度百科对“击穿”现象的解释:在强电场的作用下,绝缘材料内部会发生破坏性的放电,导致其绝缘电阻降低,电流增大,最终引发材料的损坏甚至穿孔。现在,我们用这个原理来类比Redis缓存系统中的情况。设想Redis中的缓存数据被组织成一系列的“网格”,这些网格共同构成了一层防御屏障。当某个网格由于某种原因突然变得不可用或丢失时,原本应该被该网格处理的大量请求就会如同电流般穿透这个缺口,直接冲击后端的数据库。这种现象,就类似于电学中的“击穿”,在Redis缓存的语境下,我们称之为缓存击穿。

​ 或者,你也可以这样想。"击穿"中的"击"字与"寄"发音相同。在网络上,当我们说“我寄了”,它通常隐含着“我完了”或者“我死了”的意味。沿着这个思路,缓缓存击穿就好比是某个关键的key“寄”了,导致的大量并发请求打到数据库。所以,只要你能从“击”这个字联想到“寄寄”,便能够自然而然地想到缓存击穿这一概念的具体含义。

2、问题分析

​ 核心问题在于,一旦某个热门的key失效,便会导致密集的并发请求直接涌向数据库。因此,解决方案需从两个方向着手:首先,考虑对热点key不设置过期时间以保持其持久有效;其次,探索降低数据库所承受的请求量的方法,以减轻其压力。

3、解决方案
  • 互斥锁

​ 在缓存失效后,通过互斥锁控制读数据写缓存的线程数量,比如某个key只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程。

img

优点1)强一致。互斥锁能够确保在缓存重建过程中,只有一个线程可以访问数据库并更新缓存,这样可以避免多个线程同时读取到过期的缓存数据,从而保证了数据的强一致性。

2)实现相对简单。互斥锁的实现相对简单,不需要复杂的逻辑处理,只需在缓存失效时加锁,更新完毕后释放锁即可。

缺点吞吐量低。在高并发的场景下,互斥锁可能会导致系统的可用性降低,因为大量的请求可能会因为等待锁而无法及时得到处理。

  • 逻辑过期

​ 逻辑过期方案与互斥锁方案在应对缓存击穿问题上确实有着相似之处,它们都通过使用互斥锁来防止同时对同一热点key的缓存进行更新。然而,它们的工作细节和重点略有不同。在逻辑过期方案中,每个缓存项旁边附加了一个额外的字段来表示其逻辑上的过期状态,而不是物理地从缓存系统中移除该数据。这样,即使一个缓存项已标记为逻辑上过期,它仍然存在于缓存中并可以被访问到。这种方法避免了因等待重新加载数据而导致的缓存缺失情况。当一个线程查询某个key的缓存数据时,如果该数据已经处于逻辑过期状态,无论该线程是否成功获取互斥锁,系统都会先返回这个逻辑上过期的数据。这样做的好处是即使在数据更新过程中,也能保证用户能立即得到响应,虽然数据可能是旧的。与此同时,如果某个线程成功获取了互斥锁,这意味着它获得了更新缓存数据的权限。这个线程会创建一个单独的工作线程来负责从数据库检索最新数据、更新缓存,并重置该key的逻辑过期时间。一旦这个过程完成,互斥锁便会被释放。

img

优点吞吐量高。在逻辑过期方案中,即使数据已过期,系统仍会返回过期数据给客户端,而不是等待锁释放后再去数据库拉取最新数据。

缺点: **1)牺牲数据一致性。**由于在数据更新过程中,系统可能会返回过期数据,这在一定程度上牺牲了数据的一致性。

​ **2)实现复杂。**逻辑过期方案需要维护额外的字段来记录每个缓存项的逻辑过期状态,这增加了系统的复杂性。

​ **3)耗费更多的内存。**因为增加了一个字段来维护逻辑过期的时间,这必定会造成额外的空间占用。

这里,可能有些小可爱会有一个疑问:为什么不直接使用热点数据永不过期来代替逻辑过期呢?逻辑过期的数据不也是永不过期吗?折腾这么多又是加字段又是加锁的是为了什么?直接都不过期了不就不会缓存击穿了吗?

​ 实际上,尽管可以选择配置热点数据为永不过期以取代逻辑过期的方案,但逻辑过期的存在有其独特的必要性。逻辑过期允许我们在数据达到过期时间点时主动去刷新缓存中的数据,从而确保数据的更新和准确性。如果没有设置逻辑过期,我们将失去在查询时更新缓存数据的能力,这将导致数据的实时性和一致性难以得到保障。所以,对于不常变更的数据,可以安心设置为永不过期;而对于频繁更新的数据,采用逻辑过期是更为合适的策略。

那么,什么使用逻辑过期,什么时候使用互斥锁呢?

根据上面我们总结的这两个方案的优缺点,不难得出当我们追求数据的强一致的时候,就使用互斥锁方案。当我们要求数据更高的吞吐量,并且对数据的一致性要求不高的时候,就使用逻辑过期的方案。

三、缓存雪崩
1、什么是缓存雪崩?

​ 缓存雪崩是指由于缓存系统的整体失效,导致大量请求直接到达后端数据库,进而可能造成数据库崩溃和整个系统的崩溃。这种现象通常发生在缓存服务器重启或宕机时,或者是大规模的key同时失效导致的结果。

那么,我们该怎么记忆缓存雪崩呢?

​ 其实,记住缓存雪崩相比另外两个是比较容易的。雪崩这个词本身就可以生动地描绘了缓存雪崩发生的情境:发挥你的想象,redis就是个雪山,而redis上面的key好比山上一个一个的雪花。突然间,一个自称“天下第一帅”的拙野挥洒着他的帅气,高声发出震天的呐喊:我是天下第一帅!。这一声犹如惊雷贯耳,瞬间引发了山上的积雪大面积崩塌(大量key失效)或者这声吼叫的力量如此巨大,竟像是触发了一场地震,使得整个雪山轰然倒塌(redis服务器宕机)。

2、问题分析

​ 造成缓存雪崩的关键在于同一时间的大规模的key失效,为什么会出现这个问题,主要有两种可能:第一种是Redis宕机,第二种可能就是采用了相同的过期时间。

3、解决方案
  • 过期时间随机

​ 在设置失效时间的时候加上一个随机值,比如1-5分钟随机。这样就可以避免了由于使用相同的过期时间导致在某一时刻大量key过期引发的缓存雪崩问题。

  • 使用熔断机制

​ 当系统流量达到预定的极限时,为避免对数据库造成过大压力,我们将自动显示“系统繁忙”提示。这样做可以确保至少有一部分用户能够顺畅地使用我们的服务。对于未能即时访问的用户,只要多刷新几次,也是可以获得正常访问的。

  • 缓存预热

​ 缓存预热是一种关键技术,它在系统启动前预先加载关键数据到缓存中,以减少系统上线时对后端数据库的冲击。由于新上线的系统缓存是空的,如果没有预热过程,大量并发请求将直接访问数据库,极有可能在系统上线初期导致服务崩溃。因此,通过在系统上线之前将高频率访问的数据从数据库加载到Redis等缓存系统中,可以确保用户请求首先由缓存服务处理,从而减轻数据库的压力。实施缓存预热通常涉及编写批处理任务,这些任务可以在系统启动期间执行,或者通过定时任务定期去执行。定期执行更能保证数据的实时性,但是,同样会耗费系统的部分性能,尤其是在数据量大的时候。所以,具体选择如何进行预热数据,还是需要综合考虑预热数据量的大小以及预热数据更新的是否频繁等因素。

  • redis集群

​ 保证Redis缓存的高可用,防止Redis宕机导致缓存雪崩的问题。可以使用主从+ 哨兵,Redis集群来避免单个Redis服务器宕机导致整个缓存直接失效。

  • 多级缓存

​ 通过实施多级缓存策略,我们可以优化系统的性能并降低因缓存失效导致的风险。在这种策略中,本地进程内的缓存充当第一级缓存,而Redis则作为第二级远程缓存。每一级缓存都设定有独立且差异化的超时时间,这样的设计确保了即使一级缓存的数据过期或被清除,仍能有二级或其他级别的缓存来提供数据支持。这种层级化的缓存机制为系统提供了额外的弹性层,当一层缓存遇到问题时,其他层级能够起到“安全网”的作用,从而可以有效避免雪崩现象。

  • 互斥锁

​ 这个和缓存击穿比较类似 ,都是通过互斥锁来控制读数据写缓存的线程数量,这样就避免大量请求同时击中数据库。同样,这样虽然可以避免大量key同时失效导致的缓存雪崩问题。但是,同样性能也会因为加锁的原因受到影响。如果,系统对吞吐量要求不高的情况下,这种方式其实还是不错的。因为它即解决了缓存击穿的问题,也解决了缓存雪崩的问题。可谓一举两得。