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://www.pswp.cn/news/922101.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/922101.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PHP 与 WebAssembly 的 “天然隔阂”

WebAssembly&#xff08;简称 WASM&#xff09;是一种低级二进制指令格式&#xff0c;旨在为高级语言提供高性能的编译目标&#xff0c;尤其在浏览器环境中实现接近原生的执行效率。它主要用于前端性能密集型场景&#xff08;如游戏引擎、视频编解码、3D 渲染等&#xff09;&am…

unity中通过拖拽,自定义scroll view中子物体顺序

1.在每个content的子物体上挂载DragHandler脚本&#xff0c;并且添加Canvs Group组件&#xff0c;设置见图2.DragHandler脚本内容&#xff1a;using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using System.Collections.Generic; using System.Coll…

用 Matplotlib 绘制饼图:从基础语法到实战美化,全面掌握分类数据可视化技巧

用 Matplotlib 绘制饼图:从基础语法到实战美化,全面掌握分类数据可视化技巧 在数据分析与可视化的世界里,**“图胜千言”**早已成为共识。而在众多图表类型中,饼图(Pie Chart)以其直观的比例展示方式,成为展示分类数据分布的常见选择。无论是业务报表、用户画像,还是市…

基础算法之二分算法 --- 2

大家好&#xff0c;不同的时间&#xff0c;相同的地点&#xff0c;时隔多日我们又见面了。继上次的二分算法后&#xff0c;我们这次要来学习的是二分答案了。这个部分相较于前面的二分算法难度有相当的提升&#xff0c;希望大家有所准备。虽然难度增加了&#xff0c;但是博主还…

发挥nano banana的最大能力

1. 概述Nano Banana 简介&#xff1a;Nano Banana 是 Google DeepMind 开发的 AI 图像生成与编辑模型&#xff0c;集成在 Google Gemini 平台中&#xff08;具体为 Gemini 2.5 Flash 版本&#xff09;。它以高效的图像编辑能力闻名&#xff0c;尤其在角色一致性、光影理解和快速…

leetcode 面试题01.02判定是否互为字符重排

一、问题描述二、解题思路解法一&#xff1a;对s1和s2进行sort排序&#xff0c;返回s1是否等于s2&#xff1b;解法二&#xff1a;用哈希表分别来记录s1和s2中字符出现的次数&#xff0c;统计完后&#xff0c;判断两个哈希表是否相等;三、代码实现解法一&#xff1a;时间复杂度&…

Python Yolo8 物体识别

支持单张图片/图片目录批量预标注 默认使用cuda GPU .env HTTP_PROXYhttp://192.168.2.109:10808 HTTPS_PROXYhttp://192.168.2.109:10808pyproject.toml [project] name "yolo-test" version "0.1.0" description "Add your description here&quo…

LeetCode100-234回文链表

本文基于各个大佬的文章上点关注下点赞&#xff0c;明天一定更灿烂&#xff01;前言Python基础好像会了又好像没会&#xff0c;所有我直接开始刷leetcode一边抄样例代码一边学习吧。本系列文章用来记录学习中的思考&#xff0c;写给自己看的&#xff0c;也欢迎大家在评论区指导…

BUG排查流程

引言简述Bug排查的重要性分享个人或团队在Bug排查中的常见挑战引出日记形式记录的价值日记格式设计时间戳&#xff1a;记录问题发现和解决的时间节点问题描述&#xff1a;清晰定义Bug的现象和影响范围环境信息&#xff1a;操作系统、版本号、依赖库等关键配置复现步骤&#xff…

汽车功能安全 Functional Safety ISO 26262 测试之一

汽车电子电气系统的日益复杂使得功能安全成为保障车辆可靠性和驾乘安全的关键。 本文将围绕ISO 26262标准的核心内容展开&#xff0c;帮助大家理解如何通过系统化的方法控制风险&#xff0c;进行测试&#xff0c;确保产品安全。 01 什么是功能安全&#xff1f; 首先&#xff0c…

人形机器人赛道的隐形胜负手:低延迟视频链路如何决定机器人未来

一、引言&#xff1a;爆发前夜的人形机器人赛道 2025 年&#xff0c;被业内称为“人形机器人量产元年”。政策与资本的合力&#xff0c;让这条原本还带着科幻色彩的产业赛道&#xff0c;骤然进入现实加速期。国家层面&#xff0c;《“机器人”行动计划》明确提出要推动人形机器…

从iPhone 17取消SIM卡槽,看企业如何告别“数据孤岛”

9月10日&#xff0c;苹果公司如期召开秋季新品发布会&#xff0c;正式推出iPhone 17系列。除了性能和拍照的常规升级&#xff0c;一个看似不起眼但意义深远的改变引起了广泛关注——iPhone 17 Pro系列全面取消了实体SIM卡槽&#xff0c;只保留了eSIM功能。这一举动不仅仅是技术…

【JavaWeb01】Web介绍

文章目录1.导学2.Web开发介绍2.1 Web网站的工作流程2.2 前后端分离开发1.导学 2.Web开发介绍 2.1 Web网站的工作流程 浏览器根据请求的域名请求对应的前端服务器&#xff0c;前端服务器接收到请求之后&#xff0c;把对应的前端代码返回给服务器。浏览器中有解析前端代码的解析引…

链路预测算法MATLAB实现

链路预测算法MATLAB实现 链路预测是复杂网络分析中的重要任务&#xff0c;旨在预测网络中尚未连接的两个节点之间未来产生连接的可能性。 程序概述 MATLAB程序实现了以下链路预测算法&#xff1a; 基于局部信息的相似性指标&#xff08;Common Neighbors, Jaccard, Adamic-Adar…

淘宝商品详情 API 的安全强化与生态协同创新路径

一、安全强化&#xff1a;从 “被动防御” 到 “主动免疫” 的体系升级动态身份认证与权限颗粒化构建 “生物特征 设备指纹 行为基线” 的三重认证机制&#xff1a;结合用户操作习惯&#xff08;如点击间隔、滑动轨迹&#xff09;生成动态令牌&#xff0c;对高权限接口&#…

快消26届联合利华校招AI测评及第二轮线上认知能力测评SHL笔试真题及评分要求

在求职的道路上&#xff0c;联合利华作为一家全球知名企业&#xff0c;其招聘流程一直备受关注。尤其是其AI面试环节&#xff0c;更是让许多求职者既期待又紧张。本文将详细总结联合利华AI面试的规律与应对策略&#xff0c;希望能为正在准备面试的你提供一些帮助。一、联合利华…

使用Langchain生成本地rag知识库并搭载大模型

准备设备&#xff1a; 手机aidlux2.0个人版 一、下载依赖pip install langchain langchain-community faiss-cpu pypdf二、安装ollama并下载模型 curl -fsSL https://ollama.com/install.sh | sh #需要科学上网 ollama serve & #让ollama服务在后台运行安装完毕可以查看oll…

L2-【英音】地道语音语调--语调

文章目录语调英式语调四步法语调含义降调升调降升调升降语调如何正确表情达意1. 用降调的句型语调 英语里没有任何一句话具有固定节奏模式 英式语调四步法 意群划分重音核心语调&#xff08;重中之重&#xff09;语调的选择 A French burglar broke-into-a flat while the o…

计算机视觉进阶教学之图像投影(透视)变换

目录 简介 一、了解图像投影(透视)变换 一、定义与原理 二、应用场景 三、实现方法 二、案例分析 1. 辅助函数定义 1.1.cv_show 函数 1.2.order_points 函数 1.3.four_point_transform 函数 1.4.resize 函数 2. 主程序执行流程 2.1.图像缩放处理 2.2.轮廓检测 2.…

Java面试问题记录(二)

三、系统设计与问题排查1、假设你要设计一个 “秒杀系统”&#xff0c;需要考虑高并发、高可用、防超卖等问题&#xff0c;你的整体技术方案是什么&#xff1f;从前端、接口层、服务层、存储层分别说说核心设计点。秒杀系统设计设计核心&#xff1a;瞬时高并发&#xff0c;库存…