缓存穿透、击穿、雪崩:解决方案全解析 📅 2026/7/1 4:26:07 1、缓存击穿1.1 什么是缓存穿透缓存穿透是指查询一个不存在的数据导致请求直接穿透缓存层直接访问数据库。由于数据库中也不存在该数据因此每次请求都会绕过缓存直接访问数据库从而导致数据库压力过大。1.2 缓存穿透的原因恶意攻击攻击者故意请求大量不存在的数据。业务逻辑问题业务代码未对请求参数进行校验导致非法请求直接访问数据库排查特征Redis 监控keys *不存在这些查询 key命中率极低。数据库 CPU、QPS 突然暴涨。大量查询返回空值前端频繁查不存在的数据比如 id-1、非法编号。查看命中率CMD 客户端执行redisinfo stats找到这两行keyspace_hits:命中次数 keyspace_misses:未命中次数命中率 hits/(hitsmisses) 命中率长期低于 80%大概率出现穿透。1.3 缓存穿透的解决方案缓存空值// 1. 缓存穿透缓存空值 public String getItemAntiPenetration(String id) { String key item: id; // 1. 查 Redis String value redisTemplate.opsForValue().get(key); if (value ! null) { if (NULL.equals(value)) return 商品不存在来自缓存; return value; } // 2. 查数据库模拟 String dbValue DataBases.queryDB(id); if (dbValue ! null) { redisTemplate.opsForValue().set(key, dbValue, 30, TimeUnit.MINUTES); return dbValue; } else { // 3. 关键缓存空值过期时间短1分钟 redisTemplate.opsForValue().set(key, NULL, 1, TimeUnit.MINUTES); return 商品不存在来自DB已缓存空值; } }浏览器访问http://localhost:8080/demo/penetration?id999不存在第一次返回“来自DB已缓存空值”看redis-cli里出现了SET item:999 NULL EX 60再刷新浏览器返回“来自缓存”Redis 里只有GET没有SET说明空值缓存挡住了后续无效请求布隆过滤器拦截非法 key浏览器访问http://localhost:8080/demo/bloom?id1返回商品详情。 在 ARDM 里能看到item:1进入了 Redis。访问http://localhost:8080/demo/bloom?id99999一个不存在的ID。 返回“商品不存在被布隆拦截”根本不会在 Redis 中产生任何 key更不会访问数据库。可以故意注释掉布隆过滤的判断再访问同一个非法ID发现 Redis 里出现了item:99999值为NULL。虽然也能防住但还是消耗了 Redis 内存和一次网络查询。“布隆过滤器有一定误判率可能把不存在的ID说成可能存在。但不用担心后面还有 Redis 空值缓存兜底数据库绝对安全。”有了布隆过滤器后店门口多了个智能门禁布隆过滤器它存了一份“所有合法菜名的指纹库”。客人喊“红烧砖头”门禁一扫“指纹库里没你的记录请回吧”直接拒之门外。客人喊“红烧肉”门禁一扫“有记录请进”然后才去服务员那儿查菜单。2、缓存击穿2.1 什么是缓存击穿缓存击穿是指某个热点数据在缓存中过期同时有大量并发请求访问该数据导致所有请求直接访问数据库从而导致数据库压力激增。2.2 缓存击穿的原因热点数据过期某个热点数据的缓存过期。高并发请求大量并发请求同时访问该热点数据。排查特征平时系统平稳一旦某个热门 key 过期数据库瞬间被打满。Redis 只是少了某一条热点数据不是大面积 key 失效。监控看到瞬时数据库请求尖峰Redis 缺失单个热门 key。检查 key 过期( -2 代表过期)ttl 你的热点key2.3 解决方案Service public class Demo02Service { Autowired private StringRedisTemplate redisTemplate; Autowired private RedissonClient redissonClient; // 2. 缓存击穿互斥锁双重检查 public String getHotItemAntiBreakdown(String id) { String key hot:item: id; String lockKey lock:item: id; // 1. 查缓存 String val redisTemplate.opsForValue().get(key); if (val ! null) return val; // 2. 抢锁 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 3. 双重检查 val redisTemplate.opsForValue().get(key); if (val ! null) return val; // 故意睡 10 秒模拟重建很慢 try { Thread.sleep(10_000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 4. 查库重建 String dbVal DataBases.queryDB(id); if (dbVal ! null) { // 防雪崩过期时间加随机值 long ttl 30 ThreadLocalRandom.current().nextInt(10); redisTemplate.opsForValue().set(key, dbVal, ttl, TimeUnit.MINUTES); } else { redisTemplate.opsForValue().set(key, NULL, 1, TimeUnit.MINUTES); } return dbVal; } finally { redisTemplate.delete(lockKey); // 释放锁 } } else { // 5. 没抢到锁等一会儿重试 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getHotItemAntiBreakdown(id); // 递归重试 } } }互斥锁准备工作先在浏览器访问http://localhost:8080/demo/breakdown?id1存在的商品让缓存建立。模拟热点Key过期在 ARDM 的 Key 列表里找到hot:item:1右键点击 「Delete Key」。立刻1秒内连续刷新两次浏览器或者用两个浏览器标签页同时打开该地址。快速刷新 ARDM按F5你会看到 Key 列表出现瞬时的lock:item:1几秒后消失。同时hot:item:1又重新出现了Value 是数据库里的数据。注意如果刷新不够快看不到锁可以这样删除hot:item:1后在 ARDM 里按F5观察同时去浏览器发起请求返回后再马上刷新 ARDM锁会闪现一下。热点 key 永不过期// 2 key 永不过期模拟热点数据 // 模拟数据库查询 private String queryDBHot(String id) { // 实际查 MySQL return 最新数据 id - System.currentTimeMillis(); } // 初始化热点 key永不过期 public void initHotKey(String id) { String key hot:never: id; String data queryDBHot(id); redisTemplate.opsForValue().set(key, data); // 不设过期时间 } // 读取热点 key没有过期问题直接返回 public String getHotKey(String id) { String key hot:never: id; String value redisTemplate.opsForValue().get(key); if (value ! null) { return value; } // 如果缓存里真没有比如 Redis 重启主动加载一次 String dbData queryDBHot(id); redisTemplate.opsForValue().set(key, dbData); return dbData; } // 后台刷新任务实际用 Scheduled 定时执行 Scheduled(fixedDelay 5, timeUnit TimeUnit.MINUTES) public void refreshHotKeys() { // 定期从数据库拉取最新数据直接覆盖缓存 ListString hotIds Arrays.asList(1, 2, 3); for (String id : hotIds) { String key hot:never: id; String freshData queryDBHot(id); redisTemplate.opsForValue().set(key, freshData); } }爆款菜牌子永不撤下后台悄悄换菜。物理上无 TTL靠后台任务刷新初始化热点 key 浏览器访问http://localhost:8080/demo/preload?id1假设这是预加载接口调用initHotKey或preloadHotKey的永不过期版本。 → 返回“预加载成功”。查看 ARDM 按F5找到hot:never:1。 重点看 TTL 列显示-1表示永不过期。多次查询 多次访问http://localhost:8080/demo/hot-never?id1每次都返回数据TTL 永远不变。模拟后台刷新 在你的 Java 代码中可以手动调用refreshHotKeys()方法或者等定时任务执行。 刷新后再次查询数据时间戳变了但 TTL 依旧是-1。逻辑过期牌子永远挂着但标注“新鲜度”过期就后台换新客人先拿旧的物理 TTL 可设很长或不过期靠 value 内的时间戳判断预加载逻辑过期 key 访问http://localhost:8080/demo/preload?id1调用preloadHotKey的逻辑过期版本。 → 返回“预加载成功”。查看 ARDM 找到hot:logical:1双击查看 Value。 它是 JSON{data:新鲜数据...,expireTime:1687872000000}一个未来时间戳。正常查询 访问http://localhost:8080/demo/logical?id1返回数据正常。手动模拟逻辑过期 在 ARDM 中双击hot:logical:1将expireTime改成一个过去的时间戳比如1000点保存。立即再次查询 浏览器访问http://localhost:8080/demo/logical?id1。 → 返回的仍然是旧数据逻辑过期拿旧值。 同时在项目控制台或 ARDM 里你能看到lock:logical:1闪现如果异步线程已开启几秒后hot:logical:1的 value 被更新为新的数据和未来时间戳。再次查询 等待几秒再次访问返回的已经是新数据。缓存雪崩3.1 什么是缓存雪崩缓存雪崩是指大量缓存数据在同一时间失效导致大量请求直接访问数据库从而导致数据库压力激增甚至崩溃。3.2 缓存雪崩的原因缓存集中失效缓存中的数据设置了相同的过期时间。Redis 实例宕机Redis 服务不可用导致所有缓存失效。热点数据失效某些热点数据的缓存失效导致大量请求直接访问数据库。排查特征大量 key 同时到期Redis 大量 miss。数据库压力瞬间拉满甚至宕机。查看过期 key发现过期时间高度集中。查看过期总量redisinfo stats查看expired_keys单位时间内过期的key总量短时间 expired_keys 暴涨就是雪崩前兆。3.3 解决方案给过期时间加上随机值打散失效时间。访问三个不同 ID浏览器依次访问http://localhost:8080/demo/avalanche?id1http://localhost:8080/demo/avalanche?id2http://localhost:8080/demo/avalanche?id3观察 ARDM 的 Key 列表按F5刷新你会看到av:item:1、av:item:2、av:item:3三个 key。重点看 TTL 列它们的 TTL 数值完全不一样比如一个是1789一个是1821一个是1910单位秒。对比说明“如果我没加随机值这三个 TTL 会基本一致集体在同一时刻过期数据库瞬间被压垮。现在我让过期时间互相错开压力就分散了。”限流与降级限流后厨门口拉根绳一次只放 5 个服务员进去。其他人排队或者直接给张优惠券打发走。降级后厨彻底崩溃时服务员不再问后厨直接给客人端上一盘“今日特供小菜”兜底数据并道歉“不好意思招牌菜做不了送您一道凉菜吧”。限流效果把rateLimiter的许可数设得很低如每秒 1 个然后用浏览器疯狂刷新部分请求会返回“降级兜底”。限流效果把限流器的许可调得很低 在LimitDegradeService中将RateLimiter.create(5.0)改为RateLimiter.create(0.5)每秒只放 0.5 个请求即每 2 秒一个。快速连续访问 浏览器连续多次快速访问http://localhost:8080/demo/limit-degrade?id999不存在 id以触发查询数据库逻辑。观察结果 部分请求返回降级兜底默认商品信息部分请求正常返回null空值缓存。这说明限流器生效了多余的请求被降级。降级效果可以在queryDB里故意throw new RuntimeException()观察降级数据返回。B. 降级效果模拟数据库异常在queryDB方法里故意throw new RuntimeException(数据库挂了)。再次访问http://localhost:8080/demo/limit-degrade?id1。所有请求都返回降级兜底默认商品信息不会把异常抛给用户。多级缓存Redis 是第一道防线本地缓存是第二道防线。就像餐厅门口有个大招牌菜单Redis每个服务员兜里还有一份小抄本地缓存 Caffeine。就算招牌被风吹走了服务员掏出小抄也能顶一阵。第一次查询填充多级缓存 访问http://localhost:8080/demo/multi-level?id1。 → 返回来自数据库...。 此时数据已同时写入 Redis 和本地 Caffeine 缓存。第二次查询命中本地缓存 再次访问同一个 URL。 → 返回来自本地缓存...响应极快不查 Redis。模拟 Redis 宕机方法一直接停掉 Redis 服务终端CtrlC。方法二在 ARDM 里右键选择hot:item:1删除也能模拟 Redis 部分失效。再次查询 访问http://localhost:8080/demo/multi-level?id1。 → 仍然返回来自本地缓存...服务正常 注意由于 Redis 不可用我们的代码会跳过 Redis 直接查本地但本地已有数据所以能正常返回。等本地缓存过期后 可以配置本地缓存只保留 1 分钟等过期后再请求这次会去 Redis 取如果 Redis 已恢复或者降级。缓存穿透、缓存击穿与缓存雪崩的区别特性缓存穿透缓存击穿缓存雪崩定义查询不存在的数据导致请求直接访问数据库热点数据缓存失效导致大量请求直接访问数据库大量缓存数据在同一时间失效导致请求直接访问数据库原因恶意攻击或业务逻辑问题热点数据过期或高并发请求缓存集中失效或 Redis 实例宕机影响数据库压力过大数据库压力激增数据库压力激增甚至崩溃解决方案缓存空对象、布隆过滤器、参数校验互斥锁、永不过期 后台更新、缓存预热设置随机过期时间、多级缓存、限流与降级最佳实践合理设置缓存过期时间避免缓存集中失效。使用布隆过滤器有效防止缓存穿透。多级缓存架构提高系统的容错能力。限流与降级机制保护数据库不被压垮。监控与报警实时监控缓存命中率和数据库负载及时发现并解决问题。总结缓存穿透、缓存击穿和缓存雪崩是 Redis 使用过程中常见的问题它们会导致数据库压力过大甚至系统崩溃。通过合理的设计和优化可以有效避免这些问题缓存穿透通过缓存空对象、布隆过滤器和参数校验来解决。缓存击穿通过互斥锁、永不过期 后台更新和缓存预热来解决。缓存雪崩通过设置随机过期时间、多级缓存和限流降级来解决。补充布隆过滤器一、空间问题布隆过滤器占的内存很小数据量大了肯定占空间。但布隆过滤器的核心优势就是用极小的空间换取极高的拦截率。存储方式单个ID占用总内存占用直接存 Redis Set约 50 字节简单估算约 5 GB布隆过滤器误判率 0.03不到 1 字节约 140 MB5 GB 对比 140 MB内存占用减少了 97% 以上。为什么能这么省布隆过滤器不存原始数据它存的是一张巨大的 位数组一串二进制位非 0 即 1。二、添加和删除操作这是布隆过滤器的“软肋”添加新数据很简单当数据库新增一条记录如新商品上架直接往布隆过滤器里加它的ID即可。代码示例// 新增商品逻辑public void addNewItem(String id) {//1.存入数据库saveToDatabase(id);//2.同步加入布隆过滤器bloomFilter.put(item: id); System.out.println(新商品 id 指纹已录入门禁); }比喻后厨出了一道新菜去门禁那儿用几个印章在指纹册上盖上印子。以后这道菜就可以被正常点单了。删除数据没法完美删除这是布隆过滤器最大的局限不支持删除。为什么不能删因为不同的ID可能会在布隆过滤器中共享部分“指纹位”。比喻门禁的指纹册上第17号格子的墨迹可能是“红烧肉”和“糖醋鱼”共同盖上去的。 现在“红烧肉”下架了你想把它擦掉。如果你强行把第17号格子擦干净那“糖醋鱼”的指纹也被你毁了一部分下次有人点“糖醋鱼”门禁可能会错误地把他拦在门外。那数据删了怎么办实际项目中有几种折中方案方案1定期重建最简单最常用比如每天凌晨业务低峰期清空布隆过滤器然后从数据库全量重新加载所有现存的ID。代价重建期间有短暂的风险窗口且消耗一定数据库资源。方案2使用支持删除的变种如Counting Bloom Filter每个“指纹位”不只是一个比特而是一个计数器。加一个ID计数器1删一个ID计数器-1。计数器归零这个指纹才算真正擦除。代价内存占用是标准布隆过滤器的数倍实现复杂生产中用得不多。方案3维护一个小规模的黑名单对少数已删除的热点key维护一个Redis Set记录它们。查询时布隆说“可能存在”再去查这个黑名单。如果在黑名单里说明是已删除的直接拒绝。适用删除操作极少且只对热点数据有强一致性要求的场景。修改数据本质是“先删后加”如果只是商品信息变了但ID没变布隆过滤器不需要任何操作。因为它的任务只是判断key存不存在不关心value。如果是ID都变了如item:old_123变成item:new_456那就是删除old_123添加new_456删除操作会面临上面说的问题。