Redis缓存穿透、击穿、雪崩相关问题
缓存穿透、击穿、雪崩的概念与区别,以及解决方案
概念与区别
什么是缓存穿透、缓存击穿、缓存雪崩?三者的区别是什么?
一句话原理 缓存穿透是“查不到”(绕过缓存查库),缓存击穿是“热点过期”(单一 Key 高并发打穿),缓存雪崩是“批量过期”(大量 Key 同时失效或服务宕机)。
一句话源码
穿透需在 DAO 层拦截空值 if (dbVal == null) cache.set(key, NULL);击穿需在查询逻辑中加入分布式锁 tryLock(key) 后回填缓存;雪崩需在初始化缓存时对 TTL 加随机值 setex(key, ttl + random)。
一句话项目/场景 在“秒杀系统”中,黑客请求不存在的商品 ID 导致穿透,热门商品 Key 突然失效导致击穿,凌晨时段大量预热数据同时过期导致雪崩,三者均会造成数据库瞬时压力激增甚至宕机。
详细区别与解决方案总结
| 维度 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 核心定义 | 查询根本不存在的数据 | 热点 Key 突然过期 | 大量 Key 集中过期或 Redis 宕机 |
| 触发原因 | 恶意攻击、业务缺陷请求非法 Key | 高并发访问某热点数据,且缓存失效 | 缓存设置相同过期时间、Redis 服务故障 |
| 影响范围 | 每次请求都穿透到数据库 | 单一热点数据压垮数据库 | 大量请求压垮数据库,系统瘫痪 |
| 解决方案 | 1. 布隆过滤器(拦截不存在的 Key) 2. 缓存空对象(设置短 TTL) | 1. 互斥锁(只允许一个线程查库) 2. 逻辑过期(不设 TTL,后台异步更新) | 1. 随机过期时间(打散失效点) 2. 高可用集群(防宕机) 3. 限流降级 |
缓存穿透的场景有哪些?(如查询不存在的 ID)
一句话原理 缓存穿透的场景主要分为“业务误操作”与“恶意攻击”两类,本质是请求绕过了缓存层,直接对数据库发起了无效查询。
一句话源码
在 Controller 层入口处,通常需增加参数合法性校验 if (id == null || id <= 0) return;,或在 DAO 层对空结果进行短暂缓存 if (value == null) redis.set(key, "", 60)。
一句话项目/场景
在“电商开放平台”中,爬虫遍历商品 ID(如 ID 自增且无校验),或黑客构造超大 ID(如 id=999999999999),由于数据库中不存在这些记录,导致海量请求直接穿透 Redis 压垮 MySQL。
常见具体场景总结
恶意攻击(空 ID 遍历)
- 描述:攻击者通过脚本批量请求不存在的数据,如负数 ID、超大 ID 或随机字符串 Key。
- 例子:请求
GET /user?id=-1或GET /order?id=99999999。
业务逻辑缺陷(误删或未初始化)
- 描述:业务代码逻辑错误导致请求了本不该请求的数据,或者数据已被物理删除但缓存未同步。
- 例子:商品已下架删除,但前端页面仍有残留链接被用户点击;或者查询了不匹配的复合键(如查询 A 用户的 B 订单,但 B 订单属于 C 用户)。
数据不一致(缓存与库皆空)
- 描述:大量并发请求查询一个“曾经存在但现在被删除”的数据,且删除操作未同步清理缓存(或缓存刚好自然过期),导致瞬间所有请求穿透到数据库。
- 例子:热点新闻被撤稿后,瞬间涌入的大量访问请求。
缓存击穿和缓存雪崩的本质区别是什么?(热点 key 过期 vs 大量 key 同时过期)
一句话原理 缓存击穿是 **“点”**的问题,指单一热点 Key 过期瞬间被高并发穿透;缓存雪崩是 “面” 的问题,指大量 Key 集中过期或 Redis 宕机导致整体缓存层失效。
一句话源码
击穿防护针对特定 Key 加锁 synchronized(key) 或设置逻辑过期;雪崩防护针对 Key 生成策略加随机因子 TTL = Base + Random,避免批量 Key 时间戳重叠。
一句话项目/场景 在“双十一”大促中,某爆款商品 Key 失效导致击穿,仅影响该商品下单;若零点整所有爆款商品缓存同时失效,则引发雪崩,导致整个交易中心数据库崩溃。
核心区别对比
| 维度 | 缓存击穿 | 缓存雪崩 |
|---|---|---|
| 核心差异 | 单个热点 Key 过期 | 大批量 Key 同时过期 |
| 发生时间 | 随机(取决于热点数据访问周期) | 集中(通常发生在系统重启或整点) |
| 影响范围 | 局部(仅该热点数据对应的后端资源) | 全局(整个数据库或系统服务) |
| 根本原因 | 热点数据高并发 + 缓存失效 | 过期时间设置相同 / Redis 服务宕机 |
| 形象比喻 | 大坝的一个小孔漏水(越冲越大) | 大坝整体崩塌 |
解决方案
缓存穿透:如何解决?布隆过滤器的原理是什么?如何设置空值缓存?
一句话原理 缓存穿透的解决核心是“拦截无效请求”或“兜底空结果”:通过布隆过滤器在访问缓存前拦截不存在的 Key,或对查询为空的结果缓存短时效的 Null 值。
一句话源码
在查询逻辑中,先通过 bloomFilter.mightContain(key) 校验,若不存在直接返回;若库查询为空,则调用 redis.set(key, NULL_STRING, TTL_5MIN) 存入空对象。
一句话项目/场景 在“今日头条”文章推荐场景中,针对爬虫遍历不存在的 ArticleID,先用布隆过滤器过滤 99% 的非法请求,剩余漏网之鱼通过缓存空对象防止重复穿透数据库。
详细方案解析
如何解决缓存穿透?
主要有两种主流方案,通常结合使用:
- 方案一:布隆过滤器(推荐,适合数据量大、Key 相对固定的场景,使用之前需要将数据库中所有合法ID全部写入布隆过滤器(“预热”))。
- 方案二:缓存空对象(实现简单,适合 Key 随机性高、攻击量较小的场景)。
布隆过滤器的原理是什么?
布隆过滤器是一种空间效率极高的概率型数据结构,用于判断“一个元素是否在一个集合中”。
- 数据结构:一个很长的二进制位数组,初始全为 0。
- 写入流程:当添加元素时,使用 K 个不同的 Hash 函数对该元素计算哈希值,并对位数组长度取模,将对应的 K 个 bit 位设置为 1。
- 查询流程:当查询元素时,同样用 K 个 Hash 函数计算位置。如果所有位置都是 1,则元素可能存在;如果有任何一个位置是 0,则元素一定不存在。
- 核心特性:
- 不存在则一定不存在(100% 准确)。
- 存在则可能误判(存在哈希冲突,即不同的元素可能映射到相同的 bit 位,称为“假阳性”)。
如何设置空值缓存?
- 实现方式:当数据库查询结果为
null时,不直接返回,而是向 Redis 写入一个特殊值(如"NULL"或"{}")。 - 关键设置:
- Value:设置为业务约定的空标识。
- TTL(过期时间):必须设置较短的过期时间(如 3-5 分钟)。如果永久存储,当该 Key 对应的数据真实存在了(如用户注册了原本不存在的 ID),会导致数据不一致。
- 缺点:如果攻击者每次都使用随机且不存在的 Key,会占用大量 Redis 内存空间。
缓存击穿:如何解决?互斥锁的实现方式?热点数据永不过期策略如何设计?
一句话原理 缓存击穿的解决核心在于“互斥重建”或“逻辑过期”:前者通过锁机制强制串行化数据库查询以牺牲性能换取强一致性,后者通过异步后台更新实现高性能但容忍短暂的数据不一致。
一句话源码
互斥锁方案利用 Redis 的 SETNX(或 Redisson)抢锁,未抢到则 Thread.sleep 重试;逻辑过期方案则在数据中封装 expireTime 字段,过期时获取锁并开启独立线程池异步重建缓存。
一句话项目/场景 在“双十一秒杀商品详情页”场景中,采用逻辑过期策略,保证商品信息永不过期,即使后台数据更新有毫秒级延迟,也能确保数百万并发请求不因缓存重建而阻塞,避免数据库瞬间崩溃。
详细方案解析
1. 如何解决缓存击穿?
主流方案有两种:
- 方案一:互斥锁。强一致性优先,性能较低。
- 方案二:逻辑过期。高性能优先,一致性较弱(推荐用于高并发热点场景)。
2. 互斥锁的实现方式?
核心流程:
- 查询缓存,命中则直接返回。
- 未命中,尝试获取分布式锁(如
Redis setnx lock:key 1)。 - 获取成功:查询数据库 -> 写入缓存 -> 释放锁 -> 返回数据。
- 获取失败:休眠一小段时间(如 50ms) -> 自旋重试(重新查询缓存)。
关键点:
- 必须设置锁的超时时间,防止宕机导致死锁。
- 释放锁时需验证是否是自己持有的锁(Lua 脚本保证原子性),防止误删他人锁。
3. 热点数据永不过期策略如何设计?
这里的“永不过期”是指物理层面(Redis TTL)设置为 -1,但在逻辑层面控制过期。
设计步骤:
- 数据结构改造:
在缓存存储的 Value 中增加一个
expireTime字段。 - 查询逻辑:
- 从 Redis 获取数据(因无 TTL,必然命中)。
- 判断
expireTime是否小于当前系统时间。 - 未过期:直接返回
data。 - 已过期:
- 尝试获取互斥锁。
- 获取成功:开启独立线程(线程池)执行数据库查询和缓存重建,原线程直接返回旧数据。
- 获取失败:说明有线程在重建,原线程直接返回旧数据。
优势:
- 请求永远不会被阻塞,性能极高。
- 利用“旧数据”过渡,保证服务可用性。
缓存雪崩:如何解决?过期时间随机化、多级缓存、熔断降级等策略如何应用?
一句话原理 缓存雪崩的解决核心是“错峰”与“兜底”,通过随机化过期时间避免 Key 集体失效,利用多级缓存构建多层防御体系,并在极限情况下通过熔断降级保护数据库核心链路。
一句话源码
在写入缓存时叠加随机因子 redis.set(key, value, baseTTL + new Random().nextInt(range));读取时采用双重检索 localCache.get(key) -> redis.get(key) -> db;触发阈值时 Sentinel 框架执行 entry.exit() 降级逻辑。
一句话项目/场景 在“电商平台大促”场景中,通过设置随机 TTL 避免整点集中过期,引入 Caffeine 本地缓存作为一级缓存兜底,当 Redis 集群故障时,网关层自动触发熔断返回“系统繁忙”提示,防止数据库被流量打死。
详细策略应用解析
过期时间随机化
- 如何应用: 在设置缓存过期时间时,不要设置固定值(如全部 1 小时),而是在一个基准时间上增加一个随机值。
- 目的: 打散 Key 的过期时间点,避免同一时刻大量 Key 失效,将“瞬间压力峰值”削平为“持续平稳压力”。
- 示例:
TTL = 3600秒 + Random(0, 600秒),使得过期时间分布在 1 小时到 1 小时 10 分钟之间。
多级缓存
- 如何应用:
构建 Nginx 本地缓存 -> 应用层本地缓存 -> 分布式缓存 -> 数据库 的架构体系。
- L1 本地缓存:使用 Guava 或 Caffeine,进程内缓存,速度极快但容量有限。
- L2 分布式缓存:Redis,共享存储,容量大但有网络开销。
- 目的: 当 Redis 宕机或大量 Key 过期时,L1 本地缓存仍能承担部分流量,为数据库提供缓冲期。
- 注意: 需处理多级缓存的一致性问题(通常采用 Redis Pub/Sub 通知各节点更新本地缓存)。
熔断降级
- 如何应用:
- 服务熔断:当检测到 Redis 报错率飙升或数据库响应变慢时,触发熔断机制(如 Sentinel、Hystrix),暂时切断对缓存或数据库的调用,直接返回默认值或错误页面。
- 服务降级:在系统高负荷时,主动关闭非核心业务(如评论、推荐),仅保障核心业务(如下单、支付)的读写。
- 目的: 牺牲部分用户体验(返回降级数据),保护后端数据库和核心服务不被压垮,实现系统的“有损服务”而非“全面瘫痪”。