🔥 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 缓存问题时,不再只是背书,而是能从容地写出高质量的实战代码!

相关推荐
Prince-Peng5 分钟前
技术架构系列 - 详解Kafka
分布式·中间件·架构·kafka·零拷贝·消息中间件·填谷削峰
只是懒得想了5 分钟前
Go语言ORM深度解析:GORM、XORM与entgo实战对比及最佳实践
开发语言·数据库·后端·golang
谢尔登6 分钟前
React19 渲染流程
前端·javascript·架构·ecmascript
爱吃山竹的大肚肚9 分钟前
异步导出方案
java·spring boot·后端·spring·中间件
天空属于哈夫克313 分钟前
企微自动化控制台:跨语言调用与多进程管理的技术架构
架构·自动化·企业微信
三水不滴18 分钟前
从原理、场景、解决方案深度分析Redis分布式Session
数据库·经验分享·redis·笔记·分布式·后端·性能优化
小邓睡不饱耶18 分钟前
Hadoop:从架构原理到企业级实战,大数据处理入门到精通
大数据·hadoop·架构
Hx_Ma1620 分钟前
SpringMVC框架(上)
java·后端
九皇叔叔24 分钟前
【01】微服务系列之 Nacos 安装部署
微服务·云原生·nacos·架构·springboot3
福赖27 分钟前
《微服务即使通讯中RabbitMQ的作用》
c++·微服务·架构·rabbitmq