news 2026/6/5 18:16:54

【架构实战】分布式缓存策略:从缓存穿透到缓存雪崩的全链路防护

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【架构实战】分布式缓存策略:从缓存穿透到缓存雪崩的全链路防护

一、一次缓存雪崩让我记忆犹新

2019年某个凌晨2点,Redis集群由于一次网络抖动,大量Key同时过期。

那一瞬间,所有请求涌入数据库,数据库连接池瞬间耗尽,服务全部超时。

更糟糕的是,数据库扛不住压力也挂了。我们花了2个小时才恢复。

后来算了一下,那2个小时的停机损失超过200万。从那以后,我对缓存的理解深入了很多。


二、缓存三大问题

2.1 缓存穿透

缓存穿透:查询一个不存在的数据 请求 → 缓存未命中 → 数据库未命中 → 返回空 每次都穿透到数据库 原因: 1. 业务逻辑问题(查不存在的ID) 2. 恶意攻击(大量请求不存在的数据) 3. 数据被删除了,但缓存没更新
/** * 缓存穿透防护:布隆过滤器 + 空值缓存 */@Service@Slf4jpublicclassAntiPenetrationCacheService{@AutowiredprivateStringRedisTemplateredisTemplate;@AutowiredprivateBloomFilter<Long>productBloomFilter;privatestaticfinalStringNULL_CACHE_PREFIX="null:";privatestaticfinalDurationNULL_CACHE_TTL=Duration.ofMinutes(5);/** * 查询商品(防穿透) */publicProductgetProduct(LongproductId){StringcacheKey="product:"+productId;// 1. 布隆过滤器检查(第一道防线)if(!productBloomFilter.mightContain(productId)){log.warn("布隆过滤器拦截: productId={}",productId);returnnull;}// 2. 查询缓存Stringcached=redisTemplate.opsForValue().get(cacheKey);// 2.1 命中空值缓存if(NULL_CACHE_PREFIX.equals(cached)){returnnull;}// 2.2 命中正常缓存if(cached!=null){returnJSON.parseObject(cached,Product.class);}// 3. 查询数据库Productproduct=productMapper.selectById(productId);if(product!=null){// 正常数据,写入缓存redisTemplate.opsForValue().set(cacheKey,JSON.toJSONString(product),Duration.ofMinutes(30));}else{// 空值缓存(防止穿透)redisTemplate.opsForValue().set(cacheKey,NULL_CACHE_PREFIX,NULL_CACHE_TTL);}returnproduct;}/** * 初始化布隆过滤器 */@PostConstructpublicvoidinitBloomFilter(){productBloomFilter=BloomFilter.create(Funnels.longFunnel(),1000000,// 预期元素数量0.01// 误判率1%);// 加载所有商品IDList<Long>productIds=productMapper.selectAllIds();productIds.forEach(productBloomFilter::put);log.info("布隆过滤器初始化完成: count={}",productIds.size());}}

2.2 缓存击穿

缓存击穿:热点Key过期,大量请求同时访问 某个热点Key过期的瞬间,大量并发请求同时穿透到数据库 场景: 1. 热门商品详情 2. 热门活动页面 3. 微博热搜
/** * 缓存击穿防护:互斥锁 + 逻辑过期 */@Service@Slf4jpublicclassAntiBreakdownCacheService{@AutowiredprivateStringRedisTemplateredisTemplate;/** * 方案1:互斥锁(适合一般热点数据) */publicProductgetProductWithMutex(LongproductId){StringcacheKey="product:"+productId;StringlockKey="lock:product:"+productId;// 1. 查询缓存Stringcached=redisTemplate.opsForValue().get(cacheKey);if(cached!=null){returnJSON.parseObject(cached,Product.class);}// 2. 获取互斥锁Booleanlocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",10,TimeUnit.SECONDS);if(Boolean.TRUE.equals(locked)){try{// 3. 双重检查cached=redisTemplate.opsForValue().get(cacheKey);if(cached!=null){returnJSON.parseObject(cached,Product.class);}// 4. 查询数据库Productproduct=productMapper.selectById(productId);// 5. 写入缓存if(product!=null){redisTemplate.opsForValue().set(cacheKey,JSON.toJSONString(product),Duration.ofMinutes(30));}returnproduct;}finally{// 6. 释放锁redisTemplate.delete(lockKey);}}else{// 没拿到锁,等一会儿重试try{Thread.sleep(100);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}returngetProductWithMutex(productId);}}/** * 方案2:逻辑过期(适合超级热点数据) */publicProductgetProductWithLogicalExpire(LongproductId){StringcacheKey="product:logical:"+productId;// 1. 查询缓存Stringcached=redisTemplate.opsForValue().get(cacheKey);if(cached==null){// 首次加载returnloadAndCache(cacheKey,productId);}// 2. 解析逻辑过期时间CacheData<Product>cacheData=JSON.parseObject(cached,newTypeReference<CacheData<Product>>(){});if(cacheData.getExpireTime().isAfter(LocalDateTime.now())){// 未过期,直接返回returncacheData.getData();}// 3. 已过期,异步刷新StringlockKey="lock:logical:"+productId;Booleanlocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",10,TimeUnit.SECONDS);if(Boolean.TRUE.equals(locked)){// 拿到锁,异步刷新CompletableFuture.runAsync(()->{try{loadAndCache(cacheKey,productId);}finally{redisTemplate.delete(lockKey);}});}// 返回过期数据(保证可用性)returncacheData.getData();}}/** * 逻辑过期缓存数据 */@DataclassCacheData<T>{privateTdata;privateLocalDateTimeexpireTime;}

2.3 缓存雪崩

缓存雪崩:大量Key同时过期,或缓存服务宕机 原因: 1. 大量Key设置了相同的过期时间 2. 缓存服务重启或宕机 3. 缓存服务网络故障
/** * 缓存雪崩防护 */@Service@Slf4jpublicclassAntiAvalancheCacheService{@AutowiredprivateStringRedisTemplateredisTemplate;/** * 方案1:随机过期时间 */publicvoidsetWithRandomExpire(Stringkey,Objectvalue,longbaseMinutes){// 基础过期时间 + 随机时间(0~5分钟)longrandomMinutes=ThreadLocalRandom.current().nextLong(0,5);longtotalMinutes=baseMinutes+randomMinutes;redisTemplate.opsForValue().set(key,JSON.toJSONString(value),Duration.ofMinutes(totalMinutes));}/** * 方案2:多级缓存 */publicProductgetProductWithMultiLevel(LongproductId){// L1: 本地缓存(Caffeine)Productproduct=localCache.getIfPresent(productId);if(product!=null){returnproduct;}// L2: Redis缓存StringcacheKey="product:"+productId;Stringcached=redisTemplate.opsForValue().get(cacheKey);if(cached!=null){product=JSON.parseObject(cached,Product.class);localCache.put(productId,product);returnproduct;}// L3: 数据库product=productMapper.selectById(productId);if(product!=null){// 写入L2redisTemplate.opsForValue().set(cacheKey,JSON.toJSONString(product),Duration.ofMinutes(30+ThreadLocalRandom.current().nextLong(0,5)));// 写入L1localCache.put(productId,product);}returnproduct;}/** * 方案3:熔断降级 */publicProductgetProductWithCircuitBreaker(LongproductId){Entryentry=null;try{entry=SphU.entry("getProduct");returngetProductWithMultiLevel(productId);}catch(BlockExceptione){// 被限流或熔断,返回降级数据log.warn("获取商品被熔断: productId={}",productId);returngetDefaultProduct(productId);}finally{if(entry!=null){entry.exit();}}}/** * 降级:返回默认商品信息 */privateProductgetDefaultProduct(LongproductId){returnProduct.builder().id(productId).name("商品信息加载中").price(BigDecimal.ZERO).status("LOADING").build();}}

三、缓存一致性

3.1 常见方案

缓存一致性方案: 1. Cache Aside(旁路缓存) - 读:先读缓存,miss读DB,写缓存 - 写:先更新DB,再删缓存 - 问题:极端情况下会不一致 2. Write Through(写穿透) - 写:先写缓存,缓存同步写DB - 读:只读缓存 - 问题:写入延迟高 3. Write Behind(写回) - 写:只写缓存,异步写DB - 问题:数据可能丢失 4. 双写 - 写:同时写DB和缓存 - 问题:两个写操作可能一个成功一个失败

3.2 最佳实践

/** * 缓存一致性:延迟双删 */@Service@Slf4jpublicclassCacheConsistencyService{@AutowiredprivateStringRedisTemplateredisTemplate;/** * 延迟双删策略 */@TransactionalpublicvoidupdateProduct(Productproduct){StringcacheKey="product:"+product.getId();// 1. 先删缓存redisTemplate.delete(cacheKey);// 2. 更新数据库productMapper.updateById(product);// 3. 延迟再删一次(防止旧数据被其他线程写入缓存)CompletableFuture.delayedExecutor(500,TimeUnit.MILLISECONDS).execute(()->{redisTemplate.delete(cacheKey);log.info("延迟双删: key={}",cacheKey);});}/** * 基于Canal的缓存更新 */@EventListenerpublicvoidonDataChange(DataChangeEventevent){if("t_product".equals(event.getTable())){StringproductId=event.getAfter().get("id");StringcacheKey="product:"+productId;if(event.getEventType()==CanalEntry.EventType.DELETE){redisTemplate.delete(cacheKey);}else{// 删除缓存,下次读取时重新加载redisTemplate.delete(cacheKey);}log.info("Canal更新缓存: key={}",cacheKey);}}}

四、踩坑实录

坑1:大量Key同时过期

所有商品缓存设置了30分钟过期,整点时全部过期,数据库被打爆。

解决:过期时间加随机值。

坑2:缓存和数据库不一致

先删缓存再更新数据库,但并发时旧数据被写回缓存。

解决:延迟双删,或使用Canal监听Binlog更新缓存。

坑3:热点Key打满Redis

某个热点Key的QPS超过Redis单节点极限。

解决:本地缓存 + 热点Key拆分。

坑4:BigKey

一个List存了100万个元素,操作时阻塞Redis。

解决:拆分成多个小Key,使用Hash结构。

坑5:缓存预热不充分

大促前缓存预热没做好,流量一来数据库直接被打挂。

解决:大促前手动预热热点数据,逐步放量。


五、总结

缓存全链路防护:

问题方案
穿透布隆过滤器 + 空值缓存
击穿互斥锁 + 逻辑过期
雪崩随机过期 + 多级缓存 + 熔断
一致性延迟双删 + Canal

最佳实践:

  1. 永远设置随机过期时间
  2. 热点数据用逻辑过期
  3. 多级缓存保底
  4. 缓存和数据库一致性用Canal
  5. 大促前做好缓存预热

血的教训:

缓存不是万能的,没有缓存是万万不能的。用好了是加速器,用不好是定时炸弹。

思考题:你的系统有没有遇到过缓存问题?怎么解决的?


个人观点,仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/5 18:15:04

小程序毕设项目:基于springboot+微信小程序的钓鱼论坛小程序 (源码+文档,讲解、调试运行,定制等)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围::小程序、SpringBoot、SSM、JSP、Vue、PHP、Java、pyth…

作者头像 李华
网站建设 2026/6/5 18:14:51

Mermaid CLI深度解析:智能化图表生成与自动化文档集成实战指南

Mermaid CLI深度解析&#xff1a;智能化图表生成与自动化文档集成实战指南 【免费下载链接】mermaid-cli Command line tool for the Mermaid library 项目地址: https://gitcode.com/gh_mirrors/me/mermaid-cli Mermaid CLI作为Mermaid库的命令行接口&#xff0c;实现了…

作者头像 李华
网站建设 2026/6/5 18:14:49

ImagePut:AutoHotkey图像处理终极指南 - 高效专业开源库

ImagePut&#xff1a;AutoHotkey图像处理终极指南 - 高效专业开源库 【免费下载链接】ImagePut A core library for images in AutoHotkey. Supports AutoHotkey v1 and v2. 项目地址: https://gitcode.com/gh_mirrors/im/ImagePut ImagePut是AutoHotkey生态中的核心图…

作者头像 李华
网站建设 2026/6/5 18:12:43

如何快速下载macOS完整安装包:终极图形界面解决方案指南

如何快速下载macOS完整安装包&#xff1a;终极图形界面解决方案指南 【免费下载链接】DownloadFullInstaller macOS application written in SwiftUI that downloads installer pkgs for the Install macOS Big Sur application. 项目地址: https://gitcode.com/gh_mirrors/d…

作者头像 李华
网站建设 2026/6/5 18:08:38

采购谈判实战:从“老好人”到专业博弈的成本优化心法

1. 项目背景与核心诉求&#xff1a;当“老好人”采购遇上百万级订单干了这么多年电子行业的采购&#xff0c;我自认算是个“好说话”的主。跟供应商打交道&#xff0c;能体谅的尽量体谅&#xff0c;付款流程上能快绝不拖&#xff0c;样品测试也尽量配合。久而久之&#xff0c;在…

作者头像 李华
网站建设 2026/6/5 18:08:37

HUUC-YOLOv8-MHSA-new完全指南:从数据集到智能教育系统的落地实践

HUUC-YOLOv8-MHSA-new完全指南&#xff1a;从数据集到智能教育系统的落地实践 【免费下载链接】YOLOv8-MHSA-new 该数据集为 **课堂行为分析** 提供核心支撑&#xff1a;通过训练计算机视觉模型&#xff0c;可实时监测学生课堂参与度&#xff0c;辅助教师优化互动策略&#xff…

作者头像 李华