Redis缓存击穿/穿透/雪崩:三种场景解决方案

张开发
2026/4/19 17:26:34 15 分钟阅读

分享文章

Redis缓存击穿/穿透/雪崩:三种场景解决方案
Redis缓存击穿/穿透/雪崩从原理到落地的完整解决方案在高并发业务场景中Redis作为高性能缓存组件是缓解数据库压力、提升系统响应速度的核心依赖。但随着流量规模增长缓存层可能出现击穿、穿透、雪崩三类典型故障直接导致数据库过载、服务不可用甚至引发全链路雪崩。本文将从原理、场景、解决方案三个维度系统讲解这三类问题的应对策略结合实战代码与对比分析帮助开发者构建高可用缓存架构。一、背景与问题缓存架构的核心目标是让请求尽可能在缓存层处理但在实际生产环境中以下三类问题会直接打破这个平衡当热点缓存失效时大量请求直接冲击数据库引发缓存击穿攻击者或异常流量持续请求不存在的缓存键绕过缓存直接访问数据库引发缓存穿透大规模缓存集中失效导致所有请求瞬间涌入数据库引发缓存雪崩这三类问题的本质都是缓存失效后的流量过载但触发原因、影响范围和应对策略存在显著差异。如果没有针对性的防护机制单靠增加数据库机器配置无法从根本上解决问题甚至会因为流量突增导致数据库连接池耗尽、锁竞争加剧最终引发服务雪崩。二、原理分析1. 缓存击穿热点Key的单点失效是什么某个热点缓存键如电商大促的爆款商品ID过期或被主动删除后大量并发请求直接访问数据库短时间内形成流量峰值。为什么会发生热点Key的访问量远高于普通Key缓存失效后没有足够的缓冲机制所有请求直接穿透到数据库。怎么工作的正常流程是请求→缓存→返回结果当热点Key失效后流程变为请求→缓存失效→所有请求同时访问数据库→数据库过载。优缺点影响范围集中在单个热点Key对应的业务但如果热点Key的QPS达到数万级单数据库实例会瞬间被打垮进而影响依赖该数据库的所有服务。2. 缓存穿透不存在的Key持续攻击是什么请求的缓存键在缓存和数据库中都不存在所有请求直接绕过缓存访问数据库导致数据库持续承受无意义的流量压力。为什么会发生可能是业务逻辑漏洞如用户输入非法参数、爬虫爬取不存在的资源或者是攻击者故意构造不存在的Key发起DDoS攻击。怎么工作的正常流程中缓存会拦截大部分请求但当Key不存在时缓存无法命中请求直接进入数据库且由于数据库也没有对应数据无法回写缓存导致后续相同请求依然会穿透到数据库。优缺点影响范围是整个数据库的所有业务持续的穿透流量会占用数据库连接池资源导致正常请求无法获取连接最终引发服务不可用。3. 缓存雪崩大规模缓存集中失效是什么大量缓存键在同一时间点过期或者缓存集群整体故障如Redis主从切换、网络分区导致所有请求瞬间涌入数据库引发数据库雪崩。为什么会发生常见原因包括缓存过期时间设置过于集中如所有Key都设置为24小时过期、缓存集群单点故障、批量更新缓存时的误操作。怎么工作的正常情况下缓存能处理90%以上的请求当大规模缓存失效后100%的请求都会进入数据库数据库的处理能力远低于缓存会瞬间被压垮进而导致依赖该数据库的所有服务不可用形成全链路雪崩。优缺点影响范围是整个系统的所有业务一旦发生会导致服务大面积瘫痪恢复时间长是缓存架构中最严重的故障场景。三、实现步骤与实战代码1. 缓存击穿的解决方案分布式锁热点Key永不过期针对缓存击穿核心思路是让热点Key失效后只有一个请求去更新缓存其他请求等待缓存更新完成常用方案是基于Redis的分布式锁或者直接将热点Key设置为永不过期通过后台异步更新。实战代码Redisson分布式锁实现缓存击穿防护importorg.redisson.api.RLock;importorg.redisson.api.RedissonClient;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Service;importjavax.annotation.Resource;importjava.util.concurrent.TimeUnit;ServicepublicclassProductService{ResourceprivateStringRedisTemplatestringRedisTemplate;ResourceprivateRedissonClientredissonClient;ResourceprivateProductDBMapperproductDBMapper;// 缓存键前缀privatestaticfinalStringCACHE_KEY_PREFIXproduct:info:;// 分布式锁键前缀privatestaticfinalStringLOCK_KEY_PREFIXlock:product:info:;// 缓存过期时间非热点KeyprivatestaticfinallongCACHE_EXPIRE_TIME30;// 锁过期时间防止死锁privatestaticfinallongLOCK_EXPIRE_TIME10;publicProductInfogetProductInfo(LongproductId){StringcacheKeyCACHE_KEY_PREFIXproductId;// 1. 先从缓存获取数据StringproductJsonstringRedisTemplate.opsForValue().get(cacheKey);if(productJson!null){returnJsonUtils.parseObject(productJson,ProductInfo.class);}StringlockKeyLOCK_KEY_PREFIXproductId;RLocklockredissonClient.getLock(lockKey);try{// 2. 尝试获取分布式锁只有一个请求能获取到if(lock.tryLock(0,LOCK_EXPIRE_TIME,TimeUnit.SECONDS)){try{// 3. 再次检查缓存防止其他请求已经更新了缓存productJsonstringRedisTemplate.opsForValue().get(cacheKey);if(productJson!null){returnJsonUtils.parseObject(productJson,ProductInfo.class);}// 4. 从数据库获取数据ProductInfoproductInfoproductDBMapper.selectById(productId);if(productInfo!null){// 5. 将数据写入缓存热点Key可设置为永不过期通过后台任务更新if(isHotProduct(productId)){stringRedisTemplate.opsForValue().set(cacheKey,JsonUtils.toJsonString(productInfo));}else{stringRedisTemplate.opsForValue().set(cacheKey,JsonUtils.toJsonString(productInfo),CACHE_EXPIRE_TIME,TimeUnit.MINUTES);}}returnproductInfo;}finally{// 6. 释放锁if(lock.isHeldByCurrentThread()){lock.unlock();}}}else{// 7. 其他请求等待一段时间后重试或者返回默认值TimeUnit.MILLISECONDS.sleep(50);returngetProductInfo(productId);}}catch(InterruptedExceptione){Thread.currentThread().interrupt();returnnull;}}// 判断是否为热点Key可根据访问量、业务规则动态判断privatebooleanisHotProduct(LongproductId){// 示例假设productId为1001的是爆款商品returnproductId.equals(1001L);}}代码说明双重检查缓存获取锁后再次检查缓存防止其他请求已经更新了缓存分布式锁自动过期设置锁的过期时间防止线程异常退出导致死锁热点Key特殊处理将热点Key设置为永不过期通过后台定时任务异步更新缓存避免热点Key过期引发击穿重试机制未获取到锁的请求等待一段时间后重试避免直接返回错误2. 缓存穿透的解决方案空值缓存布隆过滤器针对缓存穿透核心思路是让不存在的Key也能被缓存拦截常用方案有空值缓存和布隆过滤器两者可以结合使用。实战代码空值缓存布隆过滤器的双重防护importcom.google.common.hash.BloomFilter;importcom.google.common.hash.Funnels;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Service;importjavax.annotation.PostConstruct;importjavax.annotation.Resource;importjava.nio.charset.StandardCharsets;importjava.util.List;importjava.util.concurrent.TimeUnit;ServicepublicclassUserService{ResourceprivateStringRedisTemplatestringRedisTemplate;ResourceprivateUserDBMapperuserDBMapper;// 缓存键前缀privatestaticfinalStringCACHE_KEY_PREFIXuser:info:;// 空值缓存过期时间避免占用过多缓存空间privatestaticfinallongNULL_CACHE_EXPIRE_TIME5;// 布隆过滤器预计存储100万条数据误判率0.01%privateBloomFilterbloomFilter;PostConstructpublicvoidinitBloomFilter(){// 1. 从数据库加载所有存在的用户ID初始化布隆过滤器ListuserIdListuserDBMapper.selectAllUserId();longexpectedInsertionsuserIdList.size();doublefpp0.0001;// 误判率0.01%bloomFilterBloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8),expectedInsertions,fpp);for(LonguserId:userIdList){bloomFilter.put(userId.toString());}}publicUserInfogetUserInfo(LonguserId){StringcacheKeyCACHE_KEY_PREFIXuserId;// 1. 先通过布隆过滤器判断Key是否存在不存在直接返回nullif(!bloomFilter.mightContain(userId.toString())){returnnull;}// 2. 从缓存获取数据StringuserJsonstringRedisTemplate.opsForValue().get(cacheKey);if(userJson!null){// 3. 如果是空值标记直接返回nullif(NULL.equals(userJson)){returnnull;}returnJsonUtils.parseObject(userJson,UserInfo.class);}// 4. 从数据库获取数据UserInfouserInfouserDBMapper.selectById(userId);if(userInfo!null){// 5. 正常数据写入缓存设置正常过期时间stringRedisTemplate.opsForValue().set(cacheKey,JsonUtils.toJsonString(userInfo),30,TimeUnit.MINUTES);}else{// 6. 空值写入缓存设置较短的过期时间stringRedisTemplate.opsForValue().set(cacheKey,NULL,NULL_CACHE_EXPIRE_TIME,TimeUnit.MINUTES);}returnuserInfo;}}代码说明布隆过滤器前置拦截在请求进入缓存前先通过布隆过滤器判断Key是否可能存在不存在直接返回避免后续请求穿透到数据库空值缓存将不存在的Key对应的空值写入缓存设置较短的过期时间避免相同请求再次穿透到数据库布隆过滤器初始化从数据库加载所有存在的Key初始化布隆过滤器后续可以通过异步任务定期更新误判率控制通过调整布隆过滤器的预计插入量和误判率参数将误判率控制在可接受范围内如0.01%3. 缓存雪崩的解决方案过期时间打散缓存集群高可用针对缓存雪崩核心思路是避免大规模缓存同时失效和缓存集群故障时的降级处理常用方案包括过期时间打散、缓存集群高可用、多级缓存架构。实战代码过期时间打散Redis哨兵模式的高可用配置importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Service;importjavax.annotation.Resource;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.ThreadLocalRandom;ServicepublicclassOrderService{ResourceprivateStringRedisTemplatestringRedisTemplate;ResourceprivateOrderDBMapperorderDBMapper;// 缓存键前缀privatestaticfinalStringCACHE_KEY_PREFIXorder:info:;// 基础过期时间30分钟privatestaticfinallongBASE_EXPIRE_TIME30;// 随机偏移时间0-10分钟用于打散过期时间privatestaticfinallongRANDOM_EXPIRE_RANGE10;publicOrderInfogetOrderInfo(LongorderId){StringcacheKeyCACHE_KEY_PREFIXorderId;// 1. 从缓存获取数据StringorderJsonstringRedisTemplate.opsForValue().get(cacheKey);if(orderJson!null){returnJsonUtils.parseObject(orderJson,OrderInfo.class);}// 2. 从数据库获取数据OrderInfoorderInfoorderDBMapper.selectById(orderId);if(orderInfo!null){// 3. 计算随机过期时间打散缓存过期时间longexpireTimeBASE_EXPIRE_TIMEThreadLocalRandom.current().nextLong(RANDOM_EXPIRE_RANGE);stringRedisTemplate.opsForValue().set(cacheKey,JsonUtils.toJsonString(orderInfo),expireTime,TimeUnit.MINUTES);}returnorderInfo;}}Redis哨兵模式配置application.ymlspring:redis:sentinel:master:mymasternodes:192.168.1.100:26379,192.168.1.101:26379,192.168.1.102:26379password:redis123database:0代码说明过期时间打散通过在基础过期时间上添加随机偏移量避免大量缓存键在同一时间点过期缓存集群高可用使用Redis哨兵模式当主节点故障时哨兵会自动将从节点提升为主节点保证缓存集群的可用性多级缓存降级可以结合本地缓存如Caffeine当Redis集群故障时请求先从本地缓存获取数据避免直接冲击数据库四、对比与优化三类缓存问题的解决方案对比维度缓存击穿缓存穿透缓存雪崩核心目标控制热点Key失效后的流量拦截不存在的Key请求避免大规模缓存同时失效常用方案分布式锁、热点Key永不过期空值缓存、布隆过滤器过期时间打散、缓存集群高可用实现复杂度中等需要处理分布式锁的边界情况中等布隆过滤器需要初始化和更新较高需要缓存集群架构支持资源消耗低只针对热点Key中布隆过滤器占用内存空值缓存占用缓存空间高缓存集群需要多节点部署误判风险无布隆过滤器存在误判可通过参数控制无适用场景热点Key场景如大促爆款商品非法请求、攻击流量场景大规模缓存集群场景优化建议缓存击穿优化对于热点Key优先使用永不过期后台异步更新的方案避免分布式锁的竞争开销可以结合本地缓存如Caffeine在JVM层面再做一层缓存进一步减少Redis的访问压力缓存穿透优化布隆过滤器需要定期更新避免数据库数据变化后导致误判可以结合接口限流、参数校验从入口处拦截非法请求缓存雪崩优化多级缓存架构本地缓存→Redis缓存→数据库当Redis集群故障时本地缓存可以作为降级方案缓存预热在大促等流量高峰前提前将热点数据加载到缓存中避免流量高峰时缓存未命中五、总结核心要点缓存击穿针对热点Key通过分布式锁控制缓存更新的并发量或直接将热点Key设置为永不过期缓存穿透通过空值缓存拦截重复的无效请求通过布隆过滤器前置拦截不存在的Key缓存雪崩通过过期时间打散避免大规模缓存同时失效通过缓存集群高可用保证缓存服务的可靠性多级缓存架构结合本地缓存和分布式缓存进一步提升系统的可用性和性能实践建议监控先行通过Redis监控工具如Redis Insight、PrometheusGrafana实时监控缓存命中率、过期Key数量、请求量等指标提前发现潜在问题边界测试在压测阶段模拟缓存击穿、穿透、雪崩场景验证防护机制的有效性降级预案制定缓存集群故障时的降级方案如限流、返回默认值、切换到只读数据库等动态调整根据业务流量变化动态调整缓存过期时间、布隆过滤器参数、热点Key的判断规则保证缓存架构的灵活性

更多文章