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

【Redis】Redis缓存应用实战Day12(2026年)

写在前面

Redis作为缓存中间件,是系统架构中不可或缺的一环。但缓存使用不当,反而会带来一系列问题。今天我们来深入探讨缓存三大经典问题:缓存穿透、缓存击穿、缓存雪崩,以及它们的解决方案。

文章目录

    • 写在前面
    • 一、缓存穿透
      • 1.1 什么是缓存穿透
      • 1.2 缓存穿透的危害
      • 1.3 解决方案一:布隆过滤器
      • 1.4 解决方案二:空值缓存
      • 1.5 两种方案对比
    • 二、缓存击穿
      • 2.1 什么是缓存击穿
      • 2.2 缓存击穿与穿透的区别
      • 2.3 解决方案一:互斥锁
      • 2.4 解决方案二:热点数据预热
      • 2.5 解决方案三:逻辑过期
    • 三、缓存雪崩
      • 3.1 什么是缓存雪崩
      • 3.2 缓存雪崩的原因
      • 3.3 解决方案一:随机过期时间
      • 3.4 解决方案二:多级缓存
      • 3.5 解决方案三:熔断降级
      • 3.6 缓存雪崩解决方案对比
    • 四、缓存更新策略
      • 4.1 常见更新策略
      • 4.2 Cache Aside模式详解
      • 4.3 缓存和数据库一致性问题
    • 五、踩坑提醒
      • 5.1 缓存和数据库一致性陷阱
      • 5.2 热点key问题
      • 5.3 大key问题
    • 六、面试高频考点
      • 6.1 如何解决缓存三兄弟(穿透、击穿、雪崩)?
      • 6.2 缓存和数据库如何保证一致性?
      • 6.3 为什么删除缓存而不是更新缓存?
    • 七、参考资料
    • 八、互动话题

一、缓存穿透

1.1 什么是缓存穿透

实际场景:黑客恶意查询不存在的数据,如查询id=-1的商品,导致请求直接穿透缓存打到数据库。

缓存穿透示意图

┌─────────┐ ┌─────────┐ ┌─────────┐ │ 请求 │ → │ 缓存 │ → │ 数据库 │ │(不存在key)│ │ (无数据) │ │ (无数据)│ └─────────┘ └─────────┘ └─────────┘ ↑ │ └──────────────────────────────┘ 每次都穿透到数据库

1.2 缓存穿透的危害

危害说明
数据库压力大量请求直接打到数据库
系统崩溃数据库负载过高导致宕机
资源浪费无效请求消耗系统资源

1.3 解决方案一:布隆过滤器

经验之谈:布隆过滤器是一种空间效率很高的数据结构,可以快速判断元素是否存在于集合中。

布隆过滤器原理

元素 → 多个哈希函数 → 位数组中多个位置设为1 查询时:所有位置都是1 → 可能存在 有位置是0 → 一定不存在

Redis实现布隆过滤器

# 使用RedisBloom模块# 添加元素BF.ADDusersuser1 BF.ADDusersuser2# 判断元素是否存在BF.EXISTSusersuser1# 返回1表示可能存在BF.EXISTSusersuser999# 返回0表示一定不存在

Java代码示例

// 使用Guava布隆过滤器BloomFilter<String>bloomFilter=BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),1000000,// 预期元素数量0.01// 误判率);// 添加所有有效keyfor(Stringkey:allValidKeys){bloomFilter.put(key);}// 查询前先判断if(!bloomFilter.mightContain(key)){returnnull;// 一定不存在,直接返回}

1.4 解决方案二:空值缓存

踩坑提醒:空值缓存会占用内存,需要设置较短的过期时间,避免内存浪费。

publicObjectgetValue(Stringkey){// 1. 查询缓存Objectvalue=redisTemplate.opsForValue().get(key);// 2. 缓存命中if(value!=null){// 空值标记if("NULL".equals(value)){returnnull;}returnvalue;}// 3. 查询数据库value=database.query(key);// 4. 写入缓存if(value==null){// 空值缓存,过期时间较短redisTemplate.opsForValue().set(key,"NULL",5,TimeUnit.MINUTES);}else{redisTemplate.opsForValue().set(key,value,1,TimeUnit.HOURS);}returnvalue;}

1.5 两种方案对比

对比项布隆过滤器空值缓存
空间占用较大
精确度有误判率精确
实现复杂度较高简单
适用场景数据量大、固定集合数据量小、动态变化
维护成本需要重建过滤器自动过期

二、缓存击穿

2.1 什么是缓存击穿

实际场景:某热点商品缓存过期瞬间,大量并发请求同时查询该商品,全部穿透到数据库。

缓存击穿示意图

┌─────────┐ │ 请求1 │ │ 请求2 │ ┌─────────┐ ┌─────────┐ │ 请求3 │ → │ 缓存 │ → │ 数据库 │ │ ... │ │ (过期) │ │ (压力) │ │ 请求N │ └─────────┘ └─────────┘ └─────────┘ 热点key过期瞬间大量请求

2.2 缓存击穿与穿透的区别

对比项缓存穿透缓存击穿
数据是否存在不存在存在但过期了
请求特点恶意请求不存在的key热点key过期瞬间大量请求
影响范围持续影响瞬间影响
解决方案布隆过滤器、空值缓存互斥锁、热点预热

2.3 解决方案一:互斥锁

经验之谈:使用分布式锁保证只有一个线程去查询数据库并更新缓存,其他线程等待或返回旧数据。

publicObjectgetValueWithLock(Stringkey){// 1. 查询缓存Objectvalue=redisTemplate.opsForValue().get(key);if(value!=null){returnvalue;}// 2. 获取分布式锁StringlockKey="lock:"+key;try{// 尝试获取锁,等待时间3秒,锁过期时间10秒Booleanlocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",10,TimeUnit.SECONDS);if(Boolean.TRUE.equals(locked)){// 获取锁成功,查询数据库value=database.query(key);// 写入缓存if(value!=null){redisTemplate.opsForValue().set(key,value,1,TimeUnit.HOURS);}}else{// 获取锁失败,等待后重试Thread.sleep(100);returngetValueWithLock(key);// 递归重试}}finally{// 释放锁redisTemplate.delete(lockKey);}returnvalue;}

2.4 解决方案二:热点数据预热

实际场景:双十一大促前,提前将热点商品数据加载到缓存,并设置较长的过期时间。

@ComponentpublicclassCacheWarmUp{@AutowiredprivateRedisTemplateredisTemplate;@AutowiredprivateProductServiceproductService;// 系统启动时预热@PostConstructpublicvoidwarmUp(){// 获取热点商品列表List<Long>hotProductIds=productService.getHotProductIds();for(Longid:hotProductIds){Productproduct=productService.getById(id);if(product!=null){// 预热缓存,设置较长过期时间Stringkey="product:"+id;redisTemplate.opsForValue().set(key,product,24,TimeUnit.HOURS);}}}}

2.5 解决方案三:逻辑过期

经验之谈:不设置TTL,而是在value中存储过期时间,后台异步更新缓存。

@DatapublicclassCacheData<T>{privateTdata;privateLongexpireTime;// 逻辑过期时间}publicObjectgetValueWithLogicalExpire(Stringkey){// 1. 查询缓存Stringjson=redisTemplate.opsForValue().get(key);if(json==null){returnnull;// 直接返回,不查数据库}// 2. 解析数据CacheDatacacheData=JSON.parseObject(json,CacheData.class);// 3. 判断是否过期if(cacheData.getExpireTime()>System.currentTimeMillis()){returncacheData.getData();// 未过期}// 4. 过期了,异步更新CompletableFuture.runAsync(()->{// 获取锁StringlockKey="lock:"+key;Booleanlocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",10,TimeUnit.SECONDS);if(Boolean.TRUE.equals(locked)){try{// 查询数据库ObjectnewData=database.query(key);// 更新缓存CacheDatanewCacheData=newCacheData();newCacheData.setData(newData);newCacheData.setExpireTime(System.currentTimeMillis()+3600000);redisTemplate.opsForValue().set(key,JSON.toJSONString(newCacheData));}finally{redisTemplate.delete(lockKey);}}});// 5. 返回旧数据returncacheData.getData();}

三、缓存雪崩

3.1 什么是缓存雪崩

实际场景:凌晨2点,大量缓存同时过期,瞬间大量请求打到数据库,导致数据库崩溃。

缓存雪崩示意图

时间轴: ├──────┼──────┼──────┼──────┤ 0:00 1:00 2:00 3:00 4:00 ↑ ↑ ↑ key1 key2 key3 过期 过期 过期 ↓ ↓ ↓ └──────┴──────┘ 同时大量请求打到数据库

3.2 缓存雪崩的原因

原因说明
同时过期大量key设置了相同的过期时间
Redis宕机缓存服务不可用
网络问题缓存服务网络故障

3.3 解决方案一:随机过期时间

经验之谈:在基础过期时间上增加随机值,避免大量key同时过期。

publicvoidsetCacheWithRandomExpire(Stringkey,Objectvalue){// 基础过期时间:1小时longbaseExpire=3600;// 随机过期时间:0-600秒longrandomExpire=newRandom().nextInt(600);// 总过期时间longtotalExpire=baseExpire+randomExpire;redisTemplate.opsForValue().set(key,value,totalExpire,TimeUnit.SECONDS);}

3.4 解决方案二:多级缓存

实际场景:使用本地缓存+Redis缓存的多级缓存架构,即使Redis不可用,本地缓存还能扛一阵。

请求 → 本地缓存(Caffeine) → Redis缓存 → 数据库 (一级缓存) (二级缓存) (数据源)

多级缓存实现

@ComponentpublicclassMultiLevelCache{@AutowiredprivateRedisTemplateredisTemplate;// 本地缓存privateCache<String,Object>localCache=Caffeine.newBuilder().maximumSize(10000).expireAfterWrite(5,TimeUnit.MINUTES).build();publicObjectget(Stringkey){// 1. 先查本地缓存Objectvalue=localCache.getIfPresent(key);if(value!=null){returnvalue;}// 2. 再查Redis缓存value=redisTemplate.opsForValue().get(key);if(value!=null){// 写入本地缓存localCache.put(key,value);returnvalue;}// 3. 查询数据库value=database.query(key);if(value!=null){// 写入两级缓存redisTemplate.opsForValue().set(key,value,1,TimeUnit.HOURS);localCache.put(key,value);}returnvalue;}}

3.5 解决方案三:熔断降级

踩坑提醒:熔断降级是最后的防线,当缓存和数据库都扛不住时,通过限流保护系统。

@ComponentpublicclassCacheService{// 熔断器privateCircuitBreakercircuitBreaker=CircuitBreaker.create("cacheBreaker",CircuitBreakerConfig.custom().failureRateThreshold(50)// 失败率50%触发熔断.waitDurationInOpenState(Duration.ofSeconds(30))// 熔断30秒.build());publicObjectgetWithCircuitBreaker(Stringkey){returncircuitBreaker.executeSupplier(()->{Objectvalue=redisTemplate.opsForValue().get(key);if(value==null){value=database.query(key);if(value!=null){redisTemplate.opsForValue().set(key,value,1,TimeUnit.HOURS);}}returnvalue;},()->{// 降级逻辑:返回默认值returngetDefaultvalue(key);});}}

3.6 缓存雪崩解决方案对比

方案优点缺点适用场景
随机过期时间简单易实现不能完全避免常规场景
多级缓存性能高、容错强数据一致性复杂高并发场景
熔断降级保护系统影响用户体验极端情况

四、缓存更新策略

4.1 常见更新策略

实际场景:缓存和数据库数据一致性是分布式系统的经典难题,需要根据业务场景选择合适的策略。

策略描述一致性性能适用场景
Cache Aside先更新DB,再删除缓存较好较好读多写少
Read/Write Through由缓存代理更新DB读写均衡
Write Behind只更新缓存,异步更新DB最好写多读少

4.2 Cache Aside模式详解

面试高频考点:为什么是删除缓存而不是更新缓存?

删除 vs 更新

对比项删除缓存更新缓存
复杂度
数据一致性较好可能不一致
性能高(懒加载)低(每次写都更新)
并发问题较少较多

Cache Aside实现

publicvoidupdateData(Stringkey,Objectvalue){// 1. 先更新数据库database.update(key,value);// 2. 再删除缓存redisTemplate.delete(key);}

4.3 缓存和数据库一致性问题

踩坑提醒:在高并发场景下,即使先更新DB再删除缓存,也可能出现不一致。

问题场景

线程A: 更新DB → 删除缓存 线程B: 读缓存miss → 查DB(旧数据) → 写缓存 如果线程B在线程A删除缓存前写入,缓存就是旧数据

解决方案:延迟双删

publicvoidupdateData(Stringkey,Objectvalue){// 1. 先删除缓存redisTemplate.delete(key);// 2. 更新数据库database.update(key,value);// 3. 延迟后再次删除缓存CompletableFuture.runAsync(()->{try{Thread.sleep(500);// 延迟500msredisTemplate.delete(key);}catch(InterruptedExceptione){log.error("延迟双删失败",e);}});}

五、踩坑提醒

5.1 缓存和数据库一致性陷阱

陷阱说明解决方案
先删缓存再更新DB并发时可能读到旧数据写入缓存使用延迟双删
缓存删除失败数据库更新成功但缓存删除失败使用消息队列重试
并发写问题多线程同时写导致数据错乱使用分布式锁

5.2 热点key问题

踩坑提醒:热点key会导致单个Redis节点压力过大,需要特殊处理。

解决方案

// 方案1:热点key分散String[]keys={"hot:1","hot:2","hot:3"};intindex=newRandom().nextInt(keys.length);Objectvalue=redisTemplate.opsForValue().get(keys[index]);// 方案2:本地缓存// 使用Caffeine等本地缓存框架

5.3 大key问题

问题说明解决方案
内存占用大单个key占用过多内存拆分大key
网络阻塞传输大key阻塞网络压缩或分片
过期阻塞删除大key阻塞主线程异步删除

六、面试高频考点

6.1 如何解决缓存三兄弟(穿透、击穿、雪崩)?

答案

缓存穿透

  • 布隆过滤器:过滤不存在的key
  • 空值缓存:缓存空值,设置短过期时间
  • 参数校验:在入口处过滤非法请求

缓存击穿

  • 互斥锁:只允许一个线程查询数据库
  • 热点预热:提前加载热点数据
  • 逻辑过期:不设置TTL,后台异步更新

缓存雪崩

  • 随机过期时间:避免同时过期
  • 多级缓存:本地缓存+Redis缓存
  • 熔断降级:保护系统不被压垮

6.2 缓存和数据库如何保证一致性?

答案

  1. Cache Aside模式:先更新数据库,再删除缓存

  2. 延迟双删:删除缓存 → 更新DB → 延迟后再删除缓存

  3. 消息队列重试:删除缓存失败时,通过MQ重试

  4. Binlog订阅:通过Canal订阅MySQL binlog,异步更新缓存

  5. 强一致性场景:使用分布式锁或直接查数据库

6.3 为什么删除缓存而不是更新缓存?

答案

  1. 并发安全:删除操作是幂等的,更新可能被覆盖

  2. 性能考虑:很多场景下缓存可能根本不会被读取,更新是浪费

  3. 数据一致性:更新缓存可能失败,导致数据不一致

  4. 懒加载:删除后下次读取时再加载,数据更新鲜


七、参考资料

  1. Redis官方文档 - 缓存模式
  2. Cache Aside Pattern详解

八、互动话题

  1. 你的项目中遇到过缓存穿透、击穿、雪崩吗?是如何解决的?
  2. 对于强一致性要求的业务,你会如何设计缓存策略?
  3. 多级缓存的方案在实际应用中有什么坑?

欢迎在评论区分享你的实战经验!


下期预告:Day13我们将学习Redis分布式锁,深入理解SETNX、Redisson和Redlock算法。

http://www.cnnetsun.cn/news/2777395.html

相关文章:

  • 美陈雕塑构思卡壳?5 个宝藏网站,帮你摆脱创作难题
  • 英语专业论文怎么降低重复率?
  • git status
  • 写mysql数据库日志的时机
  • 2026年实测10款降AI率网站推荐:免费与付费全对比,毕业论文降低ai率必看
  • 如何用LRCGET批量歌词同步工具一键解决离线音乐库歌词管理难题
  • 在Apple Silicon Mac上无缝运行Windows程序的完整指南:Whisky让你的Mac更强大
  • 目标检测调参实战:用CIOU Loss在YOLOv5/v8上提升mAP的完整流程
  • 如何在macOS上获得终极视频预览体验:QLVideo完整指南
  • 计算机小程序毕设实战-基于springboot+微信小程序的视频点播微信小程序【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • 突破JSXBIN加密壁垒:Jsxer如何成为Adobe脚本开发者的得力伙伴
  • 东南亚海外仓丢件到底谁责任?5步锁定丢在哪个环节
  • Python 爬虫监控告警:日志结构化 + 异常告警 + 采集速率实时监控落地
  • 为什么硬件工程师越来越多,高水平工程师却越来越难招?
  • 如何快速掌握炉石传说自动化脚本:终极完整指南
  • 极速启动:利用快马ai在五分钟内构建jdk17特性演示原型
  • 在 Oracle EBS 中,要在同一个 OU(运营单元)下实现不同交易走不同的公司段(Company Segment / Balancing Segment),核心思路是利用 SLA(子分类账会计)
  • Python入门到精通:零基础学习指南
  • Rust特征静态与动态分发在FFI内存管理中的i-cache性能对比
  • 离散味对称性在粒子物理模型中的应用与实现
  • Unity之使用火山引擎实现文字提问流式回复
  • 滑模控制抖振抑制方案:模糊切换+自适应律的Simulink实现包
  • 移动端APP开发:MonkeyCode在 Flutter 中的应用
  • iOS背景移除终极方案:3大优势让你轻松实现专业级图像处理
  • 深入 Raft 共识协议:基于 Rust 的极简 Leader 选举与心跳维持机制实现
  • 实战避坑指南:FFmpeg处理YUV420 NV12/P010数据时,内存对齐与性能优化的那些事儿
  • Veo风格迁移部署踩坑清单:从A100到RTX 4090,6类硬件下显存溢出的5种精准定位法(含nvidia-smi实时诊断脚本)
  • 从零到交付:AI工具学习路径规划全链路拆解,含L1-L5能力跃迁评估表与动态校准机制
  • C语言开篇
  • 从502错误到丝滑pub get:一份Flutter镜像配置的防坑与自动化配置指南