@cacheable默认不缓存null,需用unless="#result==null"显式允许空值缓存,并设2~5分钟ttl、加业务前缀,配合redis布隆过滤器与互斥锁协同防御穿透。

@Cacheable默认不缓存null,必须显式干预
Spring Boot 的 @Cacheable 注解默认会跳过 null 返回值,也就是说:方法返回 null 时,缓存不会写入任何内容,下一次请求仍会再次执行方法——这正是缓存穿透的温床。
常见错误现象是日志里反复出现相同 SQL 查询不存在 ID 的记录,数据库 QPS 突增,但 Redis 中却查不到对应 key。
- 必须用
unless = "#result == null"显式允许缓存null,例如:@Cacheable(value = "user", key = "#id", unless = "#result == null") - 或者改用
@CachePut+ 手动判断,在方法内部控制空值写入逻辑 - 注意:
unless表达式在 Spring EL 中对null安全,但不要写成#result.equals(null),会 NPE
空值缓存必须带 TTL 和业务前缀
缓存空结果不是“记住它永远不存在”,而是给恶意或误请求一个冷却窗口。不设 TTL 或设太长(比如 24 小时),Redis 内存会被大量 user:-1、product:9999999 这类无效 key 占满,尤其在 ID 范围宽泛的场景下极易 OOM。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
TTL推荐设为2~5 分钟,用TimeUnit.MINUTES指定单位,避免误写成秒 - key 必须加前缀区分类型,例如
"cache:null:user:" + id,方便后期用redis-cli --scan --pattern "cache:null:user:*"清理 - 值建议存空字符串
""而非null,防止某些 Redis 客户端(如 Jedis)序列化异常
布隆过滤器必须落地 Redis,不能只靠 Guava
Guava 的 BloomFilter 是纯内存结构,JVM 重启即丢,无法用于分布式服务。线上真实场景中,布隆过滤器必须持久化到 Redis(如用 RedisBloom 模块)或专用服务。
- 查询前必须先执行
BF.EXISTS,返回0则直接拦截,不查缓存也不查 DB - 新增/下架数据时,必须同步调用
BF.ADD或BF.MADD,否则过滤器与业务状态脱节 - 误差率
fpp不建议低于0.01;expectedInsertions应按未来 3 个月增量预估,而非当前总量 - 如果用 Redisson,可直接获取
RBloomFilter:redissonClient.getBloomFilter("userFilter")
空值缓存 + 布隆过滤器必须加锁协同
很多人以为加了布隆过滤器就万事大吉,但漏掉了关键并发点:多个线程同时发现 BF.EXISTS == 0,又同时发现缓存 miss,再同时去查 DB——这本质是缓存击穿在穿透场景下的变种。
- 整个
queryWithPassThrough流程必须包裹互斥锁,推荐用SET lock:user:123 NX EX 3原子指令实现 - 锁超时时间必须短于 DB 查询平均耗时(比如查库 P99 是 200ms,锁设 300ms),防止死锁
- 获取锁失败后应退避重试(如
Thread.sleep(10)最多 3 次),而不是立刻查 DB - 空值写入和正常数据写入,都必须在锁内完成,否则仍可能重复穿透
stringRedisTemplate.opsForValue().set(),而是让布隆过滤器跟得上数据变更节奏、让空值 TTL 和锁超时在压测中不互相打架、让监控能一眼看出是漏过了布隆还是绕过了空值缓存——这些细节没埋点、没日志、没压测,上线就踩坑。










