在 Redis 面试中,缓存穿透、缓存击穿、缓存雪崩是高频必问的 “缓存三兄弟”,也是后端开发中最容易踩坑的三大问题。

很多人能说出概念,却分不清场景;能背出解决方案,却讲不清原理和区别。

缓存穿透

img

缓存穿透是指大量请求查询根本不存在的数据,导致每次请求都无法命中缓存,直接穿透到数据库,最终压垮数据库的问题。


一、通俗理解

就像一家奶茶店,有人一直问“有没有卖不存在的隐藏款奶茶”,店员每次都要去仓库确认,最后被这些无效请求累垮。

二、专业定义
  • 核心特征:查询的 key 在缓存和数据库中都不存在。
  • 典型场景:恶意攻击(用大量不存在的 ID 刷接口)、业务逻辑漏洞(参数校验不严)。
  • 风险:数据库被大量无效查询打满,影响正常业务。

三、解决方案(结合项目实战)
方案1:缓存空值(简单直接)
  • 思路:对查询结果为空的 key,也在缓存中存一个空值(或占位符),并设置短过期时间(如 60 秒)。
  • 优点:实现简单,快速见效。
  • 缺点:会占用额外缓存空间,且空值过期后仍可能穿透。
public Object getData(String key) {
    Object value = redisTemplate.opsForValue().get(key);
    if (value != null) {
        return value;
    }

    // 缓存未命中,查数据库
    Object data = dbMapper.queryData(key);
    if (data == null) {
        // 缓存空值,设置短过期时间
        redisTemplate.opsForValue().set(key, "NULL", 60, TimeUnit.SECONDS);
        return null;
    } else {
        redisTemplate.opsForValue().set(key, data, 30, TimeUnit.MINUTES);
        return data;
    }
}
方案2:布隆过滤器(高效过滤)

img

  • 思路:提前将所有可能存在的 key 存入布隆过滤器,请求先过过滤器,不存在则直接返回。
  • 优点:空间效率极高,能过滤 99% 以上的无效请求。

img

  • 缺点:存在极小的误判率(过滤器说存在,实际可能不存在),但绝不会漏判。
// 初始化布隆过滤器,预期元素数量100万,误判率0.1%
BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), 1000000, 0.001);

// 项目启动时,将所有有效key加载到布隆过滤器
public void initBloomFilter() {
    List<String> allKeys = dbMapper.getAllKeys();
    bf.addAll(allKeys);
}

public Object getData(String key) {
    // 先过布隆过滤器
    if (!bf.mightContain(key)) {
        return null; // 不存在,直接返回
    }

    // 后续流程:查缓存 -> 查数据库 -> 回写缓存
    // ...
}
方案3:接口层校验(源头拦截)
  • 思路:在 API 入口层对请求参数做合法性校验,比如 ID 范围、格式校验,直接拦截非法请求。
  • 优点:从源头杜绝无效请求,成本最低。
  • 实战:比如商品 ID 必须是正整数,且在系统已存在的 ID 范围内。

四、建议
  • 简单场景:优先用缓存空值,快速解决问题。
  • 高并发/恶意攻击场景:用布隆过滤器 + 参数校验,双重防护。
  • 终极方案:多层防护,参数校验 + 布隆过滤器 + 缓存空值,从源头到缓存全链路拦截。

缓存击穿

一、什么是缓存击穿?

img

就像一家网红奶茶店,只有一款爆款奶茶(热点key)特别火,所有人都来买这款。结果某天这款奶茶的原料刚好用完了(热点key过期),瞬间所有顾客都堵在收银台问“能不能做”,收银台(数据库)被问得手忙脚乱,直接卡壳——这就是缓存击穿:单个热点key在过期的瞬间,大量并发请求直接打穿缓存,冲击数据库

二、专业定义+核心场景

核心定义:缓存击穿是指某个高频访问的热点key,在其缓存有效期失效的瞬间,恰好有大量并发请求访问该key;由于缓存未命中,所有请求全部落到数据库上,导致数据库短时间内承受巨大压力,甚至宕机。
典型场景

  1. 电商秒杀的商品详情key、首页热点数据key;
  2. 突发流量的热点内容(比如热搜词条、爆款商品);
  3. 缓存key设置了统一过期时间,且该key是全量请求的核心依赖。
三、解决方案(从易到难,结合实战+专业术语)

我们项目里针对不同场景用了不同方案,优先级从高到低:

img

1. 方案1:热点key永不过期 + 后台定时刷新(首选)

通俗解释:把爆款奶茶的原料提前备足,永远不缺货(key不过期),店员(后台线程)每隔一段时间检查原料库存,快用完了就补货(定时刷新缓存)。
专业实现

  • 对热点key不设置过期时间,避免过期瞬间的请求冲击;
  • 启动独立的后台线程(比如Spring的@Scheduled),按固定频率主动查询数据库,更新Redis中的热点key值;
  • 优点:零并发风险,实现简单;缺点:需提前识别热点key,少量内存占用(但热点key通常数量少,可接受)。
2. 方案2:互斥锁(分布式锁)控制缓存重建(通用方案)

通俗解释:奶茶原料用完后,只允许一个顾客(线程)去仓库补货(重建缓存),其他顾客排队等待,补货完成后所有人都能买到奶茶。
专业实现

  • 当请求发现缓存未命中时,先通过Redis的SETNX(或Redisson的RLock)获取分布式锁;
  • 只有拿到锁的线程去数据库查询数据,更新缓存;
  • 未拿到锁的线程等待一小段时间(比如50ms)后重试,此时缓存已被重建,直接命中缓存;
    核心代码(Redisson实现)
public String getHotData(String key) {
    // 1. 先查缓存
    String value = redisTemplate.opsForValue().get(key);
    if (StringUtils.isNotBlank(value)) {
        return value;
    }

    // 2. 缓存未命中,加分布式锁
    RLock lock = redissonClient.getLock("lock:cache:" + key);
    try {
        // 尝试加锁,等待1秒,锁过期30秒(防止死锁)
        if (lock.tryLock(1, 30, TimeUnit.SECONDS)) {
            // 3. 拿到锁后,再次查缓存(双重检查,避免重复重建)
            value = redisTemplate.opsForValue().get(key);
            if (StringUtils.isBlank(value)) {
                // 4. 查数据库,重建缓存(设置过期时间,兜底)
                value = dbMapper.queryData(key);
                redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
            }
            return value;
        } else {
            // 5. 未拿到锁,重试(短休眠后查缓存)
            Thread.sleep(50);
            return getHotData(key);
        }
    } catch (Exception e) {
        log.error("获取热点数据异常", e);
        // 兜底:直接查数据库(避免服务不可用)
        return dbMapper.queryData(key);
    } finally {
        // 释放锁(只释放当前线程持有的锁)
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}
四、总结

总结一下,缓存击穿的核心是“热点key过期+高并发”,我们项目里的选型原则是:

  1. 能提前识别的热点key(比如秒杀商品),用“永不过期+后台刷新”,最简单高效;
  2. 无法提前识别的热点key,用“分布式锁+双重检查”,通用且可靠;
  3. 超大规模热点场景,叠加布隆过滤器兜底;
    同时我们会监控Redis的缓存命中率、数据库慢查询,提前预警热点key,从源头减少击穿风险。
核心加分点
  1. 结合实战:不说纯理论,而是讲“我们项目里用了XX方案”,体现落地能力;
  2. 双重检查:在加锁后再次查缓存,这是面试里的细节加分项(避免多线程等待锁后重复查库);
  3. 兜底思维:代码里考虑“释放锁的边界条件”“最终兜底查数据库”,体现健壮性;
  4. 监控预警:最后提到监控,体现全链路思维(不止解决问题,还能预防问题)。

缓存雪崩

img

缓存雪崩是指大量缓存Key在同一时间集中过期失效,或者Redis服务本身宕机,导致原本由缓存承载的请求瞬间全部压到数据库,从而引发数据库压力过载甚至宕机的问题。


一、通俗理解

就像景区里所有的检票闸机(缓存)同时坏了,所有游客(请求)瞬间都涌向唯一的人工窗口(数据库),导致窗口被挤爆,整个景区瘫痪。

二、专业定义
  • 核心特征:大量Key同时失效或缓存服务不可用,导致请求雪崩式涌向数据库。
  • 典型场景
    1. 大量Key设置了相同的过期时间,导致集中过期。
    2. Redis集群故障、主从切换失败,导致缓存层不可用。
    3. 突发流量(如促销活动),放大了缓存失效的影响。
  • 风险:数据库被瞬间打满,服务雪崩,影响整个系统的可用性。

三、解决方案(结合项目实战)

img

方案1:过期时间打散(最常用,解决集中过期)
  • 思路:在设置Key的过期时间时,加上一个随机偏移量,避免大量Key在同一时间过期。
  • 优点:实现简单,效果显著,是解决缓存雪崩最基础的手段。
// 基础过期时间30分钟,加上0-300秒的随机偏移
long baseExpire = 30 * 60;
long randomOffset = new Random().nextInt(300);
long finalExpire = baseExpire + randomOffset;

redisTemplate.opsForValue().set(key, value, finalExpire, TimeUnit.SECONDS);
方案2:多级缓存架构(兜底防护)
  • 思路:构建“本地缓存(如Caffeine)+ 分布式缓存(Redis)”的多级架构。
  • 流程:请求先查本地缓存 -> 再查Redis -> 最后查数据库。当Redis失效时,本地缓存可以继续提供服务,为数据库减压。
  • 优点:提供了强大的兜底能力,即使Redis宕机,系统仍能提供降级服务。
方案3:服务熔断与降级(流量控制)
  • 思路:使用Sentinel或Hystrix等组件,对数据库访问进行限流、熔断。当数据库压力超过阈值时,直接返回降级数据(如默认值、缓存的旧数据),防止数据库被打垮。
  • 优点:从流量控制层面保护核心服务,保证系统在极端情况下的可用性。
方案4:缓存预热与永不过期(热点Key)
  • 思路
    1. 缓存预热:在系统启动或流量高峰前,主动将热点数据加载到缓存中。
    2. 永不过期:对核心热点Key不设置过期时间,而是通过后台定时任务主动更新。
  • 优点:从源头避免热点Key过期,彻底杜绝因热点Key集中失效导致的雪崩。
方案5:Redis高可用部署(避免缓存服务宕机)
  • 思路:采用Redis集群(Cluster)、主从复制(Master-Slave)+哨兵(Sentinel)模式,实现Redis的高可用。
  • 优点:当某个节点故障时,能自动进行主从切换,保证缓存服务持续可用,从根本上避免因缓存服务不可用导致的雪崩。

四、对比缓存击穿

与缓存击穿的区别:核心差异在于影响的 Key 数量不同

  • 缓存雪崩:针对多个 Key,是大量缓存同时失效引发的问题;
  • 缓存击穿:针对单个热点 Key,是单个 Key 过期瞬间,大量并发请求打穿缓存引发的问题。
五、建议
  • 预防为主:通过过期时间打散热点Key永不过期,从源头减少缓存失效的概率。
  • 兜底为辅:构建多级缓存服务熔断降级,即使发生雪崩,也能保证核心服务可用。
  • 架构保障:部署Redis高可用集群,避免单点故障,提升整个缓存层的可靠性。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐