☰
Redis缓存穿透、击穿、雪崩的区别与防护方案(含可运行代码)
2026/10/4 1:07:22 网站建设 项目流程

本文把缓存三大经典问题——穿透、击穿、雪崩——从成因、区别到落地方案讲清楚,附带基于 Spring Boot + Redis 的可运行代码,以及实际项目中踩过的坑。

一、先分清三个概念

很多人把这三者混为一谈,其实它们的触发条件完全不同:

问题请求的数据缓存中数据库中后果
缓存穿透查询根本不存在的数据没有也没有每次都打到数据库
缓存击穿查询某个热点 key恰好过期有瞬间大量请求压向数据库
缓存雪崩查询大量 key集中过期有大面积请求同时落库

一句话记忆:穿透是"查无此物",击穿是"一个热点突然失效",雪崩是"一大批同时失效"。

二、缓存穿透:布隆过滤器 + 空值缓存

2.1 方案一:缓存空值

当数据库查不到数据时,仍然在 Redis 写入一个空标记(设置较短的过期时间),避免恶意请求反复打库。

publicProductgetProduct(Longid){Stringkey="product:"+id;Stringvalue=redisTemplate.opsForValue().get(key);if(value!=null){// 命中空标记if("NULL".equals(value)){returnnull;}returnobjectMapper.readValue(value,Product.class);}Productproduct=productMapper.selectById(id);if(product==null){// 空值缓存,过期 2 分钟,防止长期占用redisTemplate.opsForValue().set(key,"NULL",Duration.ofMinutes(2));returnnull;}redisTemplate.opsForValue().set(key,objectMapper.writeValueAsString(product),Duration.ofMinutes(30));returnproduct;}

2.2 方案二:布隆过滤器

对于 ID 有明确范围的场景(比如商品 ID 一定来自已存在的商品表),在请求到达缓存前,先用布隆过滤器判断 ID 是否"可能存在"。不存在的直接拦截,连 Redis 都不查。

@BeanpublicBloomFilter<Long>productBloomFilter(){// expectedInsertions 预计元素数量,fpp 误判率returnBloomFilter.create(Funnels.longFunnel(),10_000_000L,0.01);}publicvoidaddProductToBloom(Longid){productBloomFilter.put(id);}publicProductqueryWithBloom(Longid){if(!productBloomFilter.mightContain(id)){// 一定不存在,直接返回returnnull;}returngetProduct(id);}

布隆过滤器有误判率:它说"不存在"就一定不存在,说"存在"则可能误判,所以误判的少量请求仍会落库,需要配合空值缓存兜底。

三、缓存击穿:互斥锁 + 逻辑过期

热点 key 失效瞬间,大量并发同时查库。核心思路是只放一个线程去重建缓存,其余等待。

3.1 方案一:互斥锁(简单可靠)

publicProductgetHotProduct(Longid){Stringkey="hot:product:"+id;Stringvalue=redisTemplate.opsForValue().get(key);if(value!=null){returnobjectMapper.readValue(value,Product.class);}StringlockKey="lock:product:"+id;try{// SETNX 抢锁,带过期防止死锁Booleanlocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",Duration.ofSeconds(10));if(Boolean.FALSE.equals(locked)){// 没抢到锁,稍等后重试读缓存Thread.sleep(50);returngetHotProduct(id);}// 双重检查:可能在等锁期间已被其他线程重建value=redisTemplate.opsForValue().get(key);if(value!=null){returnobjectMapper.readValue(value,Product.class);}Productproduct=productMapper.selectById(id);redisTemplate.opsForValue().set(key,objectMapper.writeValueAsString(product),Duration.ofMinutes(30));returnproduct;}finally{redisTemplate.delete(lockKey);}catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewRuntimeException(e);}}

3.2 方案二:逻辑过期(不阻塞,适合高可用)

不设置物理 TTL,而是在 value 中存一个逻辑过期时间。发现逻辑过期后,异步线程去更新,当前请求先返回旧数据。牺牲短暂一致性,换取不阻塞。

classCacheData<T>{privateTdata;privateLocalDateTimeexpireTime;}publicProductgetWithLogicalExpire(Longid){Stringkey="hot:product:"+id;Stringjson=redisTemplate.opsForValue().get(key);CacheData<Product>cache=objectMapper.readValue(json,newTypeReference<CacheData<Product>>(){});if(cache.getExpireTime().isAfter(LocalDateTime.now())){returncache.getData();// 未逻辑过期}// 已过期,尝试异步刷新StringlockKey="lock:product:"+id;if(Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(lockKey,"1",Duration.ofSeconds(10)))){// 提交异步任务重建,本线程立即返回旧数据executor.submit(()->{try{rebuildHotProduct(id);}finally{redisTemplate.delete(lockKey);}});}returncache.getData();}

四、缓存雪崩:打散过期时间 + 多级兜底

大量 key 同一时刻过期,或 Redis 整体宕机,都会造成雪崩。防护从"避免集中过期"和"数据库抗压"两侧入手。

publicvoidsetWithRandomTtl(Stringkey,Stringvalue){// 基础 30 分钟 + 0~10 分钟随机,打散过期时间intrandom=ThreadLocalRandom.current().nextInt(0,600);redisTemplate.opsForValue().set(key,value,Duration.ofSeconds(1800+random));}

配套措施:

  • Redis 高可用:主从 + 哨兵或集群,避免单点故障;
  • 多级缓存:本地缓存(Caffeine)作为一级,Redis 作为二级,Redis 故障时本地缓存仍能挡一部分;
  • 限流降级:数据库侧配置熔断与限流,非核心接口返回兜底数据,保护数据库不被打垮;
  • 预热:大促前提前加载热点数据,错开上线即过期的时间点。

五、实战踩过的三个坑

坑1:空值缓存被当成正常数据反序列化

空值用字符串"NULL"标记,但直接readValue会抛异常。务必在反序列化前先判断是否为空标记;更稳妥的做法是用统一的包装结构,带一个数据是否存在的标志位,而不是靠魔法字符串。

坑2:互斥锁只 SETNX 不设过期,服务重启导致死锁

如果setIfAbsent不带过期时间,拿到锁的线程在重建过程中宕机,锁将永远不释放,热点 key 直接"卡死"。锁必须带超时;释放锁时最好用 Lua 校验 value(存入唯一标识),避免误删别人的锁:

-- 安全释放锁ifredis.call("get",KEYS[1])==ARGV[1]thenreturnredis.call("del",KEYS[1])elsereturn0end

坑3:随机 TTL 只加在部分 key 上,雪崩依旧

团队给部分热点加了随机过期,但批量导入的数据仍统一 TTL,结果导入后一小时集中过期引发雪崩。记住:凡是批量写入的缓存,都要在写入路径统一带随机 TTL,不能只在单个查询方法里处理。

六、方案选型小结

  • 恶意攻击、查不存在数据 →布隆过滤器 + 空值缓存;
  • 单个热点 key 高并发 → 强一致用互斥锁,高可用用逻辑过期;
  • 大面积 key 或 Redis 故障 →随机 TTL + 高可用 + 多级缓存 + 限流降级组合拳。

没有银弹,根据业务对一致性、可用性的要求组合使用。多数电商类系统的实践是:布隆过滤器拦穿透、互斥锁护热点、随机 TTL 防雪崩,再配 Redis 集群和限流兜底,基本可以平稳扛住日常和大促流量。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询