上一篇我们完成了Redis Cluster分布式集群搭建,实现了海量数据分片存储与高可用,彻底打通了Redis从单机到集群的全架构体系。但在高并发生产环境中,哪怕集群架构再稳定,一旦遇到恶意请求、热点数据失效、大规模缓存过期,就会引发缓存穿透、缓存击穿、缓存雪崩三大致命问题,直接导致数据库压力过载、服务宕机。

这三大问题是Redis缓存使用的核心痛点,也是后端工程师面试高频考点。本篇就用白话拆解原理、定位业务场景、给出落地代码+配置方案,从根源规避缓存陷阱,让你的高并发系统稳如泰山。

核心警示:缓存三大问题本质都是请求绕过缓存直接打数据库,高并发下数据库扛不住瞬间流量,会引发连锁宕机,必须提前设防。

一、缓存穿透:请求直击数据库,空数据反复查

1. 原理白话讲解

用户请求的数据既不在缓存中,也不在数据库中,每次请求都会绕过缓存,直接查询数据库。比如恶意请求查询不存在的商品ID、用户ID,大量无效请求压垮数据库。

2. 典型业务场景

  • 恶意爬虫/黑客批量请求不存在的接口
  • 业务逻辑漏洞,传入非法ID、参数
  • 数据已被删除,前端仍请求旧数据

3. 终极解决方案

方案1:缓存空值/默认值(简易版)

数据库查询为空时,给缓存设置一个空值或默认值,同时添加短过期时间,避免占用过多内存,后续请求直接读取缓存空值,不再查询数据库。

// 伪代码逻辑
public Object getData(String id) {
    // 查缓存
    Object cacheData = redis.get(id);
    if (cacheData != null) {
        return cacheData;
    }
    // 查数据库
    Object dbData = mapper.selectById(id);
    if (dbData != null) {
        // 有数据,缓存正常数据
        redis.set(id, dbData, 3600);
    } else {
        // 无数据,缓存空值,设置短过期(5分钟)
        redis.set(id, "", 300);
    }
    return dbData;
}

方案2:布隆过滤器(专业版,推荐)

提前将所有合法的Key(如商品ID、用户ID)存入布隆过滤器,请求进来先过过滤器,过滤器判定不存在的请求直接拦截,绝不查询数据库。

  • 优点:内存占用极小、效率极高,适合海量数据
  • 缺点:存在极小误判率,可调参降低
// 布隆过滤器使用伪代码
public Object getData(String id) {
    // 布隆过滤器判定不存在,直接返回
    if (!bloomFilter.mightContain(id)) {
        return null;
    }
    // 后续走缓存+数据库逻辑
    Object cacheData = redis.get(id);
    if (cacheData != null) return cacheData;
    Object dbData = mapper.selectById(id);
    redis.set(id, dbData, 3600);
    return dbData;
}

方案3:接口参数校验(兜底)

在接口层做参数合法性校验,过滤非法ID、负数ID、格式错误的参数,从源头拦截无效请求。


二、缓存击穿:热点Key失效,瞬间流量砸数据库

1. 原理白话讲解

某个热点Key(如爆款商品、热搜数据)缓存过期,此时大量并发请求同时过来,缓存未命中,所有请求同时查询数据库,瞬间打满数据库连接。

和缓存穿透的区别:击穿是数据存在,只是缓存过期;穿透是数据根本不存在。

2. 典型业务场景

  • 电商秒杀商品缓存过期
  • 热点新闻、热搜榜单缓存失效
  • 节假日高峰期热点数据集中过期

3. 终极解决方案

方案1:互斥锁(分布式锁,推荐)

缓存过期时,用分布式锁(Redis Setnx)控制只有一个线程去查询数据库并重建缓存,其余线程等待锁释放,直接读取新缓存,避免并发查库。

// 分布式锁解决缓存击穿伪代码
public Object getHotData(String key) {
    Object data = redis.get(key);
    if (data != null) {
        return data;
    }
    // 加分布式锁,锁超时时间短于缓存重建时间
    String lockKey = "lock:" + key;
    Boolean lock = redis.setnx(lockKey, "1", 5);
    if (lock) {
        try {
            // 双重校验,避免重复查库
            data = redis.get(key);
            if (data == null) {
                // 查询数据库
                data = mapper.selectHotData(key);
                // 重建缓存,设置合理过期时间
                redis.set(key, data, 3600);
            }
        } finally {
            // 释放锁
            redis.del(lockKey);
        }
    } else {
        // 短暂休眠,重试读取缓存
        Thread.sleep(100);
        return getHotData(key);
    }
    return data;
}

方案2:热点Key永不过期

对极致热点数据,不设置过期时间,通过后台异步线程定时更新缓存,保证数据实时性的同时,避免缓存过期击穿。

方案3:过期时间打散

给热点Key的过期时间添加随机值(如3600±60秒),避免大量热点Key同一时间集中过期。


三、缓存雪崩:大面积缓存失效,数据库全盘崩溃

1. 原理白话讲解

大量缓存Key同一时间集中过期,或Redis服务宕机,导致所有请求同时涌向数据库,数据库负载爆表,直接宕机,引发整个系统雪崩。

和击穿的区别:击穿是单个热点Key过期;雪崩是大面积Key失效/Redis宕机,影响全局。

2. 典型业务场景

  • 批量缓存设置了相同的过期时间
  • Redis服务器宕机、集群故障
  • 重启Redis导致缓存清空

3. 终极解决方案

方案1:过期时间随机化(基础必做)

给每个Key的过期时间加上随机偏移量,杜绝大面积缓存同时过期,这是成本最低、见效最快的方案。

// 过期时间随机化伪代码
// 基础过期时间1小时,随机偏移0-600秒
int expire = 3600 + new Random().nextInt(600);
redis.set(key, data, expire);

方案2:Redis高可用部署(杜绝节点宕机)

采用主从+哨兵Redis Cluster集群架构,避免单节点Redis宕机导致全盘缓存失效,实现故障自动切换。

方案3:多级缓存架构(兜底防护)

搭建本地缓存(Caffeine、Guava)+ Redis远程缓存的二级架构,Redis失效时,本地缓存兜底,分担数据库压力。

方案4:服务熔断+限流(紧急止损)

接入Sentinel、Hystrix等限流熔断组件,当数据库压力超标时,直接拒绝多余请求,返回兜底数据,防止数据库被打垮。

方案5:缓存预热(提前加载)

项目启动时,提前将热点数据加载到Redis中,避免流量进来后才去重建缓存。


四、三大缓存问题对比表

问题类型核心原因影响范围首选方案
缓存穿透查询不存在的数据,缓存不命中局部/全局(恶意攻击)布隆过滤器+参数校验
缓存击穿单个热点Key过期单个热点接口分布式互斥锁
缓存雪崩大面积Key过期/Redis宕机全局系统随机过期+高可用集群

实战口诀:穿透靠过滤,击穿靠加锁,雪崩靠打散+高可用,多级缓存做兜底,限流熔断保平安。

五、生产环境避坑小贴士

  • 空值缓存必须设短过期时间,防止内存浪费和脏数据
  • 布隆过滤器不支持删除操作,数据频繁变动需定时重建
  • 分布式锁必须加超时时间,防止线程崩溃导致死锁
  • 严禁批量数据设置相同过期时间,这是雪崩重灾区
  • 缓存监控必不可少,实时监控缓存命中率、Key过期数量

总结与下篇预告

本篇彻底拆解了Redis穿透、击穿、雪崩三大难题,从原理到场景再到落地代码,给出了一站式解决方案,吃透这些内容,既能应对面试,又能解决生产实际问题,彻底杜绝缓存引发的系统故障。

发表回复