Spring Boot 3 默认不缓存null值,需显式配置cacheNullValues=true防穿透;空值TTL建议30–60秒;须配合布隆过滤器与参数校验构建三层防线。

Spring Boot 3 默认不开启 cacheNullValues,必须显式配置
Spring Boot 3 的 @Cacheable 注解默认忽略 null 返回值,不会缓存空结果——这恰恰是缓存穿透的温床。如果你没手动打开 cacheNullValues = true,哪怕业务里返回了 null,Redis 里也压根不会存任何东西,下一次请求照样穿透到 DB。
实操建议:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 所有可能查不到数据的
@Cacheable方法,必须显式写上cacheNullValues = true,例如:@Cacheable(value = "user", key = "#userId", cacheNullValues = true)
- 全局统一控制?可在
CacheConfig或CacheManagerBean 中设置默认行为,但不如方法级显式来得安全、可追溯 - 注意:开启后,
null会被序列化为字符串"null"(取决于你用的RedisCacheConfiguration序列化器),不是 Redis 的NULL类型,也不等价于未设置 key
cacheNullValues = true 后,过期时间怎么设才不伤业务
缓存空值不是“一劳永逸”,设太长(比如 1 小时)会导致真实数据插入后,用户仍看到“查无此 ID”长达几十分钟;设太短(比如 1 秒)又起不到防穿透作用,还徒增 Redis 写压力。
实操建议:
- 空值缓存时间建议控制在
30–60秒之间,够拦住突发扫描或重试,又不至于掩盖真实数据上线 - 不要依赖
@Cacheable的timeToLive统一设——它对 null 和非 null 值一视同仁;更稳妥的是:用RedisTemplate手动 set,并单独控制空值 TTL - 如果用了 Spring Cache + 自定义
RedisCacheConfiguration,可通过entryTtl配置默认过期,但依然推荐在空值写入时显式指定TimeUnit.SECONDS
只开 cacheNullValues 不够,还得配合布隆过滤器兜底
单纯缓存 null 值能挡掉重复查同一个无效 ID 的请求,但防不住“全量遍历式攻击”,比如用 -1、999999999、UUID 格式乱码狂刷接口。这时 Redis 和 DB 都会收到海量无效查询,缓存 null 只是把压力从 DB 搬到了 Redis。
实操建议:
- 在 Controller 层或网关层加一层布隆过滤器(如
guava BloomFilter或 RedisBloom 模块),预判 ID 是否“可能存在”;不存在直接400拦截,根本不到缓存逻辑 - 布隆过滤器的误判率建议控制在
0.01(即 1%),容量按业务最大合法 ID 范围预估,别用运行时动态扩容——重建代价太高 - 注意:布隆过滤器本身不能删元素,所以对软删除/逻辑删除场景要额外设计标记位,否则删了的数据还会被判定为“存在”
空值缓存 + 布隆过滤器 + 参数校验,三者缺一不可
见过太多项目只做其中一环:开了 cacheNullValues 却没控 TTL,结果缓存了一堆永远不更新的 "null";上了布隆过滤器却没校验 ID 格式,-1 和空字符串照进缓存;参数校验写了但放在 service 层,拦截不了高频穿透流量。
关键点就三条:
- ID 类参数必须在
@Controller入口做基础校验,如userId <= 0、长度超限、非法字符,直接return或抛IllegalArgumentException - 布隆过滤器加载时机要早——应用启动时初始化,或通过定时任务同步 DB 全量有效 ID,别等第一次请求才建
- 所有缓存空值操作,必须配套监控:比如用 Prometheus 抓取
redis_key_miss_count{key=~"user:.*"}和redis_null_cache_hit指标,发现某 key 空命中率突增 5 倍,就得查是不是被扫了

















