前言在高并发项目里我们通常使用「Redis 缓存 MySQL 数据库」架构优先查询 Redis命中直接返回缓存未命中查询数据库再把结果写入缓存减轻数据库压力。 但是这个架构存在三类经典问题缓存穿透、缓存击穿、缓存雪崩。三者都会造成大量请求打到数据库严重时数据库 CPU 打满服务宕机。区分穿透查询不存在的数据缓存和数据库都没有请求直达 DB击穿热点 key 过期大量并发请求同时打到同一条数据直击 DB雪崩大量 key 同一时间过期大量请求同时涌入数据库一、缓存穿透Cache Penetration1. 定义查询数据库根本不存在的数据Redis 缓存里自然也没有这个 key。请求绕过缓存直接访问 MySQL。 如果有人恶意攻击批量传入不存在的 ID会持续压垮数据库。场景举例前端传userId-1、商品 id99999999数据库无此商品。 查询流程Redis 查不到 → 查询 MySQL返回空 → 不会把空结果写入缓存 → 下一次相同请求依旧再次查库。2. 解决方案方案 1缓存空值最简单中小项目首选查询数据库不存在时往 Redis 写入空值设置较短过期时间例如 2 分钟。 后续相同请求直接命中 Redis 空结果不再访问数据库。//伪代码 public Goods getGoodsById(Long id){ String key goods: id; String cacheVal redisTemplate.opsForValue().get(key); if(cacheVal ! null){ if(.equals(cacheVal)){ return null; // 缓存空对象代表数据不存在 } return JSON.parseObject(cacheVal, Goods.class); } //缓存未命中查询数据库 Goods goods goodsMapper.selectById(id); if(goods null){ // 数据库不存在存入空值过期2分钟 redisTemplate.opsForValue().set(key, , 2, TimeUnit.MINUTES); return null; } redisTemplate.opsForValue().set(key, JSON.toJSONString(goods), 30, TimeUnit.MINUTES); return goods; }优点实现简单缺点如果恶意传入大量不同不存在 idRedis 会存入大量无用空 key占用内存。方案 2布隆过滤器BloomFilter高并发、防恶意攻击推荐提前把数据库所有有效 id加载进布隆过滤器。请求进来先过布隆过滤器布隆过滤器判定不存在 → 直接返回不查 Redis、不查 DB布隆过滤器判定存在 → 继续查询 Redis、数据库布隆过滤器特点存在误判不会漏判。说不存在一定不存在说存在有可能不存在。优点内存占用极小适合海量数据抵御恶意攻击缺点有一定误判率新增数据需要同步维护布隆过滤器删除困难。方案 3接口层参数校验在接口最外层做参数校验ID 不能负数、ID 长度校验非法参数直接拦截。作为辅助方案不能单独解决穿透。二、缓存击穿Cache Breakdown1. 定义某个热点 Key超高访问量在 Redis 中过期。大量并发请求同时进来缓存失效全部打到数据库。重点只有一个热点 key 失效不是大批量 key。秒杀爆款商品、首页热门数据就是典型热点 key。场景秒杀商品 keyQPS 几千刚好 key 过期一瞬间上千请求同时绕过缓存访问 MySQL。2. 解决方案方案 1互斥锁分布式锁缓存失效时只放行一个请求去查数据库并更新缓存其余请求等待重试。 一般使用 Redisson 分布式锁。 伪代码public Goods getHotGoods(Long id){ String key goods: id; String val redisTemplate.opsForValue().get(key); if(val ! null){ return JSON.parseObject(val, Goods.class); } // 缓存失效尝试获取分布式锁 String lockKey lock:goods: id; boolean lock redissonClient.getLock(lockKey).tryLock(5,30,TimeUnit.SECONDS); if(lock){ try{ //拿到锁再次检查缓存防止等待期间别的线程已经更新 String val2 redisTemplate.opsForValue().get(key); if(val2 ! null){ return JSON.parseObject(val2, Goods.class); } Goods goods goodsMapper.selectById(id); redisTemplate.opsForValue().set(key, JSON.toJSONString(goods),30,TimeUnit.MINUTES); return goods; }finally { redissonClient.getLock(lockKey).unlock(); } }else{ //没拿到锁短暂休眠后重试 Thread.sleep(50); return getHotGoods(id); } }优点保证数据库不会被并发打穿数据一致性强缺点会有等待接口响应变慢存在死锁风险需要设置锁超时。方案 2热点 key 永不过期生产热点数据最推荐不设置过期时间。后台开启异步线程定时主动更新缓存而不是依靠过期被动触发更新。本质从根源上杜绝 key 过期自然不会击穿。优点没有锁等待性能最好无击穿风险缺点代码复杂度提升存在缓存和数据库短暂不一致窗口。面试区分互斥锁是按需更新永不过期 后台定时主动刷新适合超高 QPS 热点。三、缓存雪崩Cache Avalanche1. 定义大量缓存 key 在同一时间段集体过期大量请求同时无法命中缓存海量流量直接冲击数据库数据库压力瞬间飙升引发服务雪崩甚至级联宕机。和击穿区别击穿是单个热点 key 过期雪崩是大批量 key 同时过期。 另外一种雪崩场景Redis 服务宕机集群断电、主从切换故障所有缓存全部不可用流量全部打 DB。2. 解决方案方案 1过期时间加随机值【最简单优先使用】给 key 设置过期时间时增加随机偏移量避免大量 key 过期时间集中。 例如原本 30 分钟过期改为30分钟 随机1~5分钟。// 原30分钟 // redisTemplate.set(key,value,30,TimeUnit.MINUTES); // 修改后打散过期时间 int random new Random().nextInt(5); redisTemplate.opsForValue().set(key, JSON.toJSONString(goods),30random, TimeUnit.MINUTES);优点一行代码改造低成本解决大批量 key 同时过期问题缺点只能解决「key 同时过期」无法解决 Redis 宕机引发的雪崩。方案 2Redis 集群高可用主从 哨兵 / Redis Cluster部署集群主节点宕机自动故障转移避免单点 Redis 挂掉整体缓存不可用。解决 Redis 实例宕机场景。方案 3多级缓存本地 Caffeine RedisJVM 本地缓存Caffeine作为一级缓存Redis 二级缓存。即使 Redis 全部失效本地缓存还能扛住一部分流量作为兜底。优点双重保护抗风险强缺点本地缓存多实例数据一致性维护成本高。方案 4限流 熔断降级兜底防护使用 Sentinel/Guava 限流。当数据库压力过高触发熔断直接返回兜底数据拒绝新请求。属于最后一道防线防止数据库被打死。四、三大问题对比总结表问题核心原因场景核心方案缓存穿透查询不存在的数据缓存 DB 都没有恶意攻击、非法 ID缓存空值、布隆过滤器、参数校验缓存击穿单个热点 key 过期并发打 DB秒杀爆款分布式互斥锁、热点 key 永不过期异步刷新缓存雪崩大量 key 同时过期 / Redis 宕机批量数据过期Redis 故障过期加随机值、Redis 集群、多级缓存、限流熔断五、生产环境选型建议普通业务缓存空值 过期时间加随机值低成本解决穿透 雪崩热点数据使用永不过期 异步刷新避免击穿。对外高并发、防攻击系统布隆过滤器拦截穿透。核心链路增加 Sentinel 限流熔断兜底Redis 集群保证高可用。不推荐大量业务使用分布式锁锁会降低接口吞吐只在热点 key 场景使用。六、面试合集Q1缓存击穿和雪崩的区别A击穿是单个热点 key 过期雪崩是大量 key 同时失效或者 Redis 整体不可用。Q2布隆过滤器有什么缺点A存在误判无法删除元素新增数据需要同步维护。Q3热点 key 永不过期如何保证缓存和数据库一致性A数据库更新时主动更新 Redis 缓存后台定时任务兜底刷新允许短暂最终一致性。Q4缓存雪崩除了 key 同时过期还有什么场景怎么处理ARedis 集群宕机。解决方案Redis 哨兵 / 集群高可用、多级缓存、限流熔断降级。结语缓存穿透、击穿、雪崩是缓存架构最基础也是最重要的问题。面试很多人只会背概念分不清场景讲不出方案优缺点。实际开发中优先低成本方案不要过度设计高并发核心链路多层防护。理解这三个问题是设计高可用缓存架构的基础。