深入理解Redis缓存三大问题:雪崩、穿透与击穿

一、缓存雪崩 (Cache Avalanche)

什么是缓存雪崩?

缓存雪崩是指在某一时刻,大量缓存数据同时失效,导致大量请求直接打到后端数据库,造成数据库压力骤增甚至宕机的现象。这种情况就像雪崩一样,一旦发生就会引发连锁反应。

大量key同时过期 → Redis无数据 → 请求打到MySQL

产生原因

1. 大量缓存同时设置相同的过期时间
2. Redis服务宕机或重启
3. 热点数据集中过期

解决方案

1. 设置随机过期时间

// 给缓存设置随机过期时间,避免集中失效 
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(300 + new Random().nextInt(300)));

2. 缓存预热,在系统启动或低峰期,预先加载热点数据到缓存中

    @Component
    public class CacheWarmUpService {
        @PostConstruct
        public void warmUp() {
            // 预加载热点数据
            List<Product> hotProducts = productRepository.findHotProducts();
            for (Product product : hotProducts) {
                redisTemplate.opsForValue().set(
                        "product:" + product.getId(),
                        JSON.toJSONString(product),
                        Duration.ofHours(1));
            }
        }
    }

3. 限流与降级,使用Hystrix等组件进行服务降级

 @HystrixCommand(fallbackMethod = "getProductFallback")
    public Product getProduct(Long productId) {
        String productJson = redisTemplate.opsForValue().get("product:" + productId);
        if (productJson != null) {
            return JSON.parseObject(productJson, Product.class);
        }
        Product product = productRepository.findById(productId);
        if (product != null) {
            redisTemplate.opsForValue().set(
                    "product:" + productId,
                    JSON.toJSONString(product),
                    Duration.ofMinutes(30));
        }
        return product;
    }

    public Product getProductFallback(Long productId) {
        // 降级处理,返回默认值或错误信息 return new Product(productId, "商品暂时不可用", BigDecimal.ZERO); 
    }

缓存穿透 (Cache Penetration)

二、什么是缓存穿透?

缓存穿透是指查询一个不存在的数据,由于缓存和数据库中都没有该数据,每次请求都会直接打到数据库。恶意攻击者可能利用这个漏洞对系统进行攻击。

缓存穿透:查询不存在的数据 → Redis无数据 → 请求打到MySQL

产生原因

1. 查询不存在的数据
2. 恶意攻击,大量请求不存在的key
3. 缓存未正确处理空值

解决方案

1. 布隆过滤器,布隆过滤器是一种空间效率很高的概率型数据结构,可以快速判断一个元素是否可能存在

    @Service
    public class BloomFilterService {
        private BloomFilter<String> bloomFilter;

        @PostConstruct
        public void initBloomFilter() {
            // 初始化布隆过滤器
            bloomFilter = BloomFilter.create(
                    Funnels.stringFunnel(Charset.defaultCharset()),
                    1000000,  // 预期插入元素数量
                    0.01);    // 误判率
        }

        public User getUserById(Long userId) {
            // 先用布隆过滤器判断用户是否存在
            if (!bloomFilter.mightContain("user:" + userId)) {
                return null; // 直接返回,不查询数据库
            }

            String userJson = redisTemplate.opsForValue().get("user:" + userId);
            if (userJson != null) {
                return JSON.parseObject(userJson, User.class);
            }

            User user = userRepository.findById(userId);
            if (user != null) {
                redisTemplate.opsForValue().set(
                        "user:" + userId,
                        JSON.toJSONString(user),
                        Duration.ofMinutes(30));
            } else {
                // 缓存空值,防止持续穿透
                redisTemplate.opsForValue().set(
                        "user:" + userId,
                        "",
                        Duration.ofMinutes(5));
            }
            return user;
        }
    }

2. 缓存空值,即使查询结果为空,也将空结果缓存一段时间

 public User getUserById(Long userId) {
        String cached = redisTemplate.opsForValue().get("user:" + userId);
        if (cached != null) {
            return cached.isEmpty() ? null : JSON.parseObject(cached, User.class);
        }
        User user = userRepository.findById(userId);
        if (user != null) {
            redisTemplate.opsForValue().set(
                    "user:" + userId,
                    JSON.toJSONString(user),
                    Duration.ofMinutes(30));
        } else {
            // 缓存空值
            redisTemplate.opsForValue().set(
                    "user:" + userId,
                    "",
                    Duration.ofMinutes(5));
        }
        return user;
    }

缓存击穿 (Cache Breakdown)

三、什么是缓存击穿?

缓存击穿是指某个热点key在失效的瞬间,大量并发请求同时打到数据库,导致数据库压力骤增。与雪崩不同的是,击穿只针对一个特定的key。

缓存击穿:热点key失效瞬间 → Redis无数据 → 大量请求打到MySQL

产生原因

1. 热点数据过期
2. 高并发访问同一热点数据
3. 缓存失效与请求到达的时间窗口

解决方案

1. 互斥锁,使用分布式锁确保只有一个线程去加载数据

    public class CacheService {
        public String getData(String key) {
            String value = redisTemplate.opsForValue().get(key);
            if (value == null) {
                return loadFromDatabase(key);
            }
            return value;
        }

        private String loadFromDatabase(String key) {
            String lockKey = "lock:" + key;
            String lockValue = UUID.randomUUID().toString();

            // 获取分布式锁
            Boolean acquired = redisTemplate.opsForValue().setIfAbsent(
                    lockKey, lockValue, Duration.ofSeconds(10));

            if (acquired != null && acquired) {
                try {
                    // 双重检查,防止重复加载
                    String value = redisTemplate.opsForValue().get(key);
                    if (value != null) {
                        return value;
                    }

                    // 查询数据库
                    value = database.get(key);
                    if (value != null) {
                        redisTemplate.opsForValue().set(
                                key, value, Duration.ofMinutes(10));
                    } else {
                        // 缓存空值
                        redisTemplate.opsForValue().set(
                                key, "", Duration.ofMinutes(5));
                    }
                    return value;
                } finally {
                    // 释放锁
                    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                            "return redis.call('del', KEYS[1]) else return 0 end";
                    redisTemplate.execute(
                            new DefaultRedisScript<>(script, Long.class),
                            Collections.singletonList(lockKey),
                            lockValue);
                }
            } else {
                // 等待其他线程加载完成
                try {
                    Thread.sleep(100);
                    return getData(key);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return null;
                }
            }
        }
    }

2. 逻辑过期,在value中包含过期时间信息,由应用层判断是否过期

  public class CacheValue {
        private Object data;
        private long expireTime;

        public boolean isExpired() {
            return System.currentTimeMillis() > expireTime;
        }

        // getters and setters
    }

    public String getDataWithLogicExpire(String key) {
        String json = redisTemplate.opsForValue().get(key);
        if (json != null) {
            CacheValue cacheValue = JSON.parseObject(json, CacheValue.class);
            if (!cacheValue.isExpired()) {
                return cacheValue.getData().toString();
            }
            // 异步更新缓存 
            asyncUpdateCache(key);
            // 返回旧值 
            return cacheValue.getData().toString();
        }
        // 缓存不存在,加载数据
        return loadAndCacheData(key);
    }

总结

问题类型 影响范围 解决思路 核心策略
缓存雪崩 大量key 避免集中失效 随机过期时间、限流降级
缓存穿透 不存在数据 过滤无效请求 布隆过滤器、缓存空值
缓存击穿 单个热点key 控制并发访问 分布式锁、逻辑过期
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容