缓存穿透、缓存击穿、缓存雪崩
缓存穿透 (Cache Penetration)
缓存穿透指的是客户端请求查询一个在缓存和数据库中都绝对不存在的数据。由于缓存中没有命中,请求会直接到达数据库,而数据库也查不到该数据,因此不会将结果写入缓存。当有大量此类请求(尤其是恶意的攻击请求)发生时,这些请求会完全绕过缓存的保护,直接对数据库造成巨大压力,可能导致数据库服务响应缓慢甚至宕机。
- 核心特征:查询的数据本身就是不存在的。
- 例子:黑客使用大量不存在的用户ID(例如负数ID或超大ID)来频繁请求用户信息接口。
解决方案
-
缓存无效键 (Cache Null Values)
- 措施:当数据库查询确认某个key不存在时,仍然在缓存中为这个key存入一个特殊的值(比如
null或者一个约定的字符串),并设置一个较短的过期时间。 - 原理:这种方法的核心思想是将“不存在”这个状态也视为一种数据并进行缓存。当后续再次有对同一个不存在的key的请求时,缓存层可以直接返回这个预设的“空值”,从而避免了请求再次穿透到数据库。设置一个较短的过期时间,是为了防止缓存中存储大量无效key,同时也能在一定时间内有效抵挡对同一个key的重复攻击。
- 措施:当数据库查询确认某个key不存在时,仍然在缓存中为这个key存入一个特殊的值(比如
-
布隆过滤器 (Bloom Filter)
- 措施:在请求访问缓存之前,设置一个布隆过滤器。系统启动时,将所有可能存在的数据key(例如,数据库中所有的商品ID)都加载到布隆过滤器中。当一个请求到来时:
- 先去布隆过滤器检查这个key是否存在。
- 如果布隆过滤器判断key不存在,则直接拒绝该请求,返回空值。
- 如果布隆过滤器判断key可能存在,则放行请求,让它继续查询缓存和数据库。
- 原理:布隆过滤器是一种高效的概率型数据结构,用于判断一个元素是否在一个集合中。它的原理是使用一个很长的位数组(bit array)和多个独立的哈希函数。当添加一个元素时,会用多个哈希函数计算出多个哈希值,并将位数组中对应位置的bit置为1。当查询一个元素时,同样计算出多个哈希值,并检查位数组中对应位置是否全部为1。
- 优点:空间效率和查询时间都远超一般的数据结构。
- 特点:它可以100%确定一个元素绝对不在集合中,但只能概率性地判断一个元素可能在集合中(因为可能存在哈希冲突,导致误判)。通过在入口处拦截掉绝大多数不存在的key,它极大地保护了后端的缓存和数据库系统。
- 措施:在请求访问缓存之前,设置一个布隆过滤器。系统启动时,将所有可能存在的数据key(例如,数据库中所有的商品ID)都加载到布隆过滤器中。当一个请求到来时:
缓存击穿 (Cache Breakdown)
缓存击穿特指某一个访问极其频繁的热点数据(Hot Key),在它缓存过期的瞬间,有大量的并发请求同时涌入。由于此刻缓存已失效,这些请求会同时穿透缓存,直接打到后端的数据库上,导致数据库瞬间压力剧增,可能引发故障。
- 核心特征:单个热点数据在某一瞬间过期失效。
- 例子:在秒杀活动中,某个热门商品的缓存在活动进行中突然过期,成千上万的刷新请求瞬间全部打向了数据库。
解决方案
-
使用互斥锁(Mutex Lock)
- 措施:当缓存未命中时,并不是让所有线程都去查询数据库。而是先尝试获取一个互斥锁(例如基于Redis的SETNX实现的分布式锁)。
- 第一个获取到锁的线程负责从数据库加载数据,并将数据写回缓存,最后释放锁。
- 其他未获取到锁的线程则不会去访问数据库,而是会等待一小段时间后重试查询缓存。此时,第一个线程很可能已经将数据写入缓存了。
- 原理:该方案的核心是将并发的数据库查询操作串行化。通过加锁机制,确保了在缓存失效的瞬间,只有一个线程能够去执行数据库查询和缓存回写操作,其他线程则被阻塞或引导去重试,从而有效避免了大量并发请求同时冲击数据库,保护了后端系统。
- 措施:当缓存未命中时,并不是让所有线程都去查询数据库。而是先尝试获取一个互斥锁(例如基于Redis的SETNX实现的分布式锁)。
-
设置热点数据永不过期
- 措施:对于一些核心的热点数据,可以直接在缓存中设置为“永不过期”,或者将过期时间设置得非常长。
- 原理:这种方法从根本上消除了数据过期的可能性,因此也就不会发生因过期引发的缓存击穿问题。对于需要更新的数据,可以通过后台任务、消息队列或运营手动触发等方式来主动刷新缓存,而不是依赖于被动的过期淘汰机制。
-
缓存预热(Cache Preheating)
- 措施:在可预见的流量高峰到来之前(例如,大促活动开始前),通过定时任务或手动触发的方式,提前将即将成为热点的数据加载到缓存中,并设置一个合理的过期时间(例如,将过期时间设置为活动结束之后)。
- 原理:这是一种主动的缓存管理策略。它将填充缓存数据的操作从高并发的用户请求时,提前到了系统相对空闲的阶段,确保在流量洪峰到达时,热点数据已经在缓存中准备就绪,避免了临时的缓存失效问题。
缓存雪崩 (Cache Avalanche)
缓存雪崩指的是在某一个时间段内,缓存大面积、集中地失效,或者缓存服务自身发生故障宕机。这导致海量的请求无法被缓存处理,从而像雪崩一样全部涌向数据库,造成数据库压力过大甚至崩溃。
- 核心特征:大量数据同时过期 或 缓存服务不可用。
- 例子:系统启动时,将一大批数据在同一时刻加载进缓存,并设置了相同的过期时间(例如1小时)。1小时后,这些缓存同时失效,导致所有相关请求全部打向数据库。
解决方案
针对大量数据集中失效的情况:
- 设置随机过期时间
- 措施:在原有的过期时间基础上,增加一个随机的时间偏移量。例如,原本统一设置为1小时过期,可以改为
3600秒 + random(0, 600)秒。 - 原理:这种方法的核心是将缓存的过期时间点打散。通过引入随机性,可以确保缓存不会在同一时刻发生大规模的集中失效,而是会在一段时间内分批、均匀地过期。这样,落到数据库上的请求压力就会被平摊开来,避免了瞬间的压力峰值。
- 措施:在原有的过期时间基础上,增加一个随机的时间偏移量。例如,原本统一设置为1小时过期,可以改为
针对缓存服务不可用的情况:
-
构建高可用的缓存集群
- 措施:采用Redis的主从复制+哨兵(Sentinel)模式,或者Redis集群(Cluster)模式来部署缓存服务。
- 原理:通过集群化来避免单点故障。当集群中的某个节点(例如主节点)宕机时,哨兵或集群机制能够自动进行故障转移(Failover),将一个从节点提升为新的主节点,从而保证缓存服务能够持续对外提供服务,避免了整个缓存服务的瘫痪。
-
限流与降级
- 措施:
- 限流:在系统的入口层(如API网关)对请求进行限制,当检测到缓存服务异常时,只允许一小部分请求能够到达后端数据库。
- 降级:当缓存服务不可用时,暂时关闭一些非核心功能。对于某些请求,可以不访问数据库,而是直接返回一个预设的默认值、兜底数据或友好的提示页面。
- 原理:这是一种牺牲部分用户体验来换取系统整体稳定的“丢车保帅”策略。通过主动限制流量和关闭次要功能,确保在极端情况下,核心服务(如登录、支付)和后端数据库不会被彻底压垮,保证了系统的核心可用性。
- 措施:
-
多级缓存
- 措施:在应用中引入多级缓存体系,例如,使用进程内的本地缓存(如Google Guava Cache、Caffeine)作为一级缓存,使用Redis等分布式缓存作为二级缓存。
- 原理:为系统提供额外的保护层。当作为二级缓存的Redis集群发生故障时,一级本地缓存仍然可以命中一部分热点数据,抵挡住部分流量,为后端数据库提供一道缓冲,减轻缓存雪崩带来的直接冲击。