Spring缓存(二):解决缓存雪崩、击穿、穿透问题

发布于:2025-09-13 ⋅ 阅读:(22) ⋅ 点赞:(0)

1. 缓存穿透问题与解决方案

1.1 什么是缓存穿透

缓存穿透是指查询一个不存在的数据,由于缓存中没有这个数据,每次请求都会直接打到数据库。
如果有恶意用户不断请求不存在的数据,就会给数据库带来巨大压力。
这种情况下,缓存失去了保护数据库的作用。

典型场景:

  • 用户查询一个不存在的商品ID
  • 恶意攻击者故意查询大量无效数据
  • 业务逻辑错误导致的无效查询

1.2 布隆过滤器解决方案

布隆过滤器是解决缓存穿透最有效的方案之一。它可以快速判断数据是否可能存在。

@Service
public class ProductService {
    
    @Autowired
    private BloomFilter<String> productBloomFilter;
    
    @Autowired
    private ProductRepository productRepository;
    
    @Cacheable(cacheNames = "productCache", key = "#productId", 
               condition = "@productService.mightExist(#productId)")
    public Product getProduct(String productId) {
        // 只有布隆过滤器认为可能存在的数据才会查询数据库
        return productRepository.findById(productId).orElse(null);
    }
    
    public boolean mightExist(String productId) {
        // 布隆过滤器快速判断,如果返回false则一定不存在
        return productBloomFilter.mightContain(productId);
    }
    
    @CachePut(cacheNames = "productCache", key = "#product.id")
    public Product saveProduct(Product product) {
        // 保存商品时同步更新布隆过滤器
        Product savedProduct = productRepository.save(product);
        productBloomFilter.put(product.getId());
        return savedProduct;
    }
}

1.3 空值缓存策略

对于确实不存在的数据,我们可以缓存一个空值,避免重复查询数据库。

@Service
public class UserService {
    
    private static final String NULL_VALUE = "NULL";
    
    @Cacheable(cacheNames = "userCache", key = "#userId")
    public User getUserById(String userId) {
        User user = userRepository.findById(userId).orElse(null);
        // 如果用户不存在,返回一个特殊标记而不是null
        return user != null ? user : createNullUser();
    }
    
    private User createNullUser() {
        User nullUser = new User();
        nullUser.setId(NULL_VALUE);
        return nullUser;
    }
    
    // 在业务层判断是否为空值缓存
    public User getValidUser(String userId) {
        User user = getUserById(userId);
        return NULL_VALUE.equals(user.getId()) ? null : user;
    }
}

2. 缓存击穿问题与解决方案

2.1 缓存击穿现象分析

缓存击穿是指热点数据的缓存过期时,大量并发请求同时访问这个数据。
由于缓存中没有数据,所有请求都会打到数据库,可能导致数据库瞬间压力过大。

常见场景:

  • 热门商品详情页面
  • 明星用户信息
  • 热点新闻内容

2.2 互斥锁解决方案

使用分布式锁确保只有一个线程去重建缓存,其他线程等待。

@Service
public class HotDataService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private RedissonClient redissonClient;
    
    public Product getHotProduct(String productId) {
        String cacheKey = "hot_product:" + productId;
        
        // 先尝试从缓存获取
        Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
        if (product != null) {
            return product;
        }
        
        // 缓存未命中,使用分布式锁
        String lockKey = "lock:product:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试获取锁,最多等待10秒,锁30秒后自动释放
            if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
                // 双重检查,防止重复查询
                product = (Product) redisTemplate.opsForValue().get(cacheKey);
                if (product != null) {
                    return product;
                }
                
                // 查询数据库并更新缓存
                product = productRepository.findById(productId).orElse(null);
                if (product != null) {
                    // 设置随机过期时间,防止缓存雪崩
                    int expireTime = 3600 + new Random().nextInt(600); // 1小时+随机10分钟
                    redisTemplate.opsForValue().set(cacheKey, product, expireTime, TimeUnit.SECONDS);
                }
                return product;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        
        // 获取锁失败,返回空或默认值
        return null;
    }
}

2.3 逻辑过期解决方案

设置逻辑过期时间,缓存永不过期,通过后台线程异步更新。

@Component
public class LogicalExpireCache {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ThreadPoolExecutor cacheRebuildExecutor;
    
    public Product getProductWithLogicalExpire(String productId) {
        String cacheKey = "logical_product:" + productId;
        
        // 获取缓存数据(包含逻辑过期时间)
        CacheData<Product> cacheData = (CacheData<Product>) redisTemplate.opsForValue().get(cacheKey);
        
        if (cacheData == null) {
            // 缓存不存在,同步查询并设置缓存
            return rebuildCacheSync(productId, cacheKey);
        }
        
        // 检查逻辑过期时间
        if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
            // 未过期,直接返回
            return cacheData.getData();
        }
        
        // 已过期,异步更新缓存,先返回旧数据
        cacheRebuildExecutor.submit(() -> rebuildCacheAsync(productId, cacheKey));
        return cacheData.getData();
    }
    
    private Product rebuildCacheSync(String productId, String cacheKey) {
        Product product = productRepository.findById(productId).orElse(null);
        if (product != null) {
            CacheData<Product> cacheData = new CacheData<>();
            cacheData.setData(product);
            cacheData.setExpireTime(LocalDateTime.now().plusHours(1)); // 1小时后逻辑过期
            redisTemplate.opsForValue().set(cacheKey, cacheData);
        }
        return product;
    }
    
    private void rebuildCacheAsync(String productId, String cacheKey) {
        try {
            rebuildCacheSync(productId, cacheKey);
        } catch (Exception e) {
            log.error("异步重建缓存失败: productId={}", productId, e);
        }
    }
    
    @Data
    public static class CacheData<T> {
        private T data;
        private LocalDateTime expireTime;
    }
}

3. 缓存雪崩问题与解决方案

3.1 缓存雪崩场景分析

缓存雪崩是指大量缓存在同一时间过期,导致大量请求直接打到数据库。
这种情况通常发生在系统重启后或者缓存集中过期时。

典型场景:

  • 系统重启后缓存全部失效
  • 定时任务统一设置的过期时间
  • Redis服务器宕机

3.2 随机过期时间策略

通过设置随机过期时间,避免缓存同时失效。

@Service
public class AntiAvalancheService {
    
    @Cacheable(cacheNames = "randomExpireCache", key = "#key")
    public Object getCacheWithRandomExpire(String key) {
        // Spring缓存注解本身不支持随机过期,需要结合Redis操作
        return dataRepository.findByKey(key);
    }
    
    @CachePut(cacheNames = "randomExpireCache", key = "#key")
    public Object updateCacheWithRandomExpire(String key, Object data) {
        // 手动设置随机过期时间
        String cacheKey = "randomExpireCache::" + key;
        int baseExpire = 3600; // 基础过期时间1小时
        int randomExpire = new Random().nextInt(1800); // 随机0-30分钟
        
        redisTemplate.opsForValue().set(cacheKey, data, 
            baseExpire + randomExpire, TimeUnit.SECONDS);
        
        return data;
    }
}

3.3 多级缓存架构

建立多级缓存体系,即使一级缓存失效,还有二级缓存保护。

@Service
public class MultiLevelCacheService {
    
    @Autowired
    private CacheManager l1CacheManager; // 本地缓存
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate; // Redis缓存
    
    public Product getProductMultiLevel(String productId) {
        // 一级缓存:本地缓存(Caffeine)
        Cache l1Cache = l1CacheManager.getCache("productL1Cache");
        Product product = l1Cache.get(productId, Product.class);
        if (product != null) {
            return product;
        }
        
        // 二级缓存:Redis缓存
        String redisKey = "product:" + productId;
        product = (Product) redisTemplate.opsForValue().get(redisKey);
        if (product != null) {
            // 回写一级缓存
            l1Cache.put(productId, product);
            return product;
        }
        
        // 三级:数据库查询
        product = productRepository.findById(productId).orElse(null);
        if (product != null) {
            // 同时更新两级缓存
            l1Cache.put(productId, product);
            redisTemplate.opsForValue().set(redisKey, product, 
                Duration.ofHours(2)); // Redis缓存2小时
        }
        
        return product;
    }
    
    @CacheEvict(cacheNames = "productL1Cache", key = "#productId")
    public void evictProduct(String productId) {
        // 同时清除Redis缓存
        redisTemplate.delete("product:" + productId);
    }
}

4. 电商系统实战案例

4.1 商品详情页缓存策略

电商系统的商品详情页是典型的高并发场景,需要综合应用多种缓存策略。

@Service
public class ProductDetailService {
    
    @Autowired
    private BloomFilter<String> productBloomFilter;
    
    @Autowired
    private RedissonClient redissonClient;
    
    // 防穿透 + 防击穿的商品详情查询
    public ProductDetail getProductDetail(String productId) {
        // 1. 布隆过滤器防穿透
        if (!productBloomFilter.mightContain(productId)) {
            return null; // 商品不存在
        }
        
        String cacheKey = "product_detail:" + productId;
        
        // 2. 尝试从缓存获取
        ProductDetail detail = (ProductDetail) redisTemplate.opsForValue().get(cacheKey);
        if (detail != null) {
            return detail;
        }
        
        // 3. 缓存未命中,使用分布式锁防击穿
        String lockKey = "lock:product_detail:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
                // 双重检查
                detail = (ProductDetail) redisTemplate.opsForValue().get(cacheKey);
                if (detail != null) {
                    return detail;
                }
                
                // 查询数据库
                detail = buildProductDetail(productId);
                if (detail != null) {
                    // 4. 设置随机过期时间防雪崩
                    int expireTime = 7200 + new Random().nextInt(3600); // 2-3小时
                    redisTemplate.opsForValue().set(cacheKey, detail, 
                        expireTime, TimeUnit.SECONDS);
                }
                return detail;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        
        return null;
    }
    
    private ProductDetail buildProductDetail(String productId) {
        // 组装商品详情信息
        Product product = productRepository.findById(productId).orElse(null);
        if (product == null) {
            return null;
        }
        
        ProductDetail detail = new ProductDetail();
        detail.setProduct(product);
        detail.setInventory(inventoryService.getInventory(productId));
        detail.setReviews(reviewService.getTopReviews(productId));
        detail.setRecommendations(recommendationService.getRecommendations(productId));
        
        return detail;
    }
}

4.2 用户会话缓存管理

用户会话信息需要考虑安全性和性能,采用分层缓存策略。

@Service
public class UserSessionService {
    
    // 敏感信息使用短期缓存
    @Cacheable(cacheNames = "userSessionCache", key = "#sessionId", 
               condition = "#sessionId != null")
    public UserSession getUserSession(String sessionId) {
        return sessionRepository.findBySessionId(sessionId);
    }
    
    // 用户基础信息使用长期缓存
    @Cacheable(cacheNames = "userBasicCache", key = "#userId")
    public UserBasicInfo getUserBasicInfo(String userId) {
        return userRepository.findBasicInfoById(userId);
    }
    
    @CacheEvict(cacheNames = {"userSessionCache", "userBasicCache"}, 
                key = "#userId")
    public void invalidateUserCache(String userId) {
        // 用户登出或信息变更时清除相关缓存
        log.info("清除用户缓存: {}", userId);
    }
    
    // 防止会话固定攻击的缓存更新
    @CachePut(cacheNames = "userSessionCache", key = "#newSessionId")
    @CacheEvict(cacheNames = "userSessionCache", key = "#oldSessionId")
    public UserSession refreshSession(String oldSessionId, String newSessionId, String userId) {
        // 生成新的会话信息
        UserSession newSession = new UserSession();
        newSession.setSessionId(newSessionId);
        newSession.setUserId(userId);
        newSession.setCreateTime(LocalDateTime.now());
        
        sessionRepository.save(newSession);
        sessionRepository.deleteBySessionId(oldSessionId);
        
        return newSession;
    }
}

5. 缓存监控与告警

5.1 缓存命中率监控

监控缓存的命中率,及时发现缓存问题。

@Component
public class CacheMetricsCollector {
    
    private final MeterRegistry meterRegistry;
    private final Counter cacheHitCounter;
    private final Counter cacheMissCounter;
    
    public CacheMetricsCollector(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.cacheHitCounter = Counter.builder("cache.hit")
            .description("Cache hit count")
            .register(meterRegistry);
        this.cacheMissCounter = Counter.builder("cache.miss")
            .description("Cache miss count")
            .register(meterRegistry);
    }
    
    @EventListener
    public void handleCacheHitEvent(CacheHitEvent event) {
        cacheHitCounter.increment(
            Tags.of("cache.name", event.getCacheName()));
    }
    
    @EventListener
    public void handleCacheMissEvent(CacheMissEvent event) {
        cacheMissCounter.increment(
            Tags.of("cache.name", event.getCacheName()));
    }
    
    // 计算缓存命中率
    public double getCacheHitRate(String cacheName) {
        double hits = cacheHitCounter.count();
        double misses = cacheMissCounter.count();
        return hits / (hits + misses);
    }
}

5.2 缓存异常告警

当缓存出现异常时,及时告警并降级处理。

@Component
public class CacheExceptionHandler {
    
    @EventListener
    public void handleCacheException(CacheErrorEvent event) {
        log.error("缓存异常: cache={}, key={}, exception={}", 
            event.getCacheName(), event.getKey(), event.getException().getMessage());
        
        // 发送告警
        alertService.sendAlert("缓存异常", 
            String.format("缓存 %s 发生异常: %s", 
                event.getCacheName(), event.getException().getMessage()));
        
        // 记录异常指标
        meterRegistry.counter("cache.error", 
            "cache.name", event.getCacheName()).increment();
    }
    
    // 缓存降级处理
    @Recover
    public Object recoverFromCacheException(Exception ex, String key) {
        log.warn("缓存操作失败,执行降级逻辑: key={}", key);
        // 直接查询数据库或返回默认值
        return fallbackDataService.getFallbackData(key);
    }
}

6. 最佳实践总结

6.1 缓存策略选择指南

缓存穿透解决方案选择:

  • 数据量大且查询模式固定:使用布隆过滤器
  • 数据量小且查询随机性强:使用空值缓存
  • 对一致性要求高:布隆过滤器 + 空值缓存组合

缓存击穿解决方案选择:

  • 对实时性要求高:使用互斥锁方案
  • 对可用性要求高:使用逻辑过期方案
  • 并发量特别大:逻辑过期 + 异步更新

缓存雪崩解决方案选择:

  • 单机应用:随机过期时间 + 本地缓存
  • 分布式应用:多级缓存 + 熔断降级
  • 高可用要求:Redis集群 + 多级缓存

6.2 性能优化建议

  1. 合理设置过期时间:根据数据更新频率设置,避免过长或过短
  2. 控制缓存大小:定期清理无用缓存,避免内存溢出
  3. 监控缓存指标:关注命中率、响应时间、错误率等关键指标
  4. 预热关键缓存:系统启动时预加载热点数据
  5. 异步更新策略:对于非关键数据,采用异步更新减少响应时间

通过合理应用这些缓存策略,可以有效提升系统性能,保障服务稳定性。
记住,缓存是把双刃剑,既要享受性能提升,也要处理好数据一致性问题。