缓存穿透、击穿、雪崩解决方案大全:L2Cache的NullValue空值缓存实战指南

缓存穿透、击穿、雪崩解决方案大全:L2Cache的NullValue空值缓存实战指南

【免费下载链接】l2cache L2Cache 是一个基于 Caffeine + Redis 的二级缓存框架。让缓存的使用在业务开发中更加简单、高效。 【免费下载链接】l2cache 项目地址: https://gitcode.com/gh_mirrors/l2/l2cache

做缓存的人,谁没被「缓存穿透、缓存击穿、缓存雪崩」这三个词折磨过?L2Cache 是一个基于 Caffeine + Redis 的二级缓存框架,它用一套 NullValue 空值缓存机制,把缓存穿透这类高并发难题变成了「开箱即用」的能力。本文带你快速搞懂:三大缓存故障的成因、L2Cache 的 NullValue 空值缓存是怎么实现的、有哪些关键配置参数,以及一个真实的线上踩坑案例。

L2Cache二级缓存架构:Caffeine本地缓存与Redis分布式缓存协同,多节点通过Redis发布订阅同步一致性

一、缓存三大故障,一张表看懂

先对齐概念,避免踩坑时「雾里看花」:

故障类型一句话定义典型后果常规解决思路
🕳️ 缓存穿透查的数据根本不存在,缓存和 DB 都没有每次请求都打到 DB,可被恶意利用空值缓存、布隆过滤器
💥 缓存击穿某个热点 key 过期瞬间,海量请求同时穿过缓存单 key 高并发击穿 DB互斥锁、异步加载、本地缓存异步加载
🏔️ 缓存雪崩大量 key 同时失效,请求集体涌向 DBDB 负载爆表,系统级连锁故障过期时间加随机量、缓存高可用、限流降级

三者经常被混淆:穿透查的是「不存在的 key」,击穿打的是「单个热点 key」,雪崩是「大面积 key 集体失效」。

二、L2Cache 的 NullValue 空值缓存原理 🕳️

对付缓存穿透,最经典的方案就是给不存在的 key 缓存一个空值:下次同样的请求进来,直接命中空值、返回 null,再也不用穿透到 DB。

L2Cache 把这件事做成了自动化能力,核心流程分三步:

  1. 请求到达:先查一级缓存(Caffeine),未命中再查二级缓存(Redis);
  2. 回源加载:两级都没有时,调用业务方法去 DB 加载,如果加载结果是 null,且配置项 allowNullValues=true(默认开启),就会往一级、二级缓存中写入一个特殊的 NullValue 空值对象,而不是 null 本身(Redis 等存储不支持 null 值);
  3. 再次命中:后续相同 key 的请求直接命中 NullValue,由 NullValueUtil 将其解析回 null 返回给业务,请求在缓存层就被拦截,完全不会打到 DB。

关键源码位置(想深挖原理的同学可以看这里):

  • 空值对象定义:l2cache-core/src/main/java/com/github/jesse/l2cache/content/NullValue.java(可序列化的单例,用 equals 兼容 null 判断)
  • 空值存取转换:l2cache-core/src/main/java/com/github/jesse/l2cache/util/NullValueUtil.java
  • 加载时写入空值:l2cache-core/src/main/java/com/github/jesse/l2cache/load/LoadFunction.java
  • 空值清理任务:l2cache-core/src/main/java/com/github/jesse/l2cache/schedule/NullValueCacheClearTask.java

三、NullValue 四大关键配置参数 🔧

只缓存空值、永不清理,会埋下两个新雷:空值把真实数据「假死」掉、海量空值撑爆内存。L2Cache 在 L2CacheConfig.java 中提供了四个参数(均在 CacheConfig 内),帮你把风险控制在可接受范围:

参数默认值作用
allowNullValuestrue是否允许缓存空值,防止缓存穿透的总开关
nullValueExpireTimeSeconds60 秒空值条目的短 TTL,给真实数据「复活窗口」,数据入库后最多 1 分钟即可被查到
nullValueMaxSize5000空值条目数量上限,防止恶意请求灌空 key 导致 OOM(对应文档中的内存溢出实战问题)
nullValueClearPeriodSeconds10 秒定时清理任务执行频率,触发过期淘汰与基于大小的淘汰

背后的机制:每个缓存名额外维护一个专门的 nullValueCache,空值条目在其中登记;NullValueCacheClearTask 按清理频率定时执行 cleanUp(),把过期、超量的空值条目清掉。也就是说,空值缓存是「有限期、有上限」的,既挡住了穿透,又不会长期占坑。

💡 使用建议:如果业务侧「数据先查、后入库」的窗口期较长(例如商品预热上架),可适当调大 nullValueExpireTimeSeconds,否则用户可能在数据真正入库后仍短暂查到 null。

四、击穿与雪崩:L2Cache 又是如何应对的?💥🏔️

空值缓存只解决穿透,击穿和雪崩同样有解,L2Cache 的设计天然兼顾:

  • 防击穿 —— Caffeine 异步加载:一级缓存命中加载逻辑时,同一 key 的并发请求只有一个线程真正加载,其余线程等待结果,热点 key 失效瞬间不会把 DB 打爆;配合 refreshAfterWrite 异步刷新,热点数据可以在过期前被后台悄悄更新,用户无感知(详见 Caffeine简介.md);
  • 防雪崩 —— 双层保险:Caffeine 默认异步机制防止同 key/不同 key 被同时击穿;即使 Redis 层大面积故障,本地 Caffeine 一级缓存仍能兜底一部分请求(有损但可用),配合 Sentinel 等限流熔断保护下游(详见 关于缓存的几个常见问题分析和处理方案.md)。

五、一个真实的线上坑:NullValue 被「误缓存」 ⚠️

理论讲完,看一个项目里的真实实战:8 点开售的会场商品集体不显示。排查发现,某请求在 Redis 缓存已过期、且 valueLoader 为 null 的场景下,把 NullValue 错误地写进了本地缓存和 Redis,导致真实数据被空值挡住。

根因是 refreshAfterWrite 模式下,getIfPresent 获取已过期缓存会触发异步加载——而多数人对它的预期是「只读不加载」。最终修复是在加载链路中增加判断:集群环境下 valueLoader 和 value 都为 null 时直接返回 null,不再缓存 NullValue。

这个案例的价值在于:空值缓存不是「配上就完事」,理解触发链路(尤其是 refreshAfterWrite 的加载语义)才能规避误伤。完整分析见 实战-l2cache中通过getIfPresent()获取已过期缓存导致被设置为NullValue的问题分析.md获取已过期缓存导致被设置为NullValue的问题分析.md),内存溢出场景则可参考 实战-l2cache-caffeine之内存溢出分析.md。

六、落地清单:三步开启空值缓存 ✅

  1. 确认默认配置:allowNullValues 默认为 true,大多数场景无需任何额外配置,穿透防护自动生效;
  2. 按业务调参:数据「先查后建」的场景调大 nullValueExpireTimeSeconds;高防场景可下调 nullValueMaxSize;
  3. 观察日志:关注 NullValueCache put 与 Clean NullValue 两类日志,确认空值写入与清理节奏符合预期。

一句话总结:用 NullValue 空值缓存挡住穿透,用 Caffeine 异步加载扛住击穿,用双层缓存 + 限流降级防住雪崩——L2Cache 把这套组合拳封装成了 Caffeine + Redis 二级缓存的默认能力,你只需要把业务接口交给它,剩下的交给框架。

【免费下载链接】l2cache L2Cache 是一个基于 Caffeine + Redis 的二级缓存框架。让缓存的使用在业务开发中更加简单、高效。 【免费下载链接】l2cache 项目地址: https://gitcode.com/gh_mirrors/l2/l2cache

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值