空值缓存该存,但须存固定字符串“NULL”、设1–5分钟短TTL、读取时三态判断(null/“NULL”/正常值),并确保查库与写空值原子性,避免穿透。

空值缓存该不该存?先看穿透怎么发生的
Redis 本身不拒绝 null 或空字符串,但业务层如果对查不到的数据直接 SET key "" 或 SET key "null",反而会埋坑。空值缓存不是“存不存”的问题,而是“存什么、存多久、怎么识别”的问题。关键在于:数据库查无结果 ≠ 应该缓存一个“假空值”。真正要防的是恶意或异常请求反复打穿 Redis 直击 DB,比如用不存在的 user_id=999999999 暴力刷接口。
用 SET key "NULL" EX 60 是最简方案,但得加识别逻辑
空值不能和正常数据混着存,否则下游无法区分“真没数据”和“缓存里有个空”。推荐统一用固定字符串(如 "NULL")代替 null,并严格控制过期时间(通常 1–5 分钟足够)。注意别用 "",因为有些 SDK 反序列化后会把空字符串转成 null,导致判断失效。
-
SET user:123 "NULL" EX 60—— 写入明确可识别的空标记 - 读取后必须先判断是否等于
"NULL",再决定是否查库,不能只判== null - 不要给空值设太长 TTL,否则热点空 key 会长期占内存,还拖慢淘汰效率
Java 用 StringRedisTemplate 时,get() 返回 null 不代表没命中
StringRedisTemplate 的 get() 在 key 不存在时返回 null,但如果 key 存在且值是空字符串或 "NULL",它会返回对应字符串。所以不能靠 get() == null 判断“是否需要查库”,而要分三步走:
- 调用
opsForValue().get("user:123") - 结果为
null→ key 确实不存在,可查库(但要防穿透,建议此时写空值) - 结果为
"NULL"→ 明确是空缓存,直接返回空对象,不查库 - 结果为其他字符串 → 正常解析业务数据
空值缓存 + 布隆过滤器不是银弹,小项目慎加
布隆过滤器能提前拦截 99% 的非法 key 查询,但它有误判率、需要预热、还要维护一致性。如果业务只有几十个 ID 类型查询,或者 ID 是自增且范围可控,直接用空值缓存 + 短 TTL 就够了。强行上布隆过滤器反而增加部署复杂度和序列化开销,特别是当你的 user_id 是 UUID 或手机号这种非规律值时,预热成本高、空间利用率低。
真正容易被忽略的是:空值写入必须和 DB 查询在同一个原子上下文中完成,否则查库返回空、还没来得及写空缓存,这时并发请求就又打穿进去了。要么用 Lua 脚本包住查库+写缓存,要么在应用层加简单分布式锁(比如 SET lock:user:123 "1" NX EX 5),但锁时间一定要比 DB 查询耗时略长,不然可能失效。

















