redis缓存预热、缓存击穿、缓存穿透、缓存雪崩
文档
- redis单机安装
- redis集群模式 -集群搭建
- 布隆过滤器 -Bloom Filter
- springboot整合redisson单机模式
- redis实现布隆过滤器
- guava布隆过滤器及cuckoo过滤器
官方文档
说明
- redis版本:7.0.0
- springboot版本:3.2.0
缓存预热
含义
- 系统启动或上线前,提前把热点数据加载进缓存,避免刚上线时大量请求打到 DB。
使用场景
- 平时:主要预热「常驻热点」和「冷启动必用」数据,减轻日常 DB 压力、提升首屏体验
- 活动时:在平时预热基础上,额外、提前预热「本次活动相关」数据,避免活动开始瞬间打垮 DB
- 总结:高并发促销时不是"才做预热",而是"在平时预热的基础上,再针对本次活动多做一层、提前做"。
注意事项
- 每个 key 必须设置合理的过期时间,并在基础 TTL 上加随机偏移,避免大量 key 在同一时刻过期引发缓存雪崩。
- 只预热真正热点(Top N、配置指定的 id 等),不要全表预热,避免占满缓存、拉高 DB 压力。
缓存击穿
形成场景
- 一个热点key
- 大量并发同期请求这个key
- 此时缓存中不存在这个key(例如刚过期或被删除)
- 这些请求会同时去访问数据库,导致缓存击穿
解释说明
- 本身行为是非恶意的,本意是请求一个有数据的key
- 缓存中没有这个key,在数据库中也能查到,这里不强调数据库中没有数据的情况
- 重点在于:缓存中没有时,不应该所有请求都去访问数据库,只需一个请求去访问数据库,并写缓存即可
解决方案
-
双检加锁(互斥锁)
-
思路:只让一个请求去访问数据库,并写缓存
-
示例
javapublic Data get(String key) { // 一检:先查缓存 Data data = cache.get(key); if (data != null) { return data; } // 抢锁(同一 key 互斥) String lockKey = "lock:" + key; if (tryLock(lockKey)) { try { // 二检:拿到锁后再查一次缓存 data = cache.get(key); if (data != null) { return data; } // 查 DB 并写缓存 data = db.get(key); cache.set(key, data); return data; } finally { unlock(lockKey); } } else { // 没抢到锁,短暂等待后重试查缓存 Thread.sleep(50); return cache.get(key); } }
-
-
热点 key 永不过期 / 逻辑过期
-
思路:热点 key 不设 TTL,或设逻辑过期时间,到期后异步刷新,读时仍返回旧值。
-
示例
javapublic Data get(String key) { Data data = cache.get(key); if (data != null) { // 逻辑过期:检查是否过期,过期则异步刷新 if (isExpired(data)) { asyncRefresh(key); // 异步刷新,不阻塞 } return data; // 仍返回旧值,避免击穿 } // 缓存没有才查 DB data = db.get(key); cache.set(key, data, Long.MAX_VALUE); // 永不过期 return data; }
-
-
单飞(SingleFlight)
-
思路:同一 key 的并发请求合并为一次 DB 查询,其他请求等待结果。单机版直接用内存实现,分布式版需要用redis等中间件模拟
-
示例
javaConcurrentHashMap<String, CompletableFuture<Data>> flights = new ConcurrentHashMap<>(); public Data get(String key) { CompletableFuture<Data> future = flights.computeIfAbsent(key, k -> CompletableFuture.supplyAsync(() -> { try { Data data = cache.get(key); if (data != null) return data; data = db.get(key); cache.set(key, data); return data; } finally { flights.remove(key); } }) ); return future.join(); }
-
-
提前续期(异步刷新)
- 思路:在 key 即将过期时,后台异步刷新,避免到期瞬间失效。
-
多级缓存(本地缓存 + Redis)
- 思路:本地缓存(如 Caffeine)作为第一层,Redis 作为第二层,减少打到 Redis/DB 的请求。
缓存穿透
形成场景
- 请求查询一个 key
- 该 key 在数据库中不存在
- 缓存中也不会有这个 key(因为 DB 没有,不会写入缓存)
- 每次请求都会穿透缓存,直接访问数据库
- 如果大量并发请求不存在的 key(如恶意刷接口),会导致数据库压力过大
解释说明
- 行为可能是恶意的,也可能是正常的业务查询(如用户查询不存在的订单号、商品 id)
- 缓存中没有这个 key,数据库中也没有对应的数据
- 重点在于:对于不存在的 key,不应该每次都去访问数据库,应该用布隆过滤器先判断,或缓存"不存在"的结果
解决方案
-
布隆过滤器
-
思路:在查询缓存和数据库之前,先用布隆过滤器判断 key 是否可能存在,如果布隆过滤器返回 false,直接返回"不存在",不查缓存和数据库,如果返回 true,再查缓存和数据库
-
前提:在缓存穿透场景下使用布隆过滤器,需要先把"存在的元素"加载到布隆过滤器,并持续维护,否则会误拦所有请求,起不到"防穿透且放行有效数据"的作用。布隆过滤器需要先"知道"哪些元素存在,才能判断"不存在"。标准布隆过滤器不支持删除,若业务会删数据,需要定期重建布隆过滤器,或改用支持删除的布谷鸟过滤器。
-
示例
javapublic Data get(String key) { // 先用布隆过滤器判断 if (!bloomFilter.mightContain(key)) { return null; // 一定不存在,直接返回 } // 布隆过滤器说可能存在,再查缓存 Data data = cache.get(key); if (data != null) return data; // 查数据库 data = db.get(key); if (data != null) { cache.set(key, data); } return data; }
-
-
缓存空值
- 思路:对于查询结果为"不存在"的情况,也写入缓存(如缓存 null 或占位值),设置较短的过期时间(如 5 分钟),避免占用过多内存
-
参数校验
- 思路:在入口处校验请求参数,过滤明显不合法的请求(如负数 id、超长字符串)
缓存雪崩
形成场景
- 大量 key 在同一时刻或短时间内同时失效(例如设置了相同或接近的过期时间)
- Redis 节点/集群宕机、网络不可用,导致整块缓存不可用
- 此时大量请求发现缓存没有,同时去访问数据库
- 数据库瞬时压力剧增,甚至宕机,引发雪崩式故障
解释说明
- 行为通常不是恶意,而是设计或运维不当(如批量 key 同 TTL、Redis 故障)
- 与击穿不同:击穿是「一个热点 key」失效;雪崩是「大量 key 同时失效」或「整块缓存不可用」
- 重点在于:避免大量 key 同时过期,以及保证缓存服务高可用,防止瞬时流量全部打到数据库
解决方案
-
过期时间加随机值
-
思路:避免所有 key 在同一时刻过期,在基础 TTL 上加随机偏移,把过期时间打散
-
示例
javaint baseTtl = 3600; // 1 小时 int randomTtl = baseTtl + ThreadLocalRandom.current().nextInt(0, 300); // 加 0~300 秒随机 cache.set(key, value, randomTtl, TimeUnit.SECONDS);
-
-
多级缓存
-
思路:本地缓存(如 Caffeine)+ Redis,Redis 不可用时仍有本地缓存兜底
-
示例
javapublic Data get(String key) { Data data = localCache.get(key); if (data != null) return data; data = redis.get(key); if (data != null) { localCache.put(key, data); return data; } data = db.get(key); if (data != null) { redis.set(key, data); localCache.put(key, data); } return data; } -
Redis 高可用
- 思路:使用 Redis 哨兵或集群,主节点宕机时自动故障转移,减少因单点故障导致整块缓存不可用
-
限流与熔断
- 思路:对访问数据库的请求做限流,防止瞬时打满 DB,当 DB 或 Redis 异常时熔断,快速失败,避免雪崩扩散
-
缓存击穿 / 穿透 / 雪崩 对比
-
缓存击穿 / 穿透 / 雪崩 对比表(简要)
维度 缓存击穿 缓存穿透 缓存雪崩 key 数量 1 个热点 key 可能很多 key(每次 1 个),都不存在 大量 key 同时失效,或整块缓存挂掉 是否恶意 一般 非恶意(正常高并发访问热门数据) 可能正常(查不存在),也可能 恶意刷不存在的 key 多为配置/运维问题,通常 非恶意 缓存是否有数据 此刻 缓存没有 该热点 key 缓存没有该 key(不会有) 大量 key 没缓存 / Redis 不可用 DB 是否有数据 有(DB 能查到) 没有(DB 本来就没这条) 多数 key 有(DB 能查到) 现象 同一热点 key 的大量请求在失效瞬间同时打 DB 每次请求都穿透缓存直查 DB,且都是"查不到"的请求 大量本应走缓存的请求在短时间内一起打到 DB,DB 瞬时压力剧增或宕机 核心解决方案 双检 + 互斥锁、单飞、热点 key 不过期/逻辑过期、提前续期、多级缓存 布隆/布谷鸟过滤器、缓存空值(NULL 占位)、参数校验/黑名单 TTL 加随机偏移、Redis 高可用、多级缓存、限流/熔断、缓存预热、错峰过期
参考资料
注意事项
- 部分内容由AI生成
- 如有不对,欢迎指正!!!