JAVA面试|redis缓存穿透,击穿,雪崩问题

内容分享3小时前发布
0 0 0

我们来详细、通俗地解释一下Redis缓存中的三个经典问题:缓存穿透、缓存击穿、缓存雪崩。它们都涉及到缓存未能有效保护数据库,导致数据库压力过大甚至崩溃,但触发缘由和表现形式不同。

核心思想:Redis作为缓存,目的是减轻数据库压力,让高频访问的数据快速返回。如果缓存“失效”(找不到数据或大量失效),请求就会直接打到数据库上,数据库压力剧增。

一、缓存穿透

通俗比喻:你经营一家医院,挂号系统(Redis缓存)里只记录真实存在的病人档案。目前,有人(黑客或恶意程序)不停地用根本不存在的病人身份证号来挂号窗口查询(请求数据)。挂号系统每次都查不到(缓存未命中),于是不得不每次都去庞大的纸质档案库(数据库)里翻找。结果当然是永远找不到(数据库也没有),但每次查询都消耗了大量档案库管理员(数据库)的精力。如果这种无效查询海量涌来,档案库管理员就累趴下了(数据库崩溃)。

问题本质:查询一个数据库里也根本不存在的数据。 请求的数据既不在缓存中,也不在数据库中。导致每次请求都会穿透缓存,直接访问数据库。

缘由:

恶意攻击:黑客故意构造大量数据库不存在的key(如负数的ID、随机字符串)进行请求。

业务逻辑错误:程序Bug导致生成了无效的查询条件(如用户输入了不存在的商品ID)。

后果:

大量无效查询直接落到数据库,消耗数据库连接和计算资源。

数据库压力陡增,性能下降,甚至崩溃。

缓存层形同虚设。

解决方案:

缓存空对象 (Cache Null):即使数据库查不到,也在缓存中为这个key设置一个空值(或特殊标记,如 NULL、EMPTY)并设置一个较短的过期时间(如5分钟)。这样后续一样的无效请求在过期时间内会直接拿到空结果返回,不再访问数据库。(简单有效,常用)

布隆过滤器 (Bloom Filter):在缓存层之前,加一个布隆过滤器。它是一个空间效率很高的概率型数据结构,能告知你某个key 必定不存在或可能存在于数据库中。

流程:请求来了 -> 先查布隆过滤器:

如果布隆过滤器说“必定不存在” -> 直接返回空/错误,不再查缓存和数据库。

如果布隆过滤器说“可能存在” -> 再去查缓存 -> 缓存有则返回,缓存没有则查数据库 -> 数据库查到则回填缓存,数据库查不到也缓存空对象。

优点:内存占用极小,能拦截绝大部分无效请求。

缺点:有轻微误判率(可能把存在的key误判为不存在,概率可控),需要维护布隆过滤器的数据(数据库有新增时,也要在布隆过滤器中添加该 key)。

参数校验/业务层过滤:在请求到达缓存层之前,对请求参数进行严格的合法性校验(如ID范围、格式校验),拦截掉明显无效的请求。(基础但必要)

二、缓存击穿

通俗比喻:你开了一家网红奶茶店(数据库),有一款限量版奶茶(热点数据)特别火。这款奶茶的预售券(缓存)在每天上午10点(缓存过期时间)统一失效。10点整,海量顾客(并发请求)同时涌来抢购,发现预售券没了(缓存过期未命中),于是所有人瞬间挤到柜台(数据库)要求直接购买。柜台瞬间被挤爆(数据库并发压力剧增),系统瘫痪。虽然这款奶茶库存是有的(数据库有数据),但短时间的高并发直接打垮了数据库。

问题本质:一个访问量巨大的热点key在缓存过期的瞬间,海量的请求直接穿透缓存,同时打向数据库。 这个key对应的数据在数据库中是存在的。

缘由:

某个key是热点数据(访问量极大)。

该key在缓存中过期了(可能是自然过期,也可能是被主动删除)。

在这个key失效的瞬间,有大量并发请求同时到来。

后果:

在热点key失效的瞬间,数据库承受极高的并发查询压力。

可能导致数据库连接数耗尽、CPU飙升、甚至宕机。

虽然最终数据会被重新加载到缓存,但瞬间的高并发对数据库是灾难性的。

解决方案:

设置热点数据永不过期:对于极少数访问频率极高、更新不频繁(或可以接受必定延迟)的热点key,直接在缓存层面设置永不过期(或超级长的过期时间)。通过其他机制(如后台定时任务、监听数据变更)来异步更新缓存。(一劳永逸,但需谨慎选择数据)

互斥锁/分布式锁(Mutex Lock)

流程:当第一个请求发现缓存失效 -> 获取一个针对这个key的锁 -> 获取锁成功后,只有这个请求去数据库加载数据 -> 加载完成后回填缓存 -> 释放锁 -> 后续请求再从缓存中获取数据。

未获取到锁的请求:可以选择短暂等待后重试,或者直接返回默认值/错误(取决于业务容忍度)。

优点:保证只有一个线程去查数据库,避免并发冲击。

缺点:引入了锁的复杂度,获取锁失败或等待的请求会有延迟。锁的实现要可靠(如用 Redis 的 SETNX 命令实现分布式锁)。

逻辑过期 (Logical Expiration):不在缓存层面设置物理过期时间,而是在缓存值中存储一个逻辑过期时间字段。

流程:请求读取缓存 -> 检查缓存值中的逻辑过期时间:

如果未过期 -> 直接返回数据。

如果已过期 -> 获取互斥锁 -> 获取锁的线程异步去数据库加载最新数据并更新缓存(同时更新逻辑过期时间)-> 释放锁 -> 当前请求可能返回稍旧的数据(或等待更新)。

优点:用户请求几乎总能快速返回(即使是稍旧数据),后台异步更新,避免请求阻塞。

缺点:实现复杂,用户可能读到过期数据(需要业务容忍度),仍需配合锁机制。

三、缓存雪崩

通俗比喻:还是那家医院。这次不是有人查假档案,而是医院电脑系统(Redis缓存)突然故障重启(缓存服务不可用),或者所有病人的挂号记录(大量缓存key)由于设置了一样的有效期,都在凌晨2点(同一过期时间点)集体失效了。结果就是,第二天早上所有来看病的人(大量正常请求)都无法在挂号系统找到自己的记录(缓存大面积未命中),全都涌向档案库(数据库)查询。档案库管理员瞬间被海量查询淹没(数据库压力激增),整个医院系统瘫痪。

问题本质:大量的缓存key在同一时间段聚焦失效,或者Redis 缓存服务本身宕机,导致所有请求(包括正常请求)在短时间内都无法命中缓存,全部涌向数据库,造成数据库压力山崩。

缘由:

大量key同时过期:业务上为许多key设置了一样或超级接近的过期时间(如默认缓存1小时,系统启动时批量加载的数据都在同一时间过期)。

Redis实例宕机:整个Redis服务崩溃,所有缓存瞬间不可用。

后果:

在失效时间点或宕机后,海量正常请求直接穿透缓存访问数据库。

数据库承受的请求量远超其处理能力,极易导致数据库响应缓慢甚至崩溃。

系统整体瘫痪,影响范围巨大。

解决方案:

过期时间随机化:为缓存key设置过期时间时,在基础值上增加一个随机范围(如基础1小时,随机范围±10分钟)。这样大量key 的过期时间就分散开了,避免了同一时刻集体失效。(预防聚焦失效的最常用、最有效手段)

Redis高可用架构:

主从复制 + 哨兵 (Sentinel):主节点宕机,哨兵自动选举从节点升级为主节点,保证服务可用性。

Redis集群 (Cluster):将数据分片存储在多个节点上,单个节点故障只影响部分数据,整体服务仍可用。提高了并发能力和容错性。(解决单点故障的核心方案)

多级缓存:在Redis之前再加一层本地缓存(如Ehcache, Guava Cache, Caffeine)。即使Redis挂了,部分热数据还能从本地缓存获取,为Redis恢复争取时间。本地缓存的过期时间也要分散。

服务降级与熔断:

降级:当检测到数据库压力过大或缓存大面积失效时,对于非核心业务,直接返回默认值、兜底数据或友善提示(如“系统繁忙,稍后再试”),牺牲部分功能保证核心流程和系统可用。

熔断:当数据库错误率超过阈值时,自动“熔断”对数据库的访问,后续请求直接走降级逻辑。过一段时间再尝试恢复。(保护数据库的最后防线)

提前预热:对于已知即将到来的大流量(如秒杀、重大活动),提前加载相关数据到缓存,并确保它们的过期时间足够分散或设置较长。

四、总结对比

问题类型

触发缘由

影响范围

关键特征

核心解决方案

缓存穿透

查询不存在的数据 (恶意攻击、无效参数)

特定(不存在)的Key

数据库无此数据

缓存空对象、布隆过滤器、参数校验

缓存击穿

单个热点 Key 过期 + 高并发

单个热点 Key

数据库有此数据,瞬间并发穿透

永不过期(慎用)、互斥锁、逻辑过期

缓存雪崩

大量Key同时过期或Redis服务宕机

大量/全部 Key

大面积缓存失效,海量正常请求穿透

过期时间随机化、Redis 高可用(主从/集群)、降级熔断

© 版权声明

相关文章

暂无评论

none
暂无评论...