Redis缓存三大核心问题(穿透、失效、雪崩)

8 阅读13分钟

Redis缓存三大核心问题(穿透、失效、雪崩)

一、缓存穿透(Cache Penetration)

1. 产生原因

缓存穿透是指 客户端请求查询的数据,在缓存(Redis)和数据库(MySQL)中均不存在,导致所有请求直接穿透缓存,全部落到数据库上,引发数据库压力骤增,甚至宕机。

常见触发场景:

  • 恶意攻击:攻击者故意构造不存在的 key(如随机生成无效用户ID、商品ID),高频发起请求,模拟高并发穿透;
  • 业务误操作:查询不存在的业务数据(如已删除的用户、未上架的商品),且未做缓存兜底;
  • 缓存与数据库数据不一致:数据库数据已删除,但缓存未清理,后续查询该 key 时,缓存命中空值,仍会穿透到数据库(非典型穿透,属于缓存与数据库同步问题衍生)。

2. 核心危害

  • 数据库承受高并发请求,CPU、IO 负载飙升,严重时导致数据库宕机,影响整个业务链路;
  • 缓存失去作用,所有请求直接打在数据库,违背“缓存减轻数据库压力”的核心目的;
  • 可能引发连锁反应,导致依赖该数据库的其他服务不可用。

3. 解决办法(按落地优先级排序)

(1)缓存空对象(最简单、易落地)

核心思路:查询数据库发现数据不存在时,不直接返回空,而是在缓存中存入一个“空对象”(如 ""null),并设置较短的过期时间(避免占用过多内存),后续相同请求直接命中缓存空对象,无需穿透到数据库。

代码示例(Java + StringRedisTemplate)

/**
 * 缓存空对象示例(查询用户信息)
 */
public User getUserById(Long userId) {
    String key = "user:" + userId;
    // 1. 先查缓存
    String userJson = stringRedisTemplate.opsForValue().get(key);
    if (userJson != null) {
        // 缓存命中(包括空对象),直接返回
        return userJson.equals("null") ? null : JSON.parseObject(userJson, User.class);
    }

    // 2. 缓存未命中,查数据库
    User user = userMapper.selectById(userId);
    // 3. 数据库无数据,缓存空对象(过期时间设为5分钟,避免长期占用内存)
    if (user == null) {
        stringRedisTemplate.opsForValue().set(key, "null", 5, TimeUnit.MINUTES);
        return null;
    }

    // 4. 数据库有数据,缓存真实数据(过期时间设为30分钟)
    stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
    return user;
}
(2)使用布隆过滤器(高效、适合大数据量场景)

核心思路:在缓存之前增加一层布隆过滤器,将数据库中所有存在的 key(如所有用户ID、商品ID)提前存入布隆过滤器,请求过来时,先通过布隆过滤器判断 key 是否存在——不存在则直接返回,存在再走“缓存→数据库”流程,从源头拦截无效请求。

关键说明

  • 布隆过滤器特点:占用内存极小(1亿条数据仅需十几MB)、查询速度快(O(1)),但存在极小的误判率(可通过调整哈希函数数量和位数降低);
  • 适配场景:数据量极大(如亿级用户ID)、恶意攻击频繁的场景;
  • 实现方式:可基于 Redis Bitmap 手动实现,或使用 Redisson 封装的布隆过滤器(推荐,无需手动处理哈希逻辑)。

Redisson 布隆过滤器代码示例

import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;

@Component
public class BloomFilterConfig {
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private UserMapper userMapper;

    // 布隆过滤器key
    private static final String USER_BLOOM_KEY = "bloom:user:ids";
    // 预计数据量(如1亿用户)
    private static final long EXPECTED_COUNT = 100000000L;
    // 误判率(0.01 = 1%,误判率越低,占用内存越大)
    private static final double FALSE_POSITIVE_RATE = 0.01;

    /**
     * 项目启动时,初始化布隆过滤器(将所有用户ID存入)
     */
    @PostConstruct
    public void initBloomFilter() {
        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(USER_BLOOM_KEY);
        // 初始化布隆过滤器(预计数据量、误判率)
        bloomFilter.tryInit(EXPECTED_COUNT, FALSE_POSITIVE_RATE);

        // 从数据库查询所有用户ID,存入布隆过滤器(可分批查询,避免内存溢出)
        List<Long> allUserIds = userMapper.selectAllUserIds();
        for (Long userId : allUserIds) {
            bloomFilter.add(userId);
        }
    }

    /**
     * 校验用户ID是否存在(用于拦截缓存穿透)
     */
    public boolean isUserIdExists(Long userId) {
        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(USER_BLOOM_KEY);
        return bloomFilter.contains(userId);
    }
}
(3)其他补充优化
  • 接口层限流:对高频无效请求(如短时间内大量相同无效 key)进行限流,避免恶意攻击;
  • 数据校验:接口层先校验请求参数(如用户ID格式、商品ID范围),无效参数直接返回,不进入缓存和数据库流程;
  • 缓存预热:提前将热点数据、常用数据存入缓存,减少缓存未命中的概率。

二、缓存失效(Cache Invalidation)

1. 产生原因

缓存失效(也叫“缓存击穿”)是指 某一个热点 key 过期失效时,恰好有大量并发请求访问该 key,导致所有请求同时穿透缓存,打到数据库上,引发数据库瞬时压力激增(单点热点 key 引发的“小雪崩”)。

常见触发场景:

  • 热点数据集中过期:如电商秒杀商品、热门活动页面,所有请求都访问同一个热点 key,且该 key 过期时间设置相同;
  • 缓存主动删除:业务操作中手动删除热点 key(如商品下架时删除缓存),删除后瞬间有大量请求访问;
  • 缓存被动失效:Redis 内存满时,根据淘汰策略(如 LRU)淘汰热点 key,淘汰后有大量请求访问该 key。

2. 核心危害

  • 数据库瞬时承受高并发请求,可能导致数据库连接池耗尽、响应变慢,甚至短暂宕机;
  • 热点业务(如秒杀、热门商品)响应延迟,影响用户体验;
  • 若数据库处理不过来,可能引发请求堆积,导致整个业务链路卡顿。

3. 解决办法(按落地优先级排序)

(1)热点 key 过期时间“错开”(最简单、无侵入)

核心思路:对热点 key 的过期时间增加随机偏移量,避免多个热点 key 在同一时间同时过期,分散数据库压力。

代码示例

// 热点 key 过期时间:基础30分钟 + 随机0~5分钟,避免集中过期
long baseExpire = 30 * 60; // 基础过期时间(秒)
long randomExpire = new Random().nextInt(5 * 60); // 随机偏移量(0~300秒)
long totalExpire = baseExpire + randomExpire;

// 缓存热点商品数据(key:product:1001,过期时间带随机偏移)
stringRedisTemplate.opsForValue().set("product:1001", JSON.toJSONString(product), totalExpire, TimeUnit.SECONDS);
(2)互斥锁(防止并发穿透,适合高一致性场景)

核心思路:当热点 key 过期、缓存未命中时,不是所有请求都去查数据库,而是只有一个请求获取互斥锁,去数据库查询并更新缓存,其他请求等待锁释放后,直接从缓存获取数据,避免并发打库。

代码示例(Redis 分布式锁 + 互斥逻辑)

/**
 * 互斥锁解决缓存失效(查询热门商品)
 */
public Product getHotProduct(Long productId) {
    String key = "product:" + productId;
    String lockKey = "lock:product:" + productId;

    // 1. 先查缓存
    String productJson = stringRedisTemplate.opsForValue().get(key);
    if (productJson != null) {
        return JSON.parseObject(productJson, Product.class);
    }

    // 2. 缓存未命中,获取互斥锁(最多等待1秒,锁10秒后自动释放)
    Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    if (Boolean.TRUE.equals(locked)) {
        try {
            // 3. 拿到锁,查数据库
            Product product = productMapper.selectById(productId);
            if (product == null) {
                // 数据库无数据,缓存空对象(避免再次穿透)
                stringRedisTemplate.opsForValue().set(key, "null", 5, TimeUnit.MINUTES);
                return null;
            }
            // 4. 更新缓存(过期时间带随机偏移)
            long expire = 30 * 60 + new Random().nextInt(5 * 60);
            stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(product), expire, TimeUnit.SECONDS);
            return product;
        } finally {
            // 5. 释放锁(无论成功失败,都要释放锁)
            stringRedisTemplate.delete(lockKey);
        }
    } else {
        // 6. 未拿到锁,等待50毫秒后重试(避免频繁请求)
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return getHotProduct(productId); // 递归重试
    }
}
(3)热点 key 永不过期(适合非实时数据)

核心思路:对热点 key 不设置过期时间,避免过期失效;同时通过后台线程定期更新缓存数据,保证缓存与数据库数据一致。

适用场景:数据更新频率低、实时性要求不高的热点数据(如首页热门推荐、固定配置数据)。

实现思路

  1. 热点 key 存入缓存时,不设置过期时间;
  2. 开启一个定时任务(如每10分钟),从数据库查询最新数据,更新缓存;
  3. 若数据发生紧急更新(如商品价格调整),手动触发缓存更新,避免数据不一致。
(4)其他补充优化
  • 缓存预热:提前将热点数据存入缓存,并设置合理的过期时间,避免缓存未命中;
  • 扩大缓存粒度:将热点数据与关联数据合并缓存(如商品信息+库存信息),减少缓存查询次数;
  • Redis 淘汰策略优化:避免使用 LRU 淘汰策略(可能淘汰热点 key),可使用 LFU 策略(淘汰访问频率低的 key),或调整 maxmemory-policy 配置。

三、缓存雪崩(Cache Avalanche)

1. 产生原因

缓存雪崩是指 缓存服务器(Redis)整体不可用(宕机),或大量 key 在同一时间段集中过期失效,导致所有请求全部穿透缓存,直接打在数据库上,引发数据库崩溃,甚至整个业务系统瘫痪(比缓存失效更严重,是“批量失效”或“缓存整体宕机”导致的系统性问题)。

常见触发场景:

  • 缓存集群宕机:Redis 主从架构中,主节点宕机且从节点未及时切换;或 Redis 集群整体故障(如网络中断、服务器宕机);
  • 大量 key 集中过期:如电商大促前,批量缓存了大量商品数据,且设置了相同的过期时间(如24小时),到期后批量失效;
  • 缓存容量满:Redis 内存达到上限,触发淘汰策略,大量热点 key 被批量淘汰,导致请求穿透。

2. 核心危害

  • 数据库瞬间承受全量请求,CPU、IO 负载直接拉满,大概率导致数据库宕机;
  • 缓存服务完全失效,整个业务链路依赖缓存的服务全部不可用(如商品查询、用户登录);
  • 引发连锁反应,导致依赖该业务的其他系统崩溃,造成严重的生产事故。

3. 解决办法(按“预防+兜底”思路分类)

(一)预防措施(核心:避免缓存整体不可用、避免大量 key 集中过期)
1. 搭建高可用缓存架构(解决缓存宕机问题)
  • 主从复制 + 哨兵模式:部署 Redis 主从架构,搭配哨兵监控,主节点宕机时,哨兵自动将从节点切换为主节点,保证缓存服务连续性;
  • Redis 集群模式:部署 Redis Cluster 集群(至少3主3从),单个节点宕机时,其他节点正常提供服务,避免单点故障;
  • 多机房部署:跨机房部署 Redis 集群,避免单个机房故障导致缓存整体不可用。
2. 避免大量 key 集中过期(解决批量失效问题)
  • 过期时间加随机偏移:与“缓存失效”的解决办法一致,对所有 key 的过期时间增加随机偏移(如基础30分钟 + 0~10分钟随机),分散过期时间;
  • 分批设置过期时间:对批量缓存的 key,分批次设置不同的过期时间(如第一批1小时、第二批1小时10分、第三批1小时20分),避免集中过期;
  • 热点 key 永不过期:对核心热点 key,不设置过期时间,通过后台线程定期更新,避免过期失效。
3. 缓存容量优化(避免批量淘汰)
  • 合理设置 Redis 内存上限(maxmemory),预留足够的冗余空间,避免内存满触发批量淘汰;
  • 优化缓存淘汰策略:生产环境推荐使用 LFU(least frequently used,最少访问频率)策略,优先淘汰访问频率低的 key,减少热点 key 被淘汰的概率;
  • 定期清理无效缓存:后台线程定期清理过期、无效的缓存 key(如已删除数据的缓存),释放内存。
(二)兜底措施(核心:缓存失效后,减少数据库压力)
1. 服务降级与限流
  • 服务降级:缓存宕机时,对非核心业务(如商品推荐、历史订单查询)进行降级,直接返回默认数据(如“服务暂时不可用”),避免请求全部打在数据库;
  • 接口限流:通过网关(如 Nginx、Gateway)对请求进行限流,限制单位时间内访问数据库的请求数量,避免数据库被压垮。
2. 本地缓存兜底
  • 在应用层增加本地缓存(如 Caffeine、Guava Cache),缓存核心热点数据,当 Redis 缓存宕机时,请求先访问本地缓存,减少数据库压力;
  • 注意:本地缓存需设置较短的过期时间,避免与 Redis 缓存数据不一致,且本地缓存容量不宜过大,避免占用过多应用内存。
3. 数据库保护
  • 数据库读写分离:主库负责写操作,从库负责读操作,缓存宕机时,读请求打在从库上,分散主库压力;
  • 数据库限流:通过数据库连接池限制最大连接数,避免请求过多导致连接池耗尽;
  • 熔断机制:当数据库压力达到阈值(如 CPU 使用率超过80%),触发熔断,暂时拒绝部分请求,避免数据库崩溃。
(三)监控与应急
  • 实时监控:监控 Redis 集群状态(节点存活、内存使用率、QPS)、数据库状态(连接数、CPU、IO),设置告警阈值(如 Redis 节点宕机、数据库连接数超过阈值时,及时告警);
  • 应急方案:提前制定缓存雪崩应急方案,如 Redis 集群宕机时,手动切换备用集群、临时启用本地缓存、降级非核心业务,快速恢复服务。

四、三大问题对比总结

问题类型核心原因核心危害核心解决思路
缓存穿透请求不存在的数据,缓存和数据库均无数据库压力激增,可能宕机缓存空对象、布隆过滤器、接口限流
缓存失效单个热点 key 过期,并发请求穿透数据库瞬时压力激增,热点业务卡顿过期时间随机偏移、互斥锁、热点 key 永不过期
缓存雪崩缓存整体宕机/大量 key 集中过期数据库崩溃,业务系统瘫痪高可用架构、分散过期时间、服务降级、本地缓存兜底

核心总结

  1. 缓存穿透:重点“拦截无效请求”,从源头减少数据库压力;
  2. 缓存失效:重点“避免热点 key 集中过期”,防止并发穿透;
  3. 缓存雪崩:重点“保证缓存高可用”,同时做好兜底措施,避免系统性崩溃。

生产环境中,需结合业务场景(数据量、并发量、实时性要求),组合使用上述解决办法,既保证缓存的高效性,也能应对各类异常场景,确保业务稳定运行。