java面试必问21:Redis缓存穿透、击穿、雪崩:从原理到实战,面试官都佩服你
缓存穿透、击穿、雪崩从原理到实战一篇讲透面试官“谈谈缓存穿透、击穿和雪崩以及如何解决”你“穿透是查不存在的数据可以用布隆过滤器或缓存空值击穿是热点 key 过期可以用互斥锁或永不过期雪崩是大量 key 同时过期可以通过随机过期时间、多级缓存、熔断降级来应对。”面试官“那布隆过滤器为什么能避免穿透互斥锁会不会影响性能”你“……”很多人能说出概念和简单方案但一追问底层原理和实际落地细节就含糊了。本文从问题本质出发结合代码示例彻底讲透缓存三大坑的成因与解决方案。一、为什么要用缓存缓存如 Redis置于数据库之前可以大幅提升读性能降低数据库压力。但缓存不是银弹使用不当会引入三个典型问题穿透、击穿、雪崩。理解它们的区别和应对策略是高并发系统设计的必修课。二、缓存穿透1. 定义缓存穿透查询一个根本不存在的数据缓存层和存储层都不会命中。每次请求都会穿透缓存直达数据库如果恶意攻击或大量请求会导致数据库压力剧增甚至崩溃。2. 危害数据库被大量无效查询压垮。攻击者可利用不存在的 ID 进行 DDoS 攻击。3. 解决方案方案一缓存空值NULL 或特殊标记当数据库查询结果为空时也将该 key 对应的空结果缓存起来例如null或空对象并设置较短的过期时间几分钟。优点简单容易实现。缺点占用少量内存短时间内无法区分真实空数据和临时空缓存。// 伪代码publicObjectgetData(Stringkey){Objectcacheredis.get(key);if(cache!null){returncache;// 包括空值缓存}ObjectdbValuedb.query(key);if(dbValuenull){redis.setex(key,60,null);// 缓存空值60秒过期returnnull;}redis.set(key,dbValue);returndbValue;}方案二布隆过滤器Bloom Filter布隆过滤器是一种概率型数据结构可以判断一个元素一定不存在或可能存在。将所有可能存在的数据 key 放入布隆过滤器请求到达时先过过滤器如果过滤器说不存在直接返回不查数据库。如果过滤器说可能存在再查缓存和数据库。优点内存占用极小1 亿数据仅需约 120 MB且能拦截绝大多数穿透。缺点有误判率可能把不存在判为存在且无法删除元素除非使用 Counting Bloom Filter。// 初始化布隆过滤器Guava 示例BloomFilterStringbloomBloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),1000000,// 预计插入数量0.01// 误判率 1%);// 添加所有有效 keybloom.put(userId);// 查询时if(!bloom.mightContain(userId)){returnnull;// 一定不存在}// 继续查缓存和数据库最佳实践结合使用布隆过滤器拦截大部分穿透 缓存空值兜底误判部分。三、缓存击穿1. 定义缓存击穿某个热点 key如秒杀商品、爆款新闻在缓存过期的瞬间有大量并发请求同时访问该 key由于缓存失效所有请求都落到数据库上导致数据库瞬间压力过大。2. 危害数据库瞬时负载极高可能引发慢查询或连接池爆满。如果热点 key 是高频访问的击穿会导致系统响应变慢甚至宕机。3. 解决方案方案一互斥锁Mutex只允许一个线程去查询数据库并重建缓存其他线程等待或快速失败。优点保证数据库只被访问一次。缺点阻塞其他请求增加响应延迟。publicObjectgetHotData(Stringkey){Objectvalueredis.get(key);if(value!null){returnvalue;}// 尝试获取分布式锁如 Redis SET NXStringlockKeylock:key;booleanlockedredis.setnx(lockKey,1,10);// 10秒超时if(locked){try{// 双重检查防止其他线程已经重建了缓存valueredis.get(key);if(valuenull){valuedb.query(key);redis.setex(key,3600,value);}}finally{redis.del(lockKey);}}else{// 未获得锁短暂等待后重试或直接返回空Thread.sleep(50);returngetHotData(key);// 递归重试}returnvalue;}方案二逻辑过期永不过期 异步更新不设置物理过期时间而是在 value 中存入逻辑过期时间。当读取时发现逻辑过期则异步更新缓存同时立即返回旧值。优点不阻塞读请求性能高。缺点实现复杂可能返回短暂脏数据。publicclassCacheData{Objectdata;longexpireTime;}// 异步线程池ExecutorServiceexecutorExecutors.newFixedThreadPool(5);publicObjectgetHotData(Stringkey){CacheDatacacheDataredis.get(key);if(cacheDatanull){returndb.query(key);// 首次加载可加锁}if(cacheData.expireTimeSystem.currentTimeMillis()){returncacheData.data;// 未过期直接返回}// 已过期异步更新executor.submit(()-{ObjectnewDatadb.query(key);CacheDatanewCachenewCacheData(newData,System.currentTimeMillis()3600_000);redis.set(key,newCache);});// 返回旧数据returncacheData.data;}最佳实践对于极高并发热点推荐逻辑过期方案对于一般热点互斥锁足够。四、缓存雪崩1. 定义缓存雪崩由于大量缓存 key 在同一时间段过期或者缓存服务宕机导致所有请求直接打到数据库造成数据库压力骤增甚至崩溃。2. 危害数据库被瞬间压垮引发整个系统雪崩。如果缓存服务本身不可用危害更大。3. 解决方案方案一随机过期时间为缓存设置过期时间时在基准值上加上一个随机偏移量如 1~5 分钟避免大量 key 同时过期。intbaseExpire3600;// 1小时intrandomOffsetnewRandom().nextInt(300);// 0~300秒redis.setex(key,baseExpirerandomOffset,value);方案二多级缓存使用本地缓存Caffeine、Guava 分布式缓存Redis两级架构。即使 Redis 大量 key 过期本地缓存仍能挡住部分请求。注意本地缓存需要合理控制内存和更新策略。方案三熔断、降级、限流熔断当检测到数据库异常如超时率过高直接熔断快速返回错误或降级数据防止雪崩扩大。降级提供兜底数据如默认值、静态页面。限流控制访问数据库的并发量超出阈值则排队或拒绝。例如使用 Hystrix、Sentinel 实现。方案四高可用缓存集群使用 Redis Sentinel 或 Redis Cluster避免单点故障导致整个缓存层不可用。方案五预热与持久化预热在系统低峰期如每天凌晨将热点数据提前加载到缓存。持久化开启 Redis 的 RDB/AOF即使重启也能快速恢复缓存。五、三者对比问题触发原因影响解决方案穿透查询不存在的数据绕过缓存直接打库布隆过滤器、缓存空值击穿热点 key 过期瞬间大量请求击垮数据库互斥锁、逻辑过期永不过期异步雪崩大量 key 同时过期 / 缓存宕机数据库瞬间过载随机过期时间、多级缓存、熔断降级、高可用六、总结与最佳实践穿透最根本的防护是布隆过滤器可拦截绝大多数无效 key。缓存空值作为补充简单有效。击穿对于超高并发热点推荐逻辑过期方案一般热点使用互斥锁即可。雪崩关键在于打散过期时间和保证缓存高可用。随机过期时间是低成本高收益的手段配合熔断限流能构建健壮的防护体系。一句话记住穿透布隆空值挡击穿锁或逻辑扛雪崩随机多级防熔断限流保健康。希望这篇文章能帮你彻底掌握缓存三大坑在面试和实战中从容应对欢迎继续讨论。