一文搞懂:缓存三大问题(击穿、穿透、雪崩)原理及全套解决方案

缓存的击穿、穿透、雪崩是后端开发中高频出现、极易引发服务雪崩的核心问题,三者看似相似但原理和解决方案完全不同。本文用「原理+场景+落地方案+代码」的形式,一次性讲透,看完就能直接落地到生产环境。

先分清:三大问题核心区别(一张表看懂)

问题类型 核心定义 典型场景 直接后果
缓存穿透 请求根本不存在的key,缓存和DB都查不到 恶意攻击(批量查不存在的用户ID)、业务bug(传错参数) DB被大量无效请求打满,连接耗尽
缓存击穿 热点key突然失效(过期/被删除),大量请求瞬间打到DB 秒杀商品key过期、热门榜单key被删除 DB单点压力骤增,瞬间超时/宕机
缓存雪崩 大量缓存key同时失效,或缓存服务整体宕机 缓存key集中设置相同过期时间、Redis集群宕机 DB被海量请求压垮,整个服务不可用

一、缓存穿透:查「不存在的key」把DB打崩

1. 原理

请求的key在缓存中不存在,且在数据库中也不存在,导致每次请求都「穿透」缓存直接打在DB上。如果是恶意高频请求(比如每秒上万次查不存在的用户ID),DB很快会被压垮。

2. 全套解决方案(按优先级排序)

方案1:参数校验(最基础,成本最低)

在请求到达缓存/DB前,先做合法性校验,过滤掉明显无效的请求:

  • 比如用户ID是正整数,直接拦截负数/0/超长字符串;
  • 比如商品ID有固定格式,拦截不符合格式的请求。

代码示例(Java)

public User getUserById(Long userId) {
    // 第一步:参数合法性校验,直接拦截无效请求
    if (userId == null || userId <= 0 || userId > 10000000) {
        return null;
    }
    // 后续缓存/DB查询逻辑...
}

方案2:空值缓存(核心方案)

对查询结果为空的key,也写入缓存(设置极短的过期时间,比如1-5分钟),避免后续请求重复打DB。

代码示例(Java + Redis)

public User getUserById(Long userId) {
    String cacheKey = "user:" + userId;
    // 1. 查缓存
    String userJson = redisTemplate.opsForValue().get(cacheKey);
    // 空值标记(避免JSON解析问题,用特定字符串表示空)
    if ("NULL".equals(userJson)) {
        return null;
    }
    if (userJson != null) {
        return JSON.parseObject(userJson, User.class);
    }
    
    // 2. 缓存未命中,查DB
    User user = userMapper.selectById(userId);
    if (user == null) {
        // 3. 空值写入缓存,设置短过期时间(防止恶意攻击占满缓存)
        redisTemplate.opsForValue().set(cacheKey, "NULL", 5, TimeUnit.MINUTES);
        return null;
    }
    
    // 4. 正常数据写入缓存
    redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
    return user;
}

方案3:布隆过滤器(高并发/海量key场景)

如果无效key的范围极大(比如电商场景查不存在的商品ID),用布隆过滤器提前判断key是否存在,不存在则直接返回,完全拦截无效请求。

核心逻辑

  1. 启动时将DB中所有有效key加载到布隆过滤器;
  2. 请求过来先过布隆过滤器,不存在则直接返回;
  3. 存在则走正常缓存/DB流程。

代码示例(Guava布隆过滤器)

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

// 初始化布隆过滤器(预计100万key,误判率0.01)
private static BloomFilter<Long> userBloomFilter = BloomFilter.create(
    Funnels.longFunnel(),
    1000000,
    0.01
);

// 启动时加载所有有效用户ID到过滤器
@PostConstruct
public void initBloomFilter() {
    List<Long> allUserId = userMapper.selectAllUserId();
    for (Long userId : allUserId) {
        userBloomFilter.put(userId);
    }
}

public User getUserById(Long userId) {
    // 第一步:布隆过滤器拦截无效key
    if (!userBloomFilter.mightContain(userId)) {
        return null;
    }
    // 后续缓存/DB查询逻辑...
}

方案4:限流/熔断(兜底方案)

用Sentinel/Hystrix对接口做限流,当QPS超过阈值时直接拒绝;或监控DB压力,达到阈值时熔断缓存穿透的请求,避免DB被打垮。

缓存穿透方案总结

  • 基础:参数校验
  • 核心:空值缓存(中小规模)、布隆过滤器(大规模)
  • 兜底:限流/熔断

二、缓存击穿:热点key失效导致DB单点压力暴增

1. 原理

某个高频访问的「热点key」(比如秒杀商品、热门榜单)突然过期/被删除,大量请求瞬间绕过缓存直接访问DB,导致DB单点压力骤增,甚至宕机。

2. 全套解决方案(按优先级排序)

方案1:热点key永不过期(最简单)

对核心热点key,不设置过期时间,由业务代码主动更新/删除,避免被动过期。

注意:需在代码中保证热点key的更新逻辑(比如商品价格修改时,主动更新缓存),防止缓存脏数据。

方案2:互斥锁(分布式锁)(最通用)

当缓存失效时,不是所有请求都去查DB,而是只有一个请求获取锁后去查DB并更新缓存,其他请求等待锁释放后直接查缓存。

代码示例(Redis分布式锁)

public User getHotUserById(Long userId) {
    String cacheKey = "hot_user:" + userId;
    String lockKey = "lock:hot_user:" + userId;
    // 1. 查缓存
    String userJson = redisTemplate.opsForValue().get(cacheKey);
    if (userJson != null) {
        return JSON.parseObject(userJson, User.class);
    }
    
    // 2. 缓存失效,尝试获取分布式锁
    Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
    if (lockSuccess) {
        try {
            // 3. 获取锁成功,查DB并更新缓存
            User user = userMapper.selectById(userId);
            if (user != null) {
                redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), 60, TimeUnit.MINUTES);
            }
            return user;
        } finally {
            // 4. 释放锁
            redisTemplate.delete(lockKey);
        }
    } else {
        // 5. 未获取锁,等待50ms后重试
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        // 递归重试
        return getHotUserById(userId);
    }
}

方案3:提前预热(主动更新)

在热点key过期前,主动查询DB并更新缓存,避免过期瞬间的请求冲击。

  • 比如定时任务:在key过期前10分钟,主动加载最新数据到缓存;
  • 比如监控热点key的过期时间,临近过期时触发更新。

方案4:本地缓存(多级缓存)

对极致热点key,增加「本地缓存」(比如Caffeine/Guava Cache),请求先查本地缓存,再查分布式缓存,最后查DB,进一步降低分布式缓存失效的影响。

缓存击穿方案总结

  • 简单版:热点key永不过期
  • 通用版:分布式互斥锁
  • 进阶版:提前预热 + 多级缓存

三、缓存雪崩:大量key同时失效/缓存宕机导致服务雪崩

1. 原理

两种场景会引发缓存雪崩:

  1. 大量缓存key在同一时间段集中过期,海量请求瞬间打向DB;
  2. 缓存服务(如Redis集群)整体宕机,所有请求直接穿透到DB。

最终导致DB压力暴增,服务不可用,甚至整个系统雪崩。

2. 全套解决方案(分场景应对)

场景1:大量key集中过期 → 打散过期时间

核心思路:给每个key的过期时间增加随机值,避免所有key同时过期。

代码示例(Java)

// 基础过期时间30分钟,随机增加0-10分钟
int baseExpire = 30;
int randomExpire = new Random().nextInt(10);
redisTemplate.opsForValue().set(cacheKey, value, baseExpire + randomExpire, TimeUnit.MINUTES);

场景2:缓存服务宕机 → 提高缓存可用性

  1. 缓存集群化:使用Redis主从+哨兵/Redis Cluster,避免单点故障;
  2. 熔断降级:用Sentinel/Hystrix监控缓存服务状态,当缓存不可用时,暂时熔断请求(返回默认值/提示),避免DB被打垮;
  3. 限流:对所有请求做限流,即使缓存宕机,DB也只接收可控的请求量;
  4. 多级缓存:增加本地缓存(Caffeine),即使分布式缓存宕机,本地缓存仍能承接部分请求;
  5. 缓存降级:缓存宕机时,返回兜底数据(如商品默认价格、空列表),保证服务不挂。

代码示例(熔断降级伪代码)

public User getUserById(Long userId) {
    try {
        // 先查分布式缓存
        String userJson = redisTemplate.opsForValue().get("user:" + userId);
        if (userJson != null) {
            return JSON.parseObject(userJson, User.class);
        }
    } catch (Exception e) {
        // 缓存异常,触发降级,返回本地缓存/兜底数据
        log.error("缓存异常,触发降级", e);
        return getLocalCacheUser(userId); // 本地缓存兜底
    }
    // 缓存异常/未命中,查DB(同时限流)
    return userMapper.selectById(userId);
}

场景3:终极兜底 → DB层防护

即使缓存完全不可用,也要保证DB不被打垮:

  • DB读写分离/分库分表,提高DB处理能力;
  • DB加连接池限流,避免连接数耗尽;
  • 对DB查询做缓存(比如MyBatis一级/二级缓存)。

缓存雪崩方案总结

  • 预防集中过期:过期时间加随机值
  • 提高缓存可用性:集群化 + 熔断降级 + 多级缓存
  • 终极兜底:DB层限流 + 读写分离

四、三大问题核心方案对比表

问题类型 核心原因 首选方案 兜底方案
缓存穿透 查不存在的key 空值缓存(中小规模)、布隆过滤器(大规模) 参数校验 + 限流
缓存击穿 热点key失效 分布式互斥锁 热点key永不过期
缓存雪崩 大量key过期/缓存宕机 过期时间随机化 + 缓存集群化 熔断降级 + DB限流

总结

  1. 缓存穿透:核心是拦截无效请求,用空值缓存/布隆过滤器挡住不存在的key;
  2. 缓存击穿:核心是保护热点key,用分布式锁避免并发查DB,或直接设置永不过期;
  3. 缓存雪崩:核心是提高可用性 + 打散风险,过期时间随机化防集中失效,集群化+熔断降级防缓存宕机;
  4. 所有方案都需配合限流/熔断作为兜底,确保极端场景下服务不雪崩。

如果需要某类方案的完整生产级代码(比如Redis分布式锁、布隆过滤器落地、Sentinel限流配置),可以告诉我,我会补充对应的可直接运行的代码。

posted @ 2026-03-08 16:51  七星6609  阅读(3)  评论(0)    收藏  举报