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

缓存常见问题与解决方案

缓存常见问题与解决方案

文章目录

  • 缓存常见问题与解决方案
    • 1、缓存穿透
      • 1.1、 概述
      • 1.2 、非注解缓存解决方案
      • 1.3 、注解缓存解决方案
        • 代码示例:
    • 2 、缓存雪崩
      • 2.1、概述
      • 2.2 、非注解缓存解决方案
        • 代码示例:
      • 2.3 、注解缓存解决方案
        • 代码示例:
    • 3、缓存击穿
      • 3.1、概述
      • 3.2、非注解缓存解决方案
        • 代码示例:
        • 优化备注:
      • 3.3 注解缓存解决方案
        • 代码示例:
        • 优化备注:
    • 4、总结

1、缓存穿透

1.1、 概述

​ 缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

@Overridepublic SkuInfo findBySkuInfoId(Integer skuInfoId) {/*** 首先去redis中根据key查询是否缓存了key的对应相关信息。* 1. 如果没有,说明是第一次访问这个key,那么就查询数据库,再把相关数据存入redis*2. 如果有,说明之前缓存过这个key,那么就从redis中取数据,不再查数据库*///1.查询缓存String key = "sku:" + skuInfoId + ":info";Object value = redisUtils.get(key);SkuInfo skuInfo;if (value != null) {//说明缓存中缓存这个sku,直接取出缓存数据skuInfo = new Gson().fromJson(value.toString(), SkuInfo.class);} else {//说明缓存中没有缓存这个sku,查数据并缓存//1.查询sku info表skuInfo = skuInfoMapper.selectByPrimaryKey(skuInfoId.longValue());if (skuInfo != null) {//2.查询sku_image表:sku对应的图片SkuImageExample skuImageExample = new SkuImageExample();skuImageExample.createCriteria().andSkuIdEqualTo(skuInfoId.longValue());List<SkuImage> skuImages = skuImageMapper.selectByExample(skuImageExample);//3.将查询到的结果封装到sku对象中skuInfo.setSkuImages(skuImages);//4.将sku对象序列化String json = new Gson().toJson(skuInfo);//5.将序列化后的数据存入缓存中redisUtils.set(key, json);}}return skuInfo;}

我们分析一下上述代码,如果有人恶意的拿一个不存在的key去查询数据,此时redis中没有相应的缓存数据,这就会绕过redis频繁的去调用数据库查询,这样就会给数据库造成压力。

​ 有很多种方法可以有效地解决缓存穿透问题,我们选择一种,如果一个查询返回的数据为空 (不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。


1.2 、非注解缓存解决方案

@Overridepublic SkuInfo findBySkuInfoId(Integer skuInfoId) {/*** 首先去redis中根据key查询是否缓存了key的对应相关信息。* 1. 如果没有,说明是第一次访问这个key,那么就查询数据库,再把相关数据存入redis*2. 如果有,说明之前缓存过这个key,那么就从redis中取数据,不再查数据库*///1.查询缓存String key = "sku:" + skuInfoId + ":info";Object value = redisUtils.get(key);SkuInfo skuInfo;if (value != null) {//说明缓存中缓存这个sku,直接取出缓存数据skuInfo = new Gson().fromJson(value.toString(), SkuInfo.class);} else {//说明缓存中没有缓存这个sku,查数据并缓存//1.查询sku info表skuInfo = skuInfoMapper.selectByPrimaryKey(skuInfoId.longValue());if (skuInfo != null) {//2.查询sku_image表:sku对应的图片SkuImageExample skuImageExample = new SkuImageExample();skuImageExample.createCriteria().andSkuIdEqualTo(skuInfoId.longValue());List<SkuImage> skuImages = skuImageMapper.selectByExample(skuImageExample);//3.将查询到的结果封装到sku对象中skuInfo.setSkuImages(skuImages);//4.将sku对象序列化String json = new Gson().toJson(skuInfo);//5.将序列化后的数据存入缓存中redisUtils.set(key, json);} else {//说明数据库中没有这个sku,此时也将这个null数据进行缓存,并且设置过期时间为5minredisUtils.set(key, null, 5, TimeUnit.MINUTES);}}return skuInfo;}

1.3 、注解缓存解决方案

基于 Spring Cache 注解式缓存解决缓存穿透,核心思路与非注解式一致:缓存空值并设置较短过期时间。需要通过配置CacheManager和注解属性配合实现。

代码示例:
// 1. 配置Redis缓存管理器(设置默认过期时间及空值处理)
@Configuration
@EnableCaching
public class RedisCacheConfig {@Beanpublic RedisCacheManager cacheManager(RedisConnectionFactory factory) {// 默认配置(非空值缓存)RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(2)) // 非空值默认过期时间2小时.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));// 空值缓存配置(单独设置较短过期时间)RedisCacheConfiguration nullValueCacheConfig = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(5)) // 空值缓存5分钟.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))// 允许缓存null值.disableCachingNullValues(false);// 针对不同缓存名称设置不同配置(这里对skuInfo缓存单独配置空值策略)Map<String, RedisCacheConfiguration> configMap = new HashMap<>();configMap.put("skuInfo", nullValueCacheConfig);return RedisCacheManager.builder(factory).cacheDefaults(defaultCacheConfig) // 默认配置.withInitialCacheConfigurations(configMap) // 特殊缓存配置.build();}
}// 2. 业务层使用注解
@Service
public class SkuInfoService {@Autowiredprivate SkuInfoMapper skuInfoMapper;@Autowiredprivate SkuImageMapper skuImageMapper;/*** @Cacheable:查询缓存,不存在则执行方法并缓存结果*  key:缓存key*  cacheNames:缓存名称(对应上面配置的skuInfo)*  unless:结果为null时不缓存?这里设置为false,允许缓存null*/@Cacheable(key = "'sku:'+#skuInfoId+':info'", cacheNames = "skuInfo", unless = "#result == null ? false : false")public SkuInfo findBySkuInfoId(Integer skuInfoId) {// 1.查询数据库SkuInfo skuInfo = skuInfoMapper.selectByPrimaryKey(skuInfoId.longValue());if (skuInfo != null) {// 2.查询关联图片SkuImageExample example = new SkuImageExample();example.createCriteria().andSkuIdEqualTo(skuInfoId.longValue());skuInfo.setSkuImages(skuImageMapper.selectByExample(example));}// 注意:这里会返回null,而注解配置会缓存null值(5分钟过期)return skuInfo;}
}
  1. 注解式缓存需通过RedisCacheConfiguration显式开启disableCachingNullValues(false)允许缓存 null
  2. 空值缓存必须设置较短过期时间(5 分钟内),避免长期占用内存
  3. unless属性用于控制是否缓存,这里配置为始终缓存(包括 null)
  4. 优势:代码更简洁,无需手动编写缓存逻辑;劣势:空值过期时间配置较固定,灵活性略低

2 、缓存雪崩

2.1、概述

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。


2.2 、非注解缓存解决方案

核心方案:给缓存过期时间添加随机偏移量,避免大量缓存同时失效。

代码示例:
@Override
public SkuInfo findBySkuInfoId(Integer skuInfoId) {String key = "sku:" + skuInfoId + ":info";Object value = redisUtils.get(key);SkuInfo skuInfo;if (value != null) {skuInfo = new Gson().fromJson(value.toString(), SkuInfo.class);} else {// 查询数据库skuInfo = skuInfoMapper.selectByPrimaryKey(skuInfoId.longValue());if (skuInfo != null) {// 补充关联数据SkuImageExample example = new SkuImageExample();example.createCriteria().andSkuIdEqualTo(skuInfoId.longValue());skuInfo.setSkuImages(skuImageMapper.selectByExample(example));// 缓存逻辑:基础过期时间+随机偏移量String json = new Gson().toJson(skuInfo);long baseExpire = 30; // 基础30分钟long random = new Random().nextInt(5); // 0-5分钟随机值redisUtils.set(key, json, baseExpire + random, TimeUnit.MINUTES);} else {// 空值缓存(同样加随机偏移,避免空值缓存同时失效)long nullExpire = 5 + new Random().nextInt(2); // 5-7分钟redisUtils.set(key, null, nullExpire, TimeUnit.MINUTES);}}return skuInfo;
}

2.3 、注解缓存解决方案

一般可以采用多级缓存,不同级别的缓存设置不同的超时时间,尽量避免集体失效,由于注解式的灵活度很低(高度封装),建议使用非注解式解决方案

注解式通过自定义缓存过期时间生成器,为不同 key 分配随机过期时间。

代码示例:
// 1. 自定义缓存过期时间生成器
public class RandomTtlRedisCacheWriter extends DefaultRedisCacheWriter {private final Duration baseTtl;private final int randomRange; // 随机范围(分钟)public RandomTtlRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration baseTtl, int randomRange) {super(connectionFactory);this.baseTtl = baseTtl;this.randomRange = randomRange;}@Overridepublic void put(String name, byte[] key, byte[] value, Duration ttl) {// 覆盖默认ttl,使用基础时间+随机值Duration actualTtl = baseTtl.plusMinutes(new Random().nextInt(randomRange));super.put(name, key, value, actualTtl);}
}// 2. 配置缓存管理器
@Configuration
@EnableCaching
public class RedisCacheConfig {@Beanpublic RedisCacheManager cacheManager(RedisConnectionFactory factory) {// 创建带随机过期时间的缓存写入器RandomTtlRedisCacheWriter writer = new RandomTtlRedisCacheWriter(factory, Duration.ofHours(2), // 基础2小时30 // 随机0-30分钟);RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));return RedisCacheManager.builder(writer).cacheDefaults(config).build();}
}// 3. 业务层使用(与普通注解一致)
@Service
public class SkuInfoService {@Cacheable(key = "'sku:'+#skuInfoId+':info'", cacheNames = "skuInfo")public SkuInfo findBySkuInfoId(Integer skuInfoId) {// 数据库查询逻辑(同上)}
}

3、缓存击穿

3.1、概述

在高并发的情况下,大量的请求同时查询同一个key时,此时这个key正好失效了,就会导致同一时间,这些请求都会去查询数据库,这样的现象我们称为缓存击穿

3.2、非注解缓存解决方案

核心方案:分布式锁 + 双重检查,确保同一时间只有一个请求查询数据库。

代码示例:
@Override
public SkuInfo findBySkuInfoId(Integer skuInfoId) {String key = "sku:" + skuInfoId + ":info";String lockKey = "lock:sku:" + skuInfoId; // 分布式锁keyObject value = redisUtils.get(key);SkuInfo skuInfo;if (value != null) {// 缓存命中skuInfo = new Gson().fromJson(value.toString(), SkuInfo.class);return skuInfo;}// 缓存未命中,尝试获取分布式锁boolean locked = false;try {// 获取锁(设置3秒过期,避免死锁)locked = redisUtils.tryLock(lockKey, 3, TimeUnit.SECONDS);if (locked) {// 双重检查:获取锁后再次检查缓存(防止锁等待期间已被其他请求更新)Object doubleCheck = redisUtils.get(key);if (doubleCheck != null) {return new Gson().fromJson(doubleCheck.toString(), SkuInfo.class);}// 查询数据库skuInfo = skuInfoMapper.selectByPrimaryKey(skuInfoId.longValue());if (skuInfo != null) {// 补充关联数据SkuImageExample example = new SkuImageExample();example.createCriteria().andSkuIdEqualTo(skuInfoId.longValue());skuInfo.setSkuImages(skuImageMapper.selectByExample(example));// 缓存数据(带随机过期时间)String json = new Gson().toJson(skuInfo);long expire = 30 + new Random().nextInt(5);redisUtils.set(key, json, expire, TimeUnit.MINUTES);} else {// 缓存空值redisUtils.set(key, null, 5, TimeUnit.MINUTES);}return skuInfo;} else {// 未获取到锁,等待50ms后重试Thread.sleep(50);return findBySkuInfoId(skuInfoId); // 递归重试}} catch (InterruptedException e) {Thread.currentThread().interrupt();return null;} finally {// 释放锁if (locked) {redisUtils.unlock(lockKey);}}
}
优化备注:
  1. 分布式锁必须设置过期时间,防止锁持有者宕机导致死锁
  2. 双重检查机制:获取锁后再次查询缓存,避免重复查询数据库
  3. 未获取到锁时应重试(而非直接返回),重试间隔建议 50-100ms
  4. 推荐使用 Redisson 等成熟框架实现分布式锁,而非自行实现tryLock

3.3 注解缓存解决方案

基于 Spring AOP 和分布式锁实现,通过自定义注解封装锁逻辑。

代码示例:
// 1. 自定义防击穿注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheBreakdownProtection {String lockKeyPrefix() default "lock:"; // 锁key前缀long lockExpire() default 3; // 锁过期时间(秒)long retryInterval() default 50; // 重试间隔(毫秒)
}// 2. AOP切面实现
@Aspect
@Component
public class CacheBreakdownAspect {@Autowiredprivate RedisUtils redisUtils;@Around("@annotation(protection)")public Object around(ProceedingJoinPoint joinPoint, CacheBreakdownProtection protection) throws Throwable {// 获取方法参数(假设第一个参数为ID)Object[] args = joinPoint.getArgs();String id = args[0].toString();String lockKey = protection.lockKeyPrefix() + id;try {// 尝试获取锁boolean locked = redisUtils.tryLock(lockKey, protection.lockExpire(), TimeUnit.SECONDS);if (locked) {// 获取锁成功,执行原方法return joinPoint.proceed();} else {// 未获取到锁,重试Thread.sleep(protection.retryInterval());return around(joinPoint, protection); // 递归重试}} finally {// 释放锁(需判断当前线程是否持有锁,避免误释放)if (redisUtils.isLocked(lockKey)) {redisUtils.unlock(lockKey);}}}
}// 3. 业务层使用
@Service
public class SkuInfoService {@Cacheable(key = "'sku:'+#skuInfoId+':info'", cacheNames = "skuInfo")@CacheBreakdownProtection(lockKeyPrefix = "lock:sku:") // 应用防击穿注解public SkuInfo findBySkuInfoId(Integer skuInfoId) {// 数据库查询逻辑(同上)}
}
优化备注:
  1. 注解式通过 AOP 封装锁逻辑,业务代码更简洁
  2. 需注意锁的粒度:建议按 ID 维度加锁(如lock:sku:1001),避免全局锁影响性能
  3. 重试次数需有限制(可在注解中增加maxRetry属性),防止无限重试导致栈溢出
  4. 适用于高并发读、低并发写的场景,如商品详情查询

4、总结

问题核心解决方案非注解式优势注解式优势
缓存穿透缓存空值 + 短期过期灵活性高代码简洁
缓存雪崩随机过期时间 + 多级缓存易定制全局管理方便
缓存击穿分布式锁 + 双重检查控制粒度细无侵入性

文章转载自:

http://5SW0HBes.hnzrL.cn
http://iBwRqcKn.hnzrL.cn
http://VsFPXAH1.hnzrL.cn
http://RvlAnZed.hnzrL.cn
http://fRmFRmgh.hnzrL.cn
http://mibhHcn6.hnzrL.cn
http://PXJVngio.hnzrL.cn
http://smPEU1MC.hnzrL.cn
http://piwXUp0G.hnzrL.cn
http://4d6SAKIY.hnzrL.cn
http://bdDypUSD.hnzrL.cn
http://HEuC5Unf.hnzrL.cn
http://o5m10uGj.hnzrL.cn
http://Z7zkJGbG.hnzrL.cn
http://s93ng03X.hnzrL.cn
http://hJyAD06h.hnzrL.cn
http://wpIGqMgi.hnzrL.cn
http://Igu8QEx1.hnzrL.cn
http://tvQIoTKV.hnzrL.cn
http://N7JzEczr.hnzrL.cn
http://bdOdGKYz.hnzrL.cn
http://DCx2bKEQ.hnzrL.cn
http://heJutz9D.hnzrL.cn
http://o9QAr0BR.hnzrL.cn
http://HNdnD2UO.hnzrL.cn
http://8mtPlQaq.hnzrL.cn
http://t8h4V6lv.hnzrL.cn
http://yZjQRU30.hnzrL.cn
http://kxXs1OwC.hnzrL.cn
http://SV6323PZ.hnzrL.cn
http://www.dtcms.com/a/381474.html

相关文章:

  • 【pure-admin】登录页面代码详解
  • 初学鸿蒙笔记-真机调试
  • 反序列化漏洞详解
  • 使用 vue-virtual-scroller 实现高性能传输列表功能总结
  • python 实现 transformer 的 position embeding
  • 003 cargo使用
  • 制作一个简单的vscode插件
  • 【算法详解】:从 模拟 开始打开算法密匙
  • kubeadm搭建生产环境的单master多node的k8s集群
  • RocketMQ存储核心:MappedFile解析
  • 7.k8s四层代理service
  • Stable Virtual Camera:Stability AI等推出的AI模型 ,2D图像轻松转3D视频
  • Golang并发编程及其高级特性
  • 给AI配一台手机+电脑?智谱AutoGLM上线!
  • 怎么在手机上选择一款好用的桌面待办清单工具
  • 傲琪人工合成石墨片:破解智能手机散热困境的创新解决方案
  • LeetCode 刷题【74. 搜索二维矩阵、75. 颜色分类、76. 最小覆盖子串】
  • 【Linux】【实战向】Linux 进程替换避坑指南:从理解 bash 阻塞等待,到亲手实现能执行 ls/cd 的 Shell
  • SRE 系列(七)| 从技术架构到团队组织
  • 网络安全-vulnhub-Web developer 1
  • 国产延时芯片EH3B05上电延时3秒开关机芯片方案超低功耗
  • vivado下载程序后不弹出ila窗口
  • 【VC】 error MSB8041: 此项目需要 MFC 库
  • S7-200 SMART PLC 安全全指南:配置、漏洞解析与复现防护
  • 点可云进销存商城如何部署在微信小程序
  • 安卓学习 之 界面切换
  • 从 IDE 到 CLI:AI 编程代理工具全景与落地指南(附对比矩阵与脚本化示例)
  • 王道数据结构 学习笔记
  • 畅阅读小程序|畅阅读系统|基于java的畅阅读系统小程序设计与实现(源码+数据库+文档)
  • 在springboot中使用mock做controller层单元测试,请求示例包括GET(带参数)、POST(带请求头)、下载文件、上传文件等