【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布隆过滤器BloomFilterStringbloomFilterBloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),1000000,// 预期元素数量0.01// 误判率);// 添加所有有效keyfor(Stringkey:allValidKeys){bloomFilter.put(key);}// 查询前先判断if(!bloomFilter.mightContain(key)){returnnull;// 一定不存在直接返回}1.4 解决方案二空值缓存踩坑提醒空值缓存会占用内存需要设置较短的过期时间避免内存浪费。publicObjectgetValue(Stringkey){// 1. 查询缓存ObjectvalueredisTemplate.opsForValue().get(key);// 2. 缓存命中if(value!null){// 空值标记if(NULL.equals(value)){returnnull;}returnvalue;}// 3. 查询数据库valuedatabase.query(key);// 4. 写入缓存if(valuenull){// 空值缓存过期时间较短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. 查询缓存ObjectvalueredisTemplate.opsForValue().get(key);if(value!null){returnvalue;}// 2. 获取分布式锁StringlockKeylock:key;try{// 尝试获取锁等待时间3秒锁过期时间10秒BooleanlockedredisTemplate.opsForValue().setIfAbsent(lockKey,1,10,TimeUnit.SECONDS);if(Boolean.TRUE.equals(locked)){// 获取锁成功查询数据库valuedatabase.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(){// 获取热点商品列表ListLonghotProductIdsproductService.getHotProductIds();for(Longid:hotProductIds){ProductproductproductService.getById(id);if(product!null){// 预热缓存设置较长过期时间Stringkeyproduct:id;redisTemplate.opsForValue().set(key,product,24,TimeUnit.HOURS);}}}}2.5 解决方案三逻辑过期经验之谈不设置TTL而是在value中存储过期时间后台异步更新缓存。DatapublicclassCacheDataT{privateTdata;privateLongexpireTime;// 逻辑过期时间}publicObjectgetValueWithLogicalExpire(Stringkey){// 1. 查询缓存StringjsonredisTemplate.opsForValue().get(key);if(jsonnull){returnnull;// 直接返回不查数据库}// 2. 解析数据CacheDatacacheDataJSON.parseObject(json,CacheData.class);// 3. 判断是否过期if(cacheData.getExpireTime()System.currentTimeMillis()){returncacheData.getData();// 未过期}// 4. 过期了异步更新CompletableFuture.runAsync(()-{// 获取锁StringlockKeylock:key;BooleanlockedredisTemplate.opsForValue().setIfAbsent(lockKey,1,10,TimeUnit.SECONDS);if(Boolean.TRUE.equals(locked)){try{// 查询数据库ObjectnewDatadatabase.query(key);// 更新缓存CacheDatanewCacheDatanewCacheData();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小时longbaseExpire3600;// 随机过期时间0-600秒longrandomExpirenewRandom().nextInt(600);// 总过期时间longtotalExpirebaseExpirerandomExpire;redisTemplate.opsForValue().set(key,value,totalExpire,TimeUnit.SECONDS);}3.4 解决方案二多级缓存实际场景使用本地缓存Redis缓存的多级缓存架构即使Redis不可用本地缓存还能扛一阵。请求 → 本地缓存(Caffeine) → Redis缓存 → 数据库 (一级缓存) (二级缓存) (数据源)多级缓存实现ComponentpublicclassMultiLevelCache{AutowiredprivateRedisTemplateredisTemplate;// 本地缓存privateCacheString,ObjectlocalCacheCaffeine.newBuilder().maximumSize(10000).expireAfterWrite(5,TimeUnit.MINUTES).build();publicObjectget(Stringkey){// 1. 先查本地缓存ObjectvaluelocalCache.getIfPresent(key);if(value!null){returnvalue;}// 2. 再查Redis缓存valueredisTemplate.opsForValue().get(key);if(value!null){// 写入本地缓存localCache.put(key,value);returnvalue;}// 3. 查询数据库valuedatabase.query(key);if(value!null){// 写入两级缓存redisTemplate.opsForValue().set(key,value,1,TimeUnit.HOURS);localCache.put(key,value);}returnvalue;}}3.5 解决方案三熔断降级踩坑提醒熔断降级是最后的防线当缓存和数据库都扛不住时通过限流保护系统。ComponentpublicclassCacheService{// 熔断器privateCircuitBreakercircuitBreakerCircuitBreaker.create(cacheBreaker,CircuitBreakerConfig.custom().failureRateThreshold(50)// 失败率50%触发熔断.waitDurationInOpenState(Duration.ofSeconds(30))// 熔断30秒.build());publicObjectgetWithCircuitBreaker(Stringkey){returncircuitBreaker.executeSupplier(()-{ObjectvalueredisTemplate.opsForValue().get(key);if(valuenull){valuedatabase.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};intindexnewRandom().nextInt(keys.length);ObjectvalueredisTemplate.opsForValue().get(keys[index]);// 方案2本地缓存// 使用Caffeine等本地缓存框架5.3 大key问题问题说明解决方案内存占用大单个key占用过多内存拆分大key网络阻塞传输大key阻塞网络压缩或分片过期阻塞删除大key阻塞主线程异步删除六、面试高频考点6.1 如何解决缓存三兄弟穿透、击穿、雪崩答案缓存穿透布隆过滤器过滤不存在的key空值缓存缓存空值设置短过期时间参数校验在入口处过滤非法请求缓存击穿互斥锁只允许一个线程查询数据库热点预热提前加载热点数据逻辑过期不设置TTL后台异步更新缓存雪崩随机过期时间避免同时过期多级缓存本地缓存Redis缓存熔断降级保护系统不被压垮6.2 缓存和数据库如何保证一致性答案Cache Aside模式先更新数据库再删除缓存延迟双删删除缓存 → 更新DB → 延迟后再删除缓存消息队列重试删除缓存失败时通过MQ重试Binlog订阅通过Canal订阅MySQL binlog异步更新缓存强一致性场景使用分布式锁或直接查数据库6.3 为什么删除缓存而不是更新缓存答案并发安全删除操作是幂等的更新可能被覆盖性能考虑很多场景下缓存可能根本不会被读取更新是浪费数据一致性更新缓存可能失败导致数据不一致懒加载删除后下次读取时再加载数据更新鲜七、参考资料Redis官方文档 - 缓存模式Cache Aside Pattern详解八、互动话题你的项目中遇到过缓存穿透、击穿、雪崩吗是如何解决的对于强一致性要求的业务你会如何设计缓存策略多级缓存的方案在实际应用中有什么坑欢迎在评论区分享你的实战经验下期预告Day13我们将学习Redis分布式锁深入理解SETNX、Redisson和Redlock算法。