不能直接存none或空字符串,因为序列化后无法区分“key不存在”和“已缓存空值”,易导致误判;应使用显式标记如"@@empty@@"或自定义nullvalue类,并通过原子命令set ex设置ttl。

缓存空值时为什么不能直接存 None 或空字符串?
因为 None、""、0 在大多数序列化/反序列化逻辑里会被统一转成 falsy 值,和真实业务数据的空响应(比如用户字段全为 null 的合法对象)无法区分。一旦缓存层误判,就会把“本该返回空对象”的请求当成“缓存穿透已处理”,实际却漏掉了真实数据。
正确做法是用一个**显式、唯一、不可伪造的标记字符串**,比如 "@@EMPTY@@" 或 "NULL"。Java 场景下更推荐自定义类型(如 NullValue 类),避免反序列化时类型擦除问题。
- Python 示例中必须用
r.setex(cache_key, 120, "@@EMPTY@@"),不能用r.setex(cache_key, 120, None) - Spring Boot + RedisTemplate 中,
redisTemplate.opsForValue().set(key, "NULL", 60, TimeUnit.SECONDS)比存null安全 - 如果用了 JSON 序列化,务必确认
JSON.parseObject(json, Xxx.class)不会把"NULL"解析成有效对象
setex 和 set + expire 选哪个?
必须用 setex(或 SET key value EX seconds)。原因很简单:空值缓存写入必须是原子操作。如果先 set 再 expire,中间发生进程崩溃或 Redis 连接中断,就会留下永久存在的空值 key,吃光内存。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- Redis 命令层面:优先用
SET key value EX 120,不是SET key value+EXPIRE key 120 - Java 客户端:用
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS),它底层调用的就是原子SETEX - Python redis-py:用
r.setex(key, 120, "@@EMPTY@@"),不要拆成两步
空值 TTL 设多少才合理?
TTL 不是越短越好,也不是越长越省 DB。设太短(如 5 秒)会导致高频穿透;设太长(如 24 小时)会让刚写入数据库的新数据在缓存里“隐身”很久。实际应按业务容忍窗口+数据变更频率综合判断。
- 用户类数据(ID 稳定、新增少):建议
120~300秒(2~5 分钟) - 商品类数据(上架/下架频繁):建议
60秒,配合后台异步刷新机制 - 绝对不能全局硬编码一个值,比如所有空值都设
3600秒——这等于把缓存一致性风险放大到小时级 - 上线后必须监控
redis-cli --scan --pattern "user:*" | wc -l类似命令,观察空值 key 数量是否异常增长
为什么空值缓存要搭配请求校验一起用?
空值缓存本身不防攻击,只降低 DB 压力。如果攻击者持续用 user_id=-1、user_id=9999999999 这类非法 ID 轮询,空值缓存只是把压力从 DB 转移到 Redis 内存和网络带宽——大量无效 key 仍会挤占 LRU 空间,甚至触发 Redis 内存淘汰策略误删热 key。
- 必须在 Controller 层做基础校验:
if (id 10000000) return ResponseEntity.badRequest().build(); - ID 类型字段建议用布隆过滤器预筛(尤其用户/商品主键全量可枚举时),空值缓存作为兜底
- 对高频穿透请求 IP,需结合限流(如
RateLimiter或网关层sentinel)做二次拦截
空值缓存不是银弹,它只解决“查不到但又不得不查”的场景。真正要拦住恶意流量,得靠前置校验 + 过滤器 + 限流三层防线,否则再短的 TTL 也扛不住字典式攻击。










