我们来详细、通俗地解释一下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 高可用(主从/集群)、降级熔断 |