Spring缓存(二):解决缓存雪崩、击穿、穿透问题
1. 缓存穿透问题与解决方案
1.1 什么是缓存穿透
缓存穿透是指查询一个不存在的数据,由于缓存中没有这个数据,每次请求都会直接打到数据库。
如果有恶意用户不断请求不存在的数据,就会给数据库带来巨大压力。
这种情况下,缓存失去了保护数据库的作用。
典型场景:
- 用户查询一个不存在的商品ID
- 恶意攻击者故意查询大量无效数据
- 业务逻辑错误导致的无效查询
1.2 布隆过滤器解决方案
布隆过滤器是解决缓存穿透最有效的方案之一。它可以快速判断数据是否可能存在。
@Service
public class ProductService {@Autowiredprivate BloomFilter<String> productBloomFilter;@Autowiredprivate 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);// 如果用户不存在,返回一个特殊标记而不是nullreturn 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 {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate 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 {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate 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);}}@Datapublic 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 {@Autowiredprivate CacheManager l1CacheManager; // 本地缓存@Autowiredprivate 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 {@Autowiredprivate BloomFilter<String> productBloomFilter;@Autowiredprivate 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);}@EventListenerpublic void handleCacheHitEvent(CacheHitEvent event) {cacheHitCounter.increment(Tags.of("cache.name", event.getCacheName()));}@EventListenerpublic 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 {@EventListenerpublic 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();}// 缓存降级处理@Recoverpublic Object recoverFromCacheException(Exception ex, String key) {log.warn("缓存操作失败,执行降级逻辑: key={}", key);// 直接查询数据库或返回默认值return fallbackDataService.getFallbackData(key);}
}
6. 最佳实践总结
6.1 缓存策略选择指南
缓存穿透解决方案选择:
- 数据量大且查询模式固定:使用布隆过滤器
- 数据量小且查询随机性强:使用空值缓存
- 对一致性要求高:布隆过滤器 + 空值缓存组合
缓存击穿解决方案选择:
- 对实时性要求高:使用互斥锁方案
- 对可用性要求高:使用逻辑过期方案
- 并发量特别大:逻辑过期 + 异步更新
缓存雪崩解决方案选择:
- 单机应用:随机过期时间 + 本地缓存
- 分布式应用:多级缓存 + 熔断降级
- 高可用要求:Redis集群 + 多级缓存
6.2 性能优化建议
- 合理设置过期时间:根据数据更新频率设置,避免过长或过短
- 控制缓存大小:定期清理无用缓存,避免内存溢出
- 监控缓存指标:关注命中率、响应时间、错误率等关键指标
- 预热关键缓存:系统启动时预加载热点数据
- 异步更新策略:对于非关键数据,采用异步更新减少响应时间
通过合理应用这些缓存策略,可以有效提升系统性能,保障服务稳定性。
记住,缓存是把双刃剑,既要享受性能提升,也要处理好数据一致性问题。