🔥 Redis 缓存穿透、击穿、雪崩:别再只背八股文了,实战代码教你彻底解决!

在面试中,Redis 的"缓存三兄弟"(穿透、击穿、雪崩)是必问考题。很多同学背得滚瓜烂熟,但一到项目实战,代码写得漏洞百出,线上故障频发。

"老板,我的系统崩了,数据库 CPU 飙到 100% 了!"

"不是加了 Redis 缓存吗?"

"呃......好像没防住......"

今天,我们不讲枯燥的理论,直接上实战场景 和代码方案,带你彻底搞定这三个缓存杀手,让你的系统稳如老狗。

🛠️ 场景假设

我们有一个电商系统,有一个查询商品详情的接口 getProduct(Long id)。

  • 数据库:MySQL
  • 缓存:Redis
  • 并发量:高并发场景

1. 👻 缓存穿透 (Cache Penetration)

现象 :

查询一个根本不存在 的数据(例如 id = -1)。

Redis 查不到 -> 去查数据库 -> 数据库也查不到。

如果有黑客利用大量不存在的 ID 发起攻击,请求会全部打到数据库,直接把数据库打挂。

错误写法:

Java 复制代码
public Product getProduct(Long id) {
    // 1. 查缓存
    Product product = redisTemplate.opsForValue().get("product:" + id);
    if (product != null) {
        return product;
    }
    // 2. 查数据库
    product = productMapper.selectById(id);
    // 3. 写入缓存
    if (product != null) {
        redisTemplate.opsForValue().set("product:" + id, product);
    }
    return product;
}

问题:如果 product 为 null,就不会写缓存。下次查 id=-1,还是会打到数据库。

✅ 解决方案 A:缓存空对象 (简单有效)

即使数据库查不到,也往 Redis 存一个 null 或特殊标记,并设置一个较短的过期时间(防止数据后来真的有了)。

Java 复制代码
public Product getProduct(Long id) {
    String key = "product:" + id;
    // 1. 查缓存
    Object cacheValue = redisTemplate.opsForValue().get(key);
    if (cacheValue != null) {
        // 如果是空对象标记,直接返回 null
        if (cacheValue instanceof NullValue) {
            return null;
        }
        return (Product) cacheValue;
    }
    
    // 2. 查数据库
    Product product = productMapper.selectById(id);
    
    // 3. 写入缓存
    if (product != null) {
        redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
    } else {
        // 关键点:数据库没查到,也缓存一个空对象,过期时间设置短一点(例如 5 分钟)
        redisTemplate.opsForValue().set(key, new NullValue(), 5, TimeUnit.MINUTES);
    }
    return product;
}

✅ 解决方案 B:布隆过滤器 (Bloom Filter) (进阶)

在访问 Redis 之前,先用布隆过滤器判断 ID 是否存在。如果布隆过滤器说"不存在",那就一定不存在,直接返回,连 Redis 都不用查。

适合数据量巨大,且 ID 相对固定的场景。

2. 🔨 缓存击穿 (Cache Breakdown)

现象 :

一个热点 Key (例如"iPhone 16 发布"),在过期的一瞬间 ,有大量并发请求同时访问。

Redis 没数据 -> 大量请求同时涌入数据库。

数据库瞬间压力过大崩溃。

错误写法 :

同上,普通的 get -> db -> set 逻辑无法防止并发击穿。

✅ 解决方案:互斥锁 (Mutex Lock)

当缓存失效时,不是所有线程都去查数据库,而是只让一个线程去查,其他线程等待。

Java 复制代码
public Product getProduct(Long id) {
    String key = "product:" + id;
    String lockKey = "lock:product:" + id;
    
    // 1. 查缓存
    Product product = (Product) redisTemplate.opsForValue().get(key);
    if (product != null) {
        return product;
    }
    
    // 2. 缓存未命中,尝试获取分布式锁
    // setIfAbsent 相当于 SETNX,原子操作
    Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    
    if (isLock) {
        try {
            // 3. 获取锁成功,查询数据库
            product = productMapper.selectById(id);
            // 4. 重建缓存
            if (product != null) {
                redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
            } else {
                // 防止穿透
                redisTemplate.opsForValue().set(key, new NullValue(), 5, TimeUnit.MINUTES);
            }
        } finally {
            // 5. 释放锁
            redisTemplate.delete(lockKey);
        }
    } else {
        // 6. 获取锁失败,说明有其他线程正在查库,休眠一会再重试
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getProduct(id); // 递归重试
    }
    return product;
}

注:生产环境建议使用 Redisson 实现更健壮的分布式锁。

3. ❄️ 缓存雪崩 (Cache Avalanche)

现象 :

大量缓存 Key 在同一时间集中过期 ,或者 Redis 宕机 。

此刻大量请求全部打到数据库,导致数据库挂掉。

错误写法:

Java 复制代码
// 所有商品都设置固定的 30 分钟过期
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);

✅ 解决方案 A:随机过期时间

给每个 Key 的过期时间加上一个随机值,让失效时间分散开来。

codeJava

ini 复制代码
// 基础过期时间 30 分钟
long expireTime = 30 * 60; 
// 随机增加 0-300 秒
long randomTime = new Random().nextInt(300); 

redisTemplate.opsForValue().set(key, product, expireTime + randomTime, TimeUnit.SECONDS);

✅ 解决方案 B:高可用架构

  • Redis 哨兵 (Sentinel) 或 集群 (Cluster) :防止 Redis 单点故障。
  • 限流降级 (Sentinel/Hystrix) :如果 Redis 真的挂了,限制访问数据库的流量,或者直接返回默认值/错误提示,保住数据库。

📝 总结一张表

问题 核心原因 解决方案 关键词
穿透 查不存在的数据 缓存空对象、布隆过滤器 Null Object, Bloom Filter
击穿 热点 Key 过期 互斥锁、逻辑过期 Mutex Lock, SETNX
雪崩 大量 Key 同时过期 随机过期时间、高可用集群 Random TTL, Cluster

💡 架构师建议

  1. 代码健壮性:不要只依赖 Redis,数据库层也要有兜底保护(如限流)。
  2. 监控告警:对 Redis 的命中率、内存使用率、慢查询进行监控,提前发现异常。
  3. 不要过度设计:如果是内部小系统,并发量很低,简单的缓存逻辑就够了,不用非得上布隆过滤器和分布式锁。

希望这篇文章能让你在面对 Redis 缓存问题时,不再只是背书,而是能从容地写出高质量的实战代码!

相关推荐
Decay__18 分钟前
05_MySQL 常用 SQL 大全 —— 诊断、管理、优化辅助 SQL 与三大系统库速查
运维·数据库·sql·mysql·架构
代码山河3 小时前
Python就业方向全解析:后端、爬虫、数据分析、AI你选哪个?
python·学习·架构·教程·面向对象·项目
学长毕设3 小时前
基于SpringBoot的实验室资产管理系统
java·spring boot·后端
微订校园外卖平台4 小时前
校园外卖平台选模板还是定制:架构、数据流与验收清单
大数据·架构·外卖系统·本地生活·微订
努力的小雨5 小时前
给 AI 眼镜开发应用有多难?我从零做了一个陪练,还上架了
后端
ZhengEnCi5 小时前
用 AI 写代码为什么拼到最后还是拼人:算力瓶颈与领域专家提示词的本质差异
算法·架构
Decay__6 小时前
04_MySQL DBA 日常运维手册 —— 会话管理、锁与阻塞、事务监控、慢查询与日志巡检
运维·数据库·sql·mysql·架构
excel6 小时前
双 Token 与前端使用 Crypto 公私钥鉴权换 Token,哪种方案更好?
前端·后端
AI入职记7 小时前
AI 代码审查召回率实测:20 个骗过单元测试的变异体,抓到 13 个
后端
浮链序7 小时前
别让 AI 客服把用户当猴耍:百来行 Python 把「转人工」做成一条高可用链路
后端