【从零开始——Redis 进化日志|Day6】缓存的三剑客:穿透、击穿、雪崩,到底怎么防?(附生产级代码实战)

兄弟们,欢迎来到 Redis 进化日志的第六天。在 Day 5 里,我们用分布式锁解决了"超卖"问题。现在系统看起来固若金汤了对吧?

错!

在真实的互联网生产环境中,Redis 并不总是我们的守护神。如果使用姿势不对,它反而会成为击垮数据库的"帮凶"。

当海量请求绕过 Redis 直接轰炸脆弱的 MySQL 时,这种现象统称为缓存失效问题。

最经典的三个场景就是:穿透、击穿、雪崩。


一、 缓存穿透

1. 深度解析:什么是穿透?

核心定义 :请求的数据在 Redis 中不存在 ,且在 数据库中也不存在

场景模拟

  • 恶意攻击 :黑客写了个脚本,疯狂请求 id = -1id = uuid 这种数据库里绝对没有的数据。

  • 业务误操作:前端代码 Bug,传了一个不存在的商品 ID。

后果:

这就好比你去饭店点菜,非要点"女神的眼泪"。服务员(Redis)说没有,你就冲进厨房(DB)去找厨师。你每一次问,厨师都要停下来找一圈。如果有 10 万个人同时点"女神的眼泪",厨师(DB)直接累死,导致正常想吃"西红柿炒蛋"的客人也无法服务。

2. 解决方案 & 实战代码

方案 A:缓存空对象 (Cache Null)

即便 DB 查不到,我也存一个 null 或特定标识符到 Redis,并设置较短的过期时间(TTL)。

方案 B:布隆过滤器 (Bloom Filter) ------ 推荐

原理:在请求到达 Redis 之前,先加一道"安检门"。布隆过滤器利用位数组和哈希函数,能快速判断"这个 Key 一定不存在"或"可能存在"。如果它说不存在,直接驳回请求。

生产级代码(基于 Redisson):

java 复制代码
@Service
public class ProductService {

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private ProductMapper productMapper;

    private RBloomFilter<String> bloomFilter;

    // 1. 初始化布隆过滤器(通常在系统启动时预热)
    @PostConstruct
    public void initBloomFilter() {
        bloomFilter = redissonClient.getBloomFilter("product-id-filter");
        // 初始化参数:预计元素 100万,误差率 3%
        // 注意:误差率越低,占用的位数组越大,性能略微下降
        bloomFilter.tryInit(1000000L, 0.03);
        
        // 模拟预热:将所有存在的商品 ID 加载进去
        List<String> allIds = productMapper.selectAllIds();
        for (String id : allIds) {
            bloomFilter.add(id);
        }
    }

    public Product getProduct(String id) {
        String cacheKey = "product:" + id;

        // 【第一道防线】:布隆过滤器拦截
        // 如果布隆过滤器说不存在,那就一定不存在,直接返回,保护 Redis 和 DB
        if (!bloomFilter.contains(id)) {
            log.warn("非法请求,布隆过滤器拦截: {}", id);
            return null; 
        }

        // 【第二道防线】:查 Redis
        String json = redisTemplate.opsForValue().get(cacheKey);
        if (StringUtils.isNotBlank(json)) {
            // 如果存的是空值标识(防止缓存穿透的兜底方案),直接返回
            if ("EMPTY_OBJECT".equals(json)) {
                return null;
            }
            return JSON.parseObject(json, Product.class);
        }

        // 【第三道防线】:查 DB(代码见下文击穿部分)
        // ...
        return null;
    }
}

二、 缓存击穿

1. 深度解析:什么是击穿?

核心定义 :Redis 中某个 热点 Key (比如"当季爆款")在某一瞬间 刚好过期 ,而此时恰好有 海量并发请求 同时访问这个 Key。

区别

  • 穿透:查"根本不存在"的数据。

  • 击穿:查"存在但过期"的数据。

后果:

Redis 这个盾牌瞬间破了一个洞,所有流量像子弹一样穿过这个洞打在 DB 上。DB 的 CPU 和内存瞬间飙升,连接池被占满。

2. 解决方案 & 实战代码

方案:互斥锁 (Mutex Lock)

当发现缓存失效时,不是所有人都去查库。而是只有拿到锁的那一个线程去查库、写缓存,其他线程等待(自旋)或稍后重试。

生产级代码(双重检查锁模式):

java 复制代码
public Product queryWithMutex(String id) {
    String cacheKey = "product:" + id;
    
    // 1. 先查缓存
    String json = redisTemplate.opsForValue().get(cacheKey);
    if (StringUtils.isNotBlank(json)) {
        return JSON.parseObject(json, Product.class);
    }

    // 2. 缓存未命中,准备重建缓存。定义分布式锁 Key
    String lockKey = "lock:product:" + id;
    RLock lock = redissonClient.getLock(lockKey);

    try {
        // 3. 尝试加锁(阻塞等待 5秒,或者只试一次)
        boolean isLocked = lock.tryLock(5, TimeUnit.SECONDS);
        if (isLocked) {
            try {
                // 【关键点】:Double Check (双重检查)
                // 为什么?因为在你排队等锁的时候,可能前一个人已经把数据查出来塞进 Redis 了
                // 如果不查,你又会去查一遍 DB,这就浪费了
                json = redisTemplate.opsForValue().get(cacheKey);
                if (StringUtils.isNotBlank(json)) {
                    return JSON.parseObject(json, Product.class);
                }

                // 4. 真的没人查过,我去查 DB
                Product product = productMapper.selectById(id);
                
                // 5. 写入 Redis(记得处理空值,防止穿透)
                if (product == null) {
                    redisTemplate.opsForValue().set(cacheKey, "EMPTY_OBJECT", 5, TimeUnit.MINUTES);
                } else {
                    redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
                }
                return product;
            } finally {
                // 6. 释放锁
                lock.unlock();
            }
        } else {
            // 7. 没抢到锁(理论上 tryLock 会阻塞,走到这也是异常情况),降级或重试
            Thread.sleep(50);
            return queryWithMutex(id); // 递归重试
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("系统繁忙");
    }
}

三、 缓存雪崩

1. 深度解析:什么是雪崩?

核心定义

  1. 场景 A(绝望):Redis 节点宕机,所有缓存不可用。

  2. 场景 B(常见)大量 的 Key 在 同一时间 集中过期。

后果:

想象一下雪崩的场景,漫天的雪(请求)直接压塌了房子(数据库)。DB 瞬间承受平时的几十倍压力,直接报警宕机。

2. 解决方案 & 实战代码

方案 A:打散过期时间 (Random TTL)

在存入 Redis 时,不要让所有 Key 都活 1 小时。给它们加一个 随机值。

代码演示:

java 复制代码
// 设置缓存时,给 TTL 加一个随机抖动
// 基础过期时间:60分钟
long baseTtl = 60 * 60; 
// 随机抖动:0-300秒 (5分钟)
long randomTtl = new Random().nextInt(300); 

long finalTtl = baseTtl + randomTtl;

// 这样,10万个 Key 会在 5 分钟内陆续过期,而不是在第 60 分钟那一秒同时过期
redisTemplate.opsForValue().set(key, value, finalTtl, TimeUnit.SECONDS);

方案 B:高可用架构

  • 使用 Redis ClusterSentinel,防止单点故障。

  • 配置 Hystrix/Sentinel 限流降级。如果 Redis 真的崩了,直接返回"系统繁忙,请稍后再试",哪怕得罪用户,也要保住数据库。


面试官对线环节 (Interview Combat)

面试官问这块内容,通常有固定的套路。这里给你准备好了满分回答逻辑

Q1: 简单说一下穿透、击穿、雪崩的区别?

思路:用一句话概括核心差异,不要背长篇大论。

回答:

  • 穿透是查"完全不存在"的数据,流量直接打穿到 DB。

  • 击穿是查"热点且刚好过期"的数据,单点并发把 DB 打崩。

  • 雪崩是"大面积"Key 同时过期或 Redis 宕机,导致 DB 全面崩溃。

Q2: 你们项目中怎么解决击穿的?为什么不用 synchronized?

思路:体现分布式思维。

回答:

因为我们的服务是集群部署的(多台服务器),synchronized 只能锁住当前 JVM 进程,防不住其他服务器的流量。

所以我们用了 Redis 分布式锁(Redisson)。在查 DB 前先抢锁,并且在拿到锁之后做了 Double Check(双重检查),确保不会重复查询数据库。

Q3: 布隆过滤器有误判怎么处理?

思路:承认缺点,说明权衡(Trade-off)。

回答:

是的,布隆过滤器存在误判率(它说存在,可能实际不存在;但它说不存在,就一定不存在)。

在我们的业务中,我们容忍了极低概率的"误判穿透"(比如 1%),因为这已经过滤掉了 99% 的恶意流量,数据库完全扛得住。如果业务要求绝对精确,我们也可以考虑配合 Redis 的 BitMap 白名单机制。


总结:一张表看懂

问题 关键特征 形象比喻 核心解法
穿透 不存在的数据 去饭店点菜单上没有的菜,厨师白忙活 布隆过滤器、缓存空对象
击穿 过期热点数据 爆款菜刚好卖完,所有人围着厨师要 互斥锁 (分布式锁)、逻辑过期
雪崩 海量Key同时过期 饭店大厨突然请假,所有菜都做不出来 随机 TTL、Redis 集群、降级

下期预告

防住了外部攻击,咱们内部的数据一致性怎么保证?

面试官最爱问的"死锁题"来了:先删缓存还是先改数据库?什么是延时双删?

【Day 7】双写一致性难题:数据库与缓存如何不再"打架"?

关注专栏,带你接着卷!

相关推荐
吴阿福|一人公司1 分钟前
深度解析 Python 类变量修改的命名空间隔离
java·服务器·数据结构
豆豆2 分钟前
2026年如何选择适合自己的网站管理系统?
数据库·cms·wordpress·建站系统·网站管理系统·建站软件·织梦
zzz_23686 分钟前
【Java基础】链表的七十二变——从LRU缓存到手写浏览器前进后退
java·链表·缓存
番茄去哪了9 分钟前
神领物流面试题(一)
java·大数据·中间件
云烟成雨TD10 分钟前
Agent Scope Java 2.x 系列【9】接入高德 MCP 服务
java·人工智能·agent
吴声子夜歌26 分钟前
SQL经典实例——检索记录
数据库·sql
黄焖鸡能干四碗26 分钟前
软件系统概要设计说明书模版(Word)
大数据·运维·数据库·架构·需求分析
gaohe26AIliuzeyu28 分钟前
Java内部类
java·开发语言
西安邮电大学32 分钟前
有关数组的经典算法题
java·后端·其他·算法·面试
dust_and_stars35 分钟前
为什么ubuntu24 snap install code-server 不需要--classic?
网络·数据库