空值缓存必须用明确标记(如"__null__")而非null或"",读取时需三态判断;ttl须分级设置;空值写入需与db查询原子化,优先在网关层拦截非法请求。

空值缓存必须用可识别的字符串标记,不能存 null 或 ""
直接调用 redisTemplate.opsForValue().set(key, null, 60, TimeUnit.SECONDS) 是危险操作:Spring Data Redis 多数序列化器会把 null 写成 Redis 的 nil 响应,读取时返回 null,但你无法区分“key 真不存在”和“我明明写了空值却没生效”。更糟的是,有些版本反序列化后变成字符串 "null",导致业务误判为有效数据。
推荐统一使用明确标记值:
-
"__NULL__"(双下划线包裹,避免与业务字段冲突) -
"{}"(空 JSON 对象,适合已用 JSON 序列化的项目) -
new byte[0](字节数组,规避字符串编码歧义)
不要用 ""——部分 SDK 反序列化后会转成 null,破坏三态判断逻辑。
读取时必须做字符串相等判断,不能只看 == null
StringRedisTemplate.get() 的返回值有三种可能:
-
null→ key 在 Redis 中完全不存在(需查库,且建议此时写空值防并发穿透) -
"__NULL__"→ 明确的空缓存标记(直接返回空,不查库) - 其他非空字符串 → 正常业务数据(反序列化解析)
错误写法:if (cached == null) { queryDB(); } —— 它漏掉了 "__NULL__" 这种已缓存空值的情况。
正确写法:if (cached == null) { /* key 未命中,查库并写空值 */ } else if ("__NULL__".equals(cached)) { /* 空缓存,直接返回 */ } else { /* 正常数据 */ }
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
SET 命令要带 EX 且 TTL 必须分级设置
空值不是“缓存一次就完事”,TTL 设错等于没防住穿透:
- 格式非法 key(如
user:abc、user:-1)→ TTL 设为10秒,这类请求本不该进缓存层,秒级过期即可 - 格式合法但 DB 查无结果(如
user:999999999)→ TTL 控制在60–180秒,覆盖重试窗口又不阻塞真实数据上线 - 监控发现某 key 空值命中率 >80% → 自动降为
5秒并触发告警,防止被当成固定攻击点
别用 SETEX key 300 "__NULL__" 一刀切。高并发下大量 user:999999999 长期驻留,会挤占内存、拖慢淘汰效率。
空值写入必须和 DB 查询原子化,否则并发下照样穿透
典型漏洞场景:两个请求同时查 user:123,都未命中缓存 → 都去查 DB → 都得到 null → 都准备写空值 → 但只有一个能成功写入,另一个仍穿透到 DB。
解决方式只有两种:
- 用 Lua 脚本封装查 DB + 写缓存逻辑(要求 DB 查询能在 Redis 端执行,通常不现实)
- 应用层加轻量锁:
SET lock:user:123 "1" NX EX 5,锁时长要比 DB 查询耗时略长(比如 DB 平均 800ms,锁设 2 秒),否则锁提前释放会导致二次穿透
最容易被忽略的是:空值缓存只是最后一道防线。它不解决“为什么非法请求能走到这一步”,真正该拦在网关或 Controller 层——比如 @Min(1) 校验 ID,或正则限制手机号格式。否则,你写的 "__NULL__" 再规范,也扛不住每秒几千次 user:abc 的扫描流量。










