Redis击穿,穿透和雪崩详解以及解决方案

在 Java 开发中,Redis 作为常用的缓存中间件,可能会面临击穿、穿透、雪崩这三类经典问题。以下是对这三个问题的详细解析及对应的 Java 解决方案:

一、Redis 缓存击穿(Cache Breakdown)

问题描述
  • 定义 :大量请求同时访问一个过期的热点 key(如秒杀活动中的商品库存),导致请求直接穿透到数据库,引发瞬时高并发压力。
  • 核心原因
    • 热点 key 过期时,缓存失效。
    • 大量并发请求同时绕过缓存,直达数据库。
Java 解决方案
1. 互斥锁(Mutex Lock)
  • 思路:在缓存失效时,通过锁机制确保只有一个线程重建缓存,其他线程等待锁释放后从缓存获取数据。

  • 实现步骤

    1. 从 Redis 查询数据,若 key 过期或不存在,尝试获取分布式锁(如 Redisson、ZooKeeper 锁)。
    2. 获得锁的线程查询数据库,更新缓存,并释放锁。
    3. 其他线程在锁等待期间,休眠或重试查询缓存。
  • Java 代码示例(基于 Redisson)

    复制代码
    public String getProductInfo(String productId) {
        String cacheKey = "product:" + productId;
        String result = redisTemplate.opsForValue().get(cacheKey);
        
        if (result == null) { // 缓存失效
            RLock lock = redissonClient.getLock("mutex_lock:" + productId);
            try {
                lock.lock(); // 加锁
                // 二次验证(避免缓存重建期间其他线程重复查询)
                result = redisTemplate.opsForValue().get(cacheKey);
                if (result == null) {
                    // 查询数据库
                    String dbResult = queryFromDatabase(productId);
                    if (dbResult != null) {
                        redisTemplate.opsForValue().set(cacheKey, dbResult, 30, TimeUnit.SECONDS); // 重建缓存
                    }
                }
            } finally {
                lock.unlock(); // 释放锁
            }
        }
        return result;
    }
2. 热点 key 永不过期
  • 思路:为热点 key 设置逻辑过期时间(如在 value 中存储过期时间戳),通过异步线程更新缓存,避免主动过期导致的击穿。

  • 实现步骤

    1. 缓存数据时,在 value 中添加 expireTime 字段。
    2. 每次访问时,检查 expireTime,若过期则启动异步线程更新缓存,当前请求仍返回旧数据。
  • Java 代码示例

    复制代码
    public class CachedData {
        private String value;
        private long expireTime;
        // getter and setter
    }
    
    public String getHotProductInfo(String productId) {
        String cacheKey = "hot_product:" + productId;
        CachedData cachedData = redisTemplate.opsForValue().get(cacheKey);
        
        if (cachedData == null || System.currentTimeMillis() > cachedData.getExpireTime()) {
            // 启动异步线程更新缓存(避免阻塞当前请求)
            CompletableFuture.runAsync(() -> {
                RLock lock = redissonClient.getLock("hot_product_lock:" + productId);
                try {
                    lock.lock();
                    // 二次验证
                    cachedData = redisTemplate.opsForValue().get(cacheKey);
                    if (cachedData == null || System.currentTimeMillis() > cachedData.getExpireTime()) {
                        String dbResult = queryFromDatabase(productId);
                        cachedData = new CachedData();
                        cachedData.setValue(dbResult);
                        cachedData.setExpireTime(System.currentTimeMillis() + 30 * 1000); // 逻辑过期时间
                        redisTemplate.opsForValue().set(cacheKey, cachedData, 60, TimeUnit.SECONDS); // 物理过期时间设为逻辑过期时间的 2 倍
                    }
                } finally {
                    lock.unlock();
                }
            });
            // 返回旧数据或默认值(若首次查询)
            return cachedData != null ? cachedData.getValue() : defaultResponse();
        }
        return cachedData.getValue();
    }

二、Redis 缓存穿透(Cache Penetration)

问题描述
  • 定义 :大量请求访问不存在的 key(如恶意攻击、非法参数),导致请求直接穿透缓存,每次都查询数据库,造成数据库压力激增。
  • 核心原因
    • 缓存层不存储无效 key,导致所有无效请求直达数据库。
    • 攻击方利用不存在的 key 进行批量请求。
Java 解决方案
1. 布隆过滤器(Bloom Filter)
  • 思路:在请求进入数据库前,使用布隆过滤器过滤掉不存在的 key,避免无效请求到达数据库。

  • 实现步骤

    1. 提前将数据库中存在的 key 加载到布隆过滤器中。
    2. 每次请求先通过布隆过滤器判断 key 是否存在,若不存在则直接返回无效响应。
  • Java 代码示例(基于 Google Guava)

    复制代码
    // 初始化布隆过滤器(建议使用 Redis 存储布隆过滤器数据,避免内存溢出)
    private static BloomFilter<String> bloomFilter = BloomFilter.create(
        Funnels.stringFunnel(Charset.forName("UTF-8")), 
        1000000, // 预计元素数量
        0.01 // 误判率
    );
    
    // 服务启动时加载现有 key 到布隆过滤器(示例)
    @PostConstruct
    public void loadExistingKeys() {
        List<String> productIds = productDao.getAllProductIds(); // 从数据库获取所有存在的 productId
        bloomFilter.putAll(productIds);
    }
    
    public String getProductInfo(String productId) {
        if (!bloomFilter.mightContain(productId)) { // key 不存在
            return "无效的 productId";
        }
        // 正常查询缓存和数据库
        String cacheKey = "product:" + productId;
        String result = redisTemplate.opsForValue().get(cacheKey);
        if (result == null) {
            String dbResult = queryFromDatabase(productId);
            if (dbResult != null) {
                redisTemplate.opsForValue().set(cacheKey, dbResult, 30, TimeUnit.SECONDS);
            } else {
                // 缓存空值(防止重复查询)
                redisTemplate.opsForValue().set(cacheKey, "", 5, TimeUnit.MINUTES);
            }
            return dbResult;
        }
        return result;
    }
2. 缓存空值
  • 思路:当数据库查询结果为 null 时,将空值存入缓存(设置较短过期时间),避免后续相同请求穿透到数据库。

  • Java 代码示例

    复制代码
    public String getProductInfo(String productId) {
        String cacheKey = "product:" + productId;
        String result = redisTemplate.opsForValue().get(cacheKey);
        
        if (result == null) { // 缓存未命中
            String dbResult = queryFromDatabase(productId);
            redisTemplate.opsForValue().set(cacheKey, 
                dbResult != null ? dbResult : "", // 空值存为 ""
                dbResult != null ? 30 : 5, // 存在数据则设正常过期时间,空值设短过期时间(如 5 分钟)
                TimeUnit.SECONDS);
            return dbResult;
        }
        return result.isEmpty() ? null : result; // 空值返回 null
    }

三、Redis 缓存雪崩(Cache Avalanche)

问题描述
  • 定义大量缓存 key 同时过期或 Redis 服务宕机,导致大量请求直接涌入数据库,造成数据库负载过高甚至崩溃。
  • 核心原因
    • 缓存层大面积失效(如同一批次 key 的过期时间集中设置)。
    • Redis 实例故障(如主从切换、集群节点宕机)。
Java 解决方案
1. 过期时间随机化
  • 思路:为缓存 key 设置随机过期时间(在固定时间基础上增加随机偏移量),避免大量 key 同时过期。

  • Java 代码示例

    复制代码
    public void setProductCache(String productId, String data) {
        int baseExpireTime = 30 * 60; // 30 分钟
        int randomOffset = ThreadLocalRandom.current().nextInt(10 * 60); // 随机偏移 0~10 分钟
        int expireTime = baseExpireTime + randomOffset;
        redisTemplate.opsForValue().set("product:" + productId, data, expireTime, TimeUnit.SECONDS);
    }
2. 限流与降级
  • 思路

    • 限流:通过令牌桶、信号量等机制限制单位时间内进入数据库的请求量(如使用 Hystrix、Resilience4j 或 Spring Cloud Sentinel)。
    • 降级:当数据库压力过大时,直接返回默认值或提示信息,保护数据库。
  • Java 代码示例(基于 Resilience4j)

    复制代码
    // 引入 Resilience4j 依赖
    // 添加限流注解
    @CircuitBreaker(name = "databaseCircuitBreaker", fallbackMethod = "fallbackGetProductInfo")
    public String getProductInfo(String productId) {
        String cacheKey = "product:" + productId;
        String result = redisTemplate.opsForValue().get(cacheKey);
        if (result == null) {
            String dbResult = queryFromDatabase(productId); // 可能触发限流
            if (dbResult != null) {
                redisTemplate.opsForValue().set(cacheKey, dbResult, 30, TimeUnit.SECONDS);
            }
            return dbResult;
        }
        return result;
    }
    
    // 降级方法
    public String fallbackGetProductInfo(String productId, Throwable throwable) {
        log.error("数据库查询失败,productId: {}, error: {}", productId, throwable.getMessage());
        return "服务繁忙,请稍后重试"; // 返回默认值或提示
    }
3. Redis 高可用架构
  • 思路:搭建 Redis 集群(如 Sentinel 或 Cluster 模式),避免单点故障导致缓存层整体不可用。

  • 配置示例(Spring Boot + Redis Cluster)

    复制代码
    spring.redis.cluster.nodes=redis://node1:7000,redis://node2:7001,redis://node3:7002
    spring.redis.cluster.max-redirects=3

四、总结对比

问题类型 核心原因 典型解决方案 Java 关键技术 / 工具
击穿 单个热点 key 过期 互斥锁、热点 key 永不过期 Redisson、异步线程
穿透 大量无效 key 请求 布隆过滤器、缓存空值 Guava BloomFilter、Redis 空值缓存
雪崩 大量 key 同时过期或 Redis 宕机 过期时间随机化、限流降级、高可用架构 Resilience4j、Redis Cluster

五、最佳实践建议

  1. 预防为主
    • 对热点数据提前预热缓存,避免突发流量击穿。
    • 接口层做参数校验,拦截非法 key(如空值、格式错误)。
  2. 监控与报警
    • 监控 Redis 内存使用率、缓存命中率、过期 key 数量。
    • 监控数据库 QPS、TPS,设置阈值触发报警。
  3. 综合方案
    • 针对高并发场景,组合使用互斥锁 + 布隆过滤器 + 限流降级,形成多层防护。
相关推荐
程序员云帆哥12 分钟前
MySQL JDBC Driver URL参数配置规范
数据库·mysql·jdbc
TDengine (老段)29 分钟前
TDengine 数学函数 FLOOR 用户手册
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
Olrookie1 小时前
若依前后端分离版学习笔记(二十)——实现滑块验证码(vue3)
java·前端·笔记·后端·学习·vue·ruoyi
大气层煮月亮1 小时前
Oracle EBS ERP开发——报表生成Excel标准模板设计
数据库·oracle·excel
云和数据.ChenGuang1 小时前
达梦数据库的命名空间
数据库·oracle
倚栏听风雨2 小时前
java.lang.SecurityException异常
java
星河队长2 小时前
VS创建C++动态库和C#访问过程
java·c++·c#
三三木木七2 小时前
mysql拒绝连接
数据库·mysql
蹦跶的小羊羔2 小时前
sql数据库语法
数据库·sql
唐古乌梁海2 小时前
【mysql】InnoDB的聚簇索引和非聚簇索引工作原理
数据库·mysql