Redis缓存穿透、击穿、雪崩相关问题

总结摘要
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。


常见具体场景总结

  1. 恶意攻击(空 ID 遍历)

    • 描述:攻击者通过脚本批量请求不存在的数据,如负数 ID、超大 ID 或随机字符串 Key。
    • 例子:请求 GET /user?id=-1GET /order?id=99999999
  2. 业务逻辑缺陷(误删或未初始化)

    • 描述:业务代码逻辑错误导致请求了本不该请求的数据,或者数据已被物理删除但缓存未同步。
    • 例子:商品已下架删除,但前端页面仍有残留链接被用户点击;或者查询了不匹配的复合键(如查询 A 用户的 B 订单,但 B 订单属于 C 用户)。
  3. 数据不一致(缓存与库皆空)

    • 描述:大量并发请求查询一个“曾经存在但现在被删除”的数据,且删除操作未同步清理缓存(或缓存刚好自然过期),导致瞬间所有请求穿透到数据库。
    • 例子:热点新闻被撤稿后,瞬间涌入的大量访问请求。

缓存击穿和缓存雪崩的本质区别是什么?(热点 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. 互斥锁的实现方式?

核心流程

  1. 查询缓存,命中则直接返回。
  2. 未命中,尝试获取分布式锁(如 Redis setnx lock:key 1)。
  3. 获取成功:查询数据库 -> 写入缓存 -> 释放锁 -> 返回数据。
  4. 获取失败:休眠一小段时间(如 50ms) -> 自旋重试(重新查询缓存)。

关键点

  • 必须设置锁的超时时间,防止宕机导致死锁。
  • 释放锁时需验证是否是自己持有的锁(Lua 脚本保证原子性),防止误删他人锁。

3. 热点数据永不过期策略如何设计?

这里的“永不过期”是指物理层面(Redis TTL)设置为 -1,但在逻辑层面控制过期。

设计步骤

  1. 数据结构改造: 在缓存存储的 Value 中增加一个 expireTime 字段。
    1
    2
    3
    4
    
    {
      "data": { "id": 1, "name": "iPhone 15" }, // 实际业务数据
      "expireTime": 1709500800000 // 逻辑过期时间戳
    }
  2. 查询逻辑
    • 从 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),暂时切断对缓存或数据库的调用,直接返回默认值或错误页面。
    • 服务降级:在系统高负荷时,主动关闭非核心业务(如评论、推荐),仅保障核心业务(如下单、支付)的读写。
  • 目的: 牺牲部分用户体验(返回降级数据),保护后端数据库和核心服务不被压垮,实现系统的“有损服务”而非“全面瘫痪”。