【大白话说Java面试题 第174题】【07_Redis篇】第10题:什么是缓存击穿、缓存穿透、缓存雪崩?对应的解决方案有哪些?

发布时间:2026/7/14 23:56:22
【大白话说Java面试题 第174题】【07_Redis篇】第10题:什么是缓存击穿、缓存穿透、缓存雪崩?对应的解决方案有哪些? 第10题什么是缓存击穿、缓存穿透、缓存雪崩对应的解决方案有哪些回答核心考点缓存三大问题击穿、穿透、雪崩是 Redis 面试的高频考点但大厂面试不会只问定义和解决方案而是深入考察三种问题的本质区别单 Key vs 多 Key vs 不存在 Key、互斥锁与逻辑过期的选型差异、布隆过滤器的数学原理与误判率计算、Redis 集群高可用架构Sentinel vs Cluster以及生产环境中如何组合多种方案构建防御体系。面试官真正想判断的是你是否能从架构层面设计一套完整的缓存防护策略而非背诵标准答案。1. 三种问题的本质区别问题触发条件影响范围根本原因危害等级缓存击穿单个热点 Key 过期单个 Key高并发 热点 Key 同时失效 高缓存穿透查询不存在的数据任意 Key恶意攻击或业务逻辑缺陷 高缓存雪崩大量 Key 同时失效 / Redis 宕机大量 Key批量过期或集群故障 致命关键区分击穿 有数据但缓存刚好过期并发重建穿透 无数据请求直达数据库如 ID -1雪崩 多个 Key 同时失效或 Redis 不可用。2. 缓存击穿Hot Key Expiration2.1 问题根因热点 Key如秒杀商品、热门文章缓存过期瞬间大量并发请求同时到达。由于缓存已失效所有请求直达数据库形成流量尖峰。请求1 ──→ Redismiss──→ DB ──→ 重建缓存 请求2 ──→ Redismiss──→ DB ──→ 重建缓存 请求3 ──→ Redismiss──→ DB ──→ 重建缓存 ...N个请求同时打到DB2.2 解决方案一互斥锁分布式锁核心思路只允许一个线程重建缓存其他线程等待或返回旧值。publicDatagetData(Stringkey){// 1. 查询缓存Datadata(Data)redisTemplate.opsForValue().get(key);if(data!null)returndata;// 2. 获取分布式锁StringlockKeylock:key;RLocklockredissonClient.getLock(lockKey);try{// 尝试获取锁最多等待 100msbooleanisLockedlock.tryLock(100,10,TimeUnit.MILLISECONDS);if(!isLocked){// 获取锁失败短暂休眠后重试或返回降级数据Thread.sleep(50);returngetData(key);// 递归重试}// 3. 双重检查防止锁等待期间其他线程已重建data(Data)redisTemplate.opsForValue().get(key);if(data!null)returndata;// 4. 查询数据库并重建缓存datadataMapper.selectByKey(key);if(data!null){redisTemplate.opsForValue().set(key,data,30,TimeUnit.MINUTES);}returndata;}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}}优点保证只有一个线程重建缓存数据库压力可控。缺点其他线程需要等待或重试响应时间增加分布式锁本身有性能开销如果重建缓存耗时很长如 5 秒等待线程会堆积。2.3 解决方案二逻辑过期时间永不过期 异步刷新核心思路缓存不设 TTL或设极长 TTL在业务数据中维护一个逻辑过期时间字段。查询时发现逻辑过期则返回旧数据并异步重建缓存。DatapublicclassCacheDataT{privateTdata;// 实际数据privatelongexpireTime;// 逻辑过期时间毫秒时间戳}publicDatagetData(Stringkey){CacheDataDatacacheData(CacheDataData)redisTemplate.opsForValue().get(key);if(cacheDatanull){// 缓存不存在查询数据库并写入首次加载DatadatadataMapper.selectByKey(key);cacheDatanewCacheData(data,System.currentTimeMillis()30*60*1000);redisTemplate.opsForValue().set(key,cacheData);returndata;}// 缓存存在检查逻辑过期时间if(cacheData.getExpireTime()System.currentTimeMillis()){// 未过期直接返回returncacheData.getData();}// 已过期返回旧数据异步重建DatadatacacheData.getData();// 异步重建线程池或 MQcacheRebuildExecutor.submit(()-{// 获取锁防止并发重建if(redisTemplate.opsForValue().setIfAbsent(lock:rebuild:key,1,10,TimeUnit.SECONDS)){try{DatanewDatadataMapper.selectByKey(key);CacheDataDatanewCachenewCacheData(newData,System.currentTimeMillis()30*60*1000);redisTemplate.opsForValue().set(key,newCache);}finally{redisTemplate.delete(lock:rebuild:key);}}});returndata;// 返回旧数据保证可用性}优点热点 Key 永不过期不存在击穿问题逻辑过期后异步重建用户无感知延迟无需分布式锁性能更高。缺点需要修改数据结构增加expireTime字段逻辑过期期间返回旧数据数据一致性降低首次加载缓存为空时仍需查询数据库。适用场景热点数据如秒杀商品、首页推荐、对一致性要求不极端严格的读多写少场景。2.4 两种方案对比维度互斥锁逻辑过期一致性高重建完成后返回最新数据中逻辑过期期间返回旧数据性能低线程等待/重试高无等待直接返回实现复杂度低中需修改数据结构和异步逻辑适用场景一致性要求高的场景热点数据、可用性优先的场景3. 缓存穿透Cache Penetration3.1 问题根因请求的数据在缓存和数据库中都不存在如 ID -1、非法参数。由于缓存不会存储不存在的结果每次请求都直达数据库。恶意攻击者可能利用此漏洞发起大量请求压垮数据库。请求 ID-1 ──→ Redismiss──→ DB无数据──→ 不缓存 ──→ 返回空 请求 ID-1 ──→ Redismiss──→ DB无数据──→ 不缓存 ──→ 返回空 ...无限循环DB 压力持续3.2 解决方案一缓存空对象Null Object核心思路查询数据库为空时将空值缓存起来如空字符串、空对象并设置较短的过期时间。publicDatagetData(Stringkey){// 1. 查询缓存ObjectcacheValueredisTemplate.opsForValue().get(key);if(cacheValue!null){if(EMPTY.equals(cacheValue)){returnnull;// 空对象直接返回}return(Data)cacheValue;}// 2. 查询数据库DatadatadataMapper.selectByKey(key);if(data!null){redisTemplate.opsForValue().set(key,data,30,TimeUnit.MINUTES);}else{// 3. 缓存空对象过期时间较短如 5 分钟redisTemplate.opsForValue().set(key,EMPTY,5,TimeUnit.MINUTES);}returndata;}优点实现简单有效防止重复查询不存在的数据。缺点额外内存开销大量不存在的 Key 被缓存占用 Redis 内存数据不一致窗口如果数据库后续插入了该数据缓存中的空对象在过期前会阻止查询可配合延迟双删解决。优化设置空对象的过期时间较短如 1~5 分钟并限制空对象的缓存数量如 LRU 淘汰。3.3 解决方案二布隆过滤器Bloom Filter核心思路在缓存之前加一层布隆过滤器快速判断数据是否可能存在。如果布隆过滤器判断不存在直接返回不查询缓存和数据库。请求 ──→ 布隆过滤器 ──→ 判断可能存在 ──→ 查询缓存 ──→ 查询数据库 ↓ 判断肯定不存在 ──→ 直接返回空布隆过滤器原理初始化一个长度为 m 的位数组所有位设为 0使用 k 个哈希函数将元素映射到位数组的 k 个位置将这些位置设为 1查询时用同样的 k 个哈希函数计算位置如果所有位置都是 1则可能存在如果有任意位置是 0则肯定不存在。误判率计算误判率 p ≈ (1 - e^(-kn/m))^k 其中 - m位数组长度 - n已插入元素数量 - k哈希函数数量 最优哈希函数数量k (m/n) * ln(2) 最优位数组长度m -n * ln(p) / (ln(2))^2示例期望存储 100 万个元素误判率 1%m ≈ 100万 * 1.44 * log2(1/0.01) ≈1.44 MBk ≈ 0.7 * (m/n) ≈7 个哈希函数// Guava 布隆过滤器示例BloomFilterStringbloomFilterBloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),1000000,// 预期插入数量0.01// 误判率 1%);// 初始化加载所有有效 KeyListStringallKeysdataMapper.selectAllKeys();allKeys.forEach(bloomFilter::put);publicDatagetData(Stringkey){// 1. 布隆过滤器判断if(!bloomFilter.mightContain(key)){returnnull;// 肯定不存在直接返回}// 2. 查询缓存Datadata(Data)redisTemplate.opsForValue().get(key);if(data!null)returndata;// 3. 查询数据库布隆过滤器判断可能存在但实际可能不存在datadataMapper.selectByKey(key);if(data!null){redisTemplate.opsForValue().set(key,data,30,TimeUnit.MINUTES);}returndata;}优点内存占用极小百万级数据仅需 1~2 MB查询时间复杂度 O(k)极快不存在空对象缓存的内存膨胀问题。缺点存在误判率判断可能存在时实际可能不存在需查询数据库兜底不支持删除传统布隆过滤器无法删除元素可用 Counting Bloom Filter 或 Cuckoo Filter 改进初始化成本高需要预加载所有有效 Key数据新增时需同步更新布隆过滤器。适用场景数据量固定或增长缓慢、查询频繁、内存敏感的场景如用户 ID 校验、商品 ID 校验。3.4 两种方案对比维度缓存空对象布隆过滤器内存占用高每个空 Key 都占内存低固定大小位数组查询性能高Redis 查询极高内存位运算实现复杂度低中需预加载和维护支持删除✅❌传统布隆过滤器误判率0存在可控制适用场景空 Key 数量少数据量大、内存敏感4. 缓存雪崩Cache Avalanche4.1 问题根因批量过期大量 Key 设置了相同的过期时间同时失效Redis 宕机Redis 集群故障所有缓存不可用。Key1 过期 ──→ DB 查询 Key2 过期 ──→ DB 查询 Key3 过期 ──→ DB 查询 ...大量 Key 同时失效DB 瞬间被压垮4.2 解决方案一随机化过期时间核心思路在基础过期时间上增加随机偏移量分散 Key 的失效时间。// 基础过期时间 30 分钟随机增加 0~10 分钟intbaseTTL30*60;intrandomTTLThreadLocalRandom.current().nextInt(0,10*60);redisTemplate.opsForValue().set(key,data,baseTTLrandomTTL,TimeUnit.SECONDS);效果原本同时失效的 1000 个 Key分散在 10 分钟内逐步失效数据库压力平滑。4.3 解决方案二Redis 高可用集群架构原理故障恢复适用场景主从 Sentinel一主多从Sentinel 监控主节点故障并自动切换秒级中小规模读写分离Redis Cluster数据分片到 16384 个 Slot多主多从自动故障转移大规模数据量大Codis代理层分片对客户端透明手动或自动已有系统改造Redis Cluster 架构客户端 │ ▼ [Redis Cluster] ┌─────┬─────┬─────┐ │ M1 │ M2 │ M3 │ ← 主节点各负责 5461 个 Slot │ S1 │ S2 │ S3 │ ← 从节点复制主节点 └─────┴─────┴─────┘关键配置cluster-require-full-coverage no部分 Slot 不可用时其他 Slot 继续服务cluster-node-timeout 15000节点故障判定时间15 秒。4.4 解决方案三多级缓存 降级策略请求 ──→ 本地缓存CaffeineTTL1分钟──→ 命中返回 ↓ 未命中 Redis 缓存TTL30分钟──→ 命中返回 ↓ 未命中 数据库 ──→ 返回数据并重建缓存降级策略Redis 宕机时本地缓存继续提供服务可能数据较旧数据库压力过大时返回降级数据如静态页面、默认数据熔断机制数据库 QPS 超过阈值时触发熔断直接返回降级数据。4.5 解决方案四预热与异步加载// 缓存预热系统启动时加载热点数据PostConstructpublicvoidwarmupCache(){ListDatahotDatadataMapper.selectHotData();hotData.forEach(data-{intttl30*60ThreadLocalRandom.current().nextInt(0,10*60);redisTemplate.opsForValue().set(data:data.getId(),data,ttl,TimeUnit.SECONDS);});}5. 综合防御体系设计生产环境中单一方案无法应对所有场景需要组合多种方案构建分层防御体系┌─────────────────────────────────────────────────────────────┐ │ 第一层布隆过滤器 / 参数校验 │ │ → 拦截非法请求防止穿透 │ ├─────────────────────────────────────────────────────────────┤ │ 第二层本地缓存Caffeine │ │ → 降级兜底Redis 宕机时仍可用 │ ├─────────────────────────────────────────────────────────────┤ │ 第三层Redis 缓存 │ │ → 随机过期时间防止雪崩 │ │ → 热点 Key 逻辑过期 / 互斥锁防止击穿 │ ├─────────────────────────────────────────────────────────────┤ │ 第四层数据库 │ │ → 连接池限流 熔断降级 │ └─────────────────────────────────────────────────────────────┘6. 生产环境避坑指南6.1 互斥锁的等待时间不宜过长如果重建缓存耗时 5 秒等待线程过多会导致线程池耗尽。建议锁等待时间设为 100ms 以内获取锁失败时返回降级数据或短暂休眠后重试监控锁竞争频率过高时告警。6.2 空对象缓存的过期时间要短空对象设置 5 分钟过期如果攻击者构造大量不存在的 KeyRedis 内存会被迅速占满。建议空对象过期时间设为 1~2 分钟使用 Redis 的maxmemory-policy allkeys-lru自动淘汰对空对象 Key 做数量监控突增时触发告警。6.3 布隆过滤器的初始化与更新布隆过滤器需要预加载所有有效 Key如果数据量很大如亿级初始化耗时较长。建议系统启动时异步加载数据新增时通过 MQ 同步更新布隆过滤器定期重建布隆过滤器如每天凌晨防止误判率累积。6.4 Redis Cluster 的脑裂问题网络分区时可能出现多个主节点同时提供服务导致数据不一致。建议配置min-slaves-to-write 1和min-slaves-max-lag 10确保至少有一个从节点同步使用 Sentinel 或 Cluster 的自动故障转移而非手动切换。6.5 监控与告警缓存命中率低于阈值如 80%时告警数据库 QPS 突增时告警缓存失效信号Redis 内存使用率超过 80% 时告警热点 Key 访问频率监控识别潜在击穿风险。7. 面试官追问与高分回答模板追问 1“什么是缓存击穿、穿透、雪崩有什么区别”低分回答“击穿是热点 Key 过期穿透是查询不存在的数据雪崩是大量 Key 同时失效。”没有触及本质区别高分回答三种问题的本质区别在于触发条件和影响范围缓存击穿单个热点 Key过期高并发请求同时重建。根本原因是’高并发 热点 Key 同时失效’影响范围有限但瞬时压力大。缓存穿透查询不存在的数据如 ID-1缓存和数据库都 miss。根本原因是’恶意攻击或业务逻辑缺陷’影响范围取决于攻击强度。缓存雪崩大量 Key 同时失效或 Redis 宕机。根本原因是’批量过期时间相同’或’集群故障’影响范围最大可能导致数据库整体崩溃。用一句话区分击穿是’有数据但刚好过期’穿透是’根本没有数据’雪崩是’太多数据同时过期’。追问 2“缓存击穿用互斥锁还是逻辑过期怎么选”高分回答选择取决于业务对一致性和可用性的要求互斥锁保证重建完成后返回最新数据一致性高。但其他线程需要等待或重试响应时间增加适合一致性要求高的场景如库存查询。逻辑过期热点 Key 永不过期逻辑过期后返回旧数据并异步重建。用户无感知延迟性能更高但逻辑过期期间数据不一致。适合热点数据如秒杀商品、首页推荐、可用性优先的场景。实际工程中逻辑过期更常用因为热点数据的短暂不一致通常可接受而互斥锁的等待在高并发下会成为瓶颈。追问 3“布隆过滤器的误判率怎么计算如何降低”高分回答布隆过滤器的误判率公式为p ≈ (1 - e(-kn/m))k其中 m 是位数组长度n 是已插入元素数k 是哈希函数数量。降低误判率的方法增加位数组长度 mm 越大每个元素占用的位数越多冲突概率越低。但内存占用增加。增加哈希函数数量 k最优 k (m/n) * ln(2)。但 k 过大时位数组很快被填满反而增加误判率。定期重建随着元素增多误判率逐渐上升定期重建布隆过滤器可恢复初始误判率。示例存储 100 万元素误判率 1%需要 m ≈ 1.44 MBk ≈ 7。追问 4“缓存雪崩时如果 Redis 集群也宕了怎么办”高分回答Redis 集群宕机时的防御策略分三层本地缓存兜底使用 Caffeine/Guava 本地缓存即使 Redis 不可用本地缓存仍能提供服务数据可能较旧但保证可用性。熔断降级数据库 QPS 超过阈值时触发熔断如 Hystrix/Sentinel直接返回降级数据静态页面、默认数据保护数据库不被压垮。快速恢复Redis Cluster 的自动故障转移秒级配合 Sentinel 监控和告警运维人员快速介入修复。关键原则任何缓存系统都必须假设缓存会失效设计兜底方案。本地缓存 熔断降级是 Redis 宕机时的最后防线。追问 5“如果攻击者知道你的空对象缓存策略故意构造大量随机 Key 攻击怎么办”高分回答这是缓存穿透的高级攻击场景需要多层防御布隆过滤器前置在缓存空对象之前加布隆过滤器随机 Key 大概率被拦截不查询数据库。限流熔断对同一 IP 或同一用户的请求频率做限流如每秒 10 次超过阈值直接拒绝。空对象缓存 LRU 淘汰即使少量随机 Key 穿透到数据库空对象缓存后设置极短 TTL30 秒并配合 Redis 的allkeys-lru策略自动淘汰防止内存占满。IP 黑名单识别异常请求模式将恶意 IP 加入黑名单。参数校验业务层对请求参数做合法性校验如 ID 必须是正整数拦截明显非法的请求。组合使用以上方案可将穿透攻击的影响降到最低。追问 6“Redis Cluster 和 Sentinel 有什么区别雪崩场景下怎么选”高分回答Redis Sentinel 和 Cluster 的核心区别在于数据分片Sentinel主从复制 哨兵监控数据不分片。一主多从Sentinel 监控主节点故障并自动切换。适合数据量小 10GB、读多写少的场景。缺点是主节点单点写入无法水平扩展。Cluster数据分片到 16384 个 Slot多主多从。支持水平扩展数据量可达 TB 级。每个主节点负责部分 Slot客户端通过 CRC16 计算 Key 对应的 Slot 和节点。适合大规模、数据量大的场景。雪崩场景下Cluster 更优多主节点分散写入压力单节点故障只影响部分 Slot自动故障转移从节点晋升为主节点无需人工介入支持cluster-require-full-coverage no部分 Slot 不可用时其他 Slot 继续服务。Sentinel 适合中小规模Cluster 适合大规模分布式系统。8. 方案选型速查表问题推荐方案备选方案不适用场景缓存击穿逻辑过期 异步重建互斥锁分布式锁一致性要求极高的实时数据缓存穿透布隆过滤器 参数校验缓存空对象短 TTL数据频繁新增/删除缓存雪崩随机过期时间 Cluster多级缓存 熔断降级无Redis 宕机本地缓存 熔断降级Sentinel 自动切换无热点 Key逻辑过期 本地缓存互斥锁无恶意攻击限流 黑名单 布隆过滤器空对象缓存无面试官想要的满分总结缓存三大问题的防御不是选哪个方案而是构建分层防御体系。缓存击穿的本质是高并发 热点 Key 同时失效逻辑过期是首选热点 Key 永不过期异步重建互斥锁作为备选。缓存穿透的本质是查询不存在的数据布隆过滤器前置拦截是最佳方案内存占用极小配合参数校验和限流。缓存雪崩的本质是批量失效或集群故障随机过期时间分散压力Redis Cluster保证高可用本地缓存 熔断降级作为最后防线。生产环境必须记住任何缓存系统都要假设缓存会失效。本地缓存兜底、熔断降级保护、监控告警及时发现三者缺一不可。布隆过滤器的误判率、互斥锁的等待时间、空对象的过期时间都需要根据实际业务压测和监控动态调整。最后缓存问题的根本解决往往不在技术层面而在产品层面合理设置缓存粒度、避免大 Key、控制过期时间分布、引导用户合理查询------很多时候最好的防御是让问题根本不要发生。觉得对您有帮助麻烦点点关注啦您的关注是我创作的最大动力~