spring cacheable默认不缓存null值,因框架为防误存无效结果而静默丢弃null;需显式配置unless="#result == null"跳过缓存或改用空字符串等方案实现空值缓存。

Spring Boot默认不缓存null值,直接用@Cacheable写空值会失败——必须显式配置跳过null判断。
为什么@Cacheable不存null?
Spring Cache的@Cacheable默认行为是:如果方法返回null,就跳过缓存写入。这是为了防止业务误把null当有效结果缓存,但恰好和防穿透需求冲突——你就是要缓存“查无此ID”这个事实。
常见错误现象:redis-cli里查不到shop:999999,日志显示反复走DB,说明空值根本没进Redis。
- 除非加
unless = "#result == null",否则@Cacheable对null直接静默丢弃 - 用
cacheManager自定义时,若没重写getMissingCache或覆盖CacheResolver,null仍被拦截 - 某些老版本Spring(如2.1.x)甚至会抛
IllegalArgumentException: Null value not allowed
三种能落地的null缓存写法
别碰@CachePut——它不校验条件,容易把null刷进缓存后永远删不掉;优先选以下可控方式:
- 用
@Cacheable(unless = "#result == null"):最轻量,适合简单场景;注意unless在方法执行后才判断,DB已查一次 - 手动调
stringRedisTemplate.opsForValue().set("cache:null:shop:"+id, "", 3, TimeUnit.MINUTES):值写""而非null,避免Jedis/Lettuce序列化异常 - 自定义
CacheResolver,在resolveCaches里对null返回专用缓存实例(如nullCache),再配独立TTL策略
示例(手动写空值):
if (shop == null) {
stringRedisTemplate.opsForValue().set("cache:null:shop:" + id, "", 3, TimeUnit.MINUTES);
return null;
}
空值缓存的key和TTL必须带约束
空值缓存不是“记住不存在”,而是给恶意扫描一个冷却期。放任不管会撑爆Redis内存。
-
key必须带前缀,如"cache:null:shop:",否则无法用redis-cli --scan --pattern "cache:null:*"批量清理 - TTL严格控制在
2~5分钟,用TimeUnit.MINUTES;设成24小时等于给攻击者建了个永久黑名单 - 值统一用
""(空字符串),不用null或"NULL",避免不同客户端反序列化不一致
布隆过滤器不落地=白做
只靠空值缓存扛不住高频恶意请求——每秒1万次扫号,就算每个空值TTL 3分钟,Redis也要存60万个key。真正的防线在前面:
- Guava
BloomFilter纯内存,服务重启即失效,线上必须用RedisBloom模块或BF.ADD/BF.EXISTS命令操作Redis里的布隆结构 - 新增/下架数据时,必须同步调
BF.ADD,漏一次就漏一批穿透请求 - 查询逻辑必须是:
BF.EXISTS key == 0 → 直接return null,这步要在@Cacheable之前,否则空值缓存都来不及触发
真正难的不是写对set("cache:null:shop:123", ""),而是让布隆过滤器的expectedInsertions按未来3个月增量预估、让lock:shop:123的超时时间比DB查询耗时短30%、让监控能区分出是BF.EXISTS返回0还是空值缓存命中——这些细节没埋点,压测时根本看不出问题。











