当前位置: 首页 > news >正文

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 性能优化建议

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

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


文章转载自:

http://xHDBitjL.msmtf.cn
http://aM3vyr3a.msmtf.cn
http://OFscg5Sc.msmtf.cn
http://Fks8cbgF.msmtf.cn
http://HNCOZkYf.msmtf.cn
http://vtUrxmww.msmtf.cn
http://khZmP3Be.msmtf.cn
http://Cg3zzMQe.msmtf.cn
http://ibsi77U1.msmtf.cn
http://nY0RsDuY.msmtf.cn
http://fvc8ea2E.msmtf.cn
http://4TJ6paje.msmtf.cn
http://q0ExTX0N.msmtf.cn
http://9gF4FMAF.msmtf.cn
http://YoT0lIVg.msmtf.cn
http://lrDPPsYd.msmtf.cn
http://cOSRb281.msmtf.cn
http://YVibwCkp.msmtf.cn
http://1taGkRet.msmtf.cn
http://PR7uIm4a.msmtf.cn
http://IqRDrotH.msmtf.cn
http://JSXQCpnO.msmtf.cn
http://24ik7nAk.msmtf.cn
http://NBci8DHc.msmtf.cn
http://6UrCVyCf.msmtf.cn
http://iruNaTYy.msmtf.cn
http://86WjmZYY.msmtf.cn
http://stIeAohn.msmtf.cn
http://X4Oy7u2I.msmtf.cn
http://XzMpL4xT.msmtf.cn
http://www.dtcms.com/a/379785.html

相关文章:

  • LabVIEW加载 STL 模型至 3D 场景 源码见附件
  • Tessent_ijtag_ug——第 4 章 ICL 提取(2)
  • 前端WebSocket实时通信实现
  • 2025年- H133-Lc131. 反转字符串(字符串)--Java版
  • 萨顿四条原则
  • NumPy 2.x 完全指南【三十八】伪随机数生成器
  • GitHub 热榜项目 - 日榜(2025-09-12)
  • O3.3 opencv指纹识别
  • 在线会议系统是一个基于Vue3 + Spring Boot的现代化在线会议管理平台,集成了视频会议、实时聊天、AI智能助手等多项先进技术。
  • 每日一算:打家劫舍
  • MemGPT: Towards LLMs as Operating Systems
  • MySQL与PostgreSQL核心区别对比
  • Redis基础命令速查:从连接到数据操作,新手也能上手
  • 信息安全工程师考点-网络安全法律与标准
  • 阿里云OSS vs 腾讯云COS vs AWS S3:对象存储价格与性能深度对比
  • vim复制本地到linux服务器上,换行缩进过大,不对的问题
  • 【贪心算法】day9
  • HarmonyOS 5分布式数据管理初探:实现跨设备数据同步
  • 【Unity UGUI 交互组件——InputFild(TMP版本)(11)】
  • 基于QVTKOpenGLNativeWidget的三维点云可视化实现
  • Qwen3 中注意力机制实现
  • 基于librdkafa C++客户端生产者发送数据失败问题处理#2
  • Maya绑定:渲染编辑器Hypershade简单使用,给小球添加材质纹理
  • 前端基础 —— A / HTML
  • 线性代数 | 行列式与矩阵区别
  • Redis 核心数据结构:String 类型深度解析与 C++ 实战
  • 【Linux】面试常考!Linux 进程核心考点:写时拷贝优化原理 + 进程等待实战,一篇理清进程一生
  • 根据当前门店经纬度,求出1km内的门店
  • java类冲突
  • 线上的Python服务如何部署?