空值缓存必须用固定字符串标记(如"null"),禁用none/空字符串;cache.get()返回三态需分别处理;空值写入须与db查询原子化,推荐redis分布式锁+60–300秒ttl。

空值缓存必须用字符串标记,不能存 null 或空字符串
很多 Django 项目直接用 cache.set(key, None) 或 cache.set(key, "") 存“空结果”,这会导致后续无法区分:是 key 真不存在,还是缓存没写成功,或是业务返回了合法的空响应(比如空列表)。Redis 客户端行为也不一致——django-redis 默认把 None 序列化为 "None" 字符串,而某些配置下又会静默丢弃。实际应统一用固定标记,例如 "NULL":
cache.set("user:999999999", "NULL", timeout=60)- 读取后必须显式判断:
if cached == "NULL": return None,不能只靠is None或not cached - 避免用
"":部分前端或序列化逻辑会把空字符串转成null,下游误判风险高
cache.get() 返回值有三态,必须分情况处理
Django 的 cache.get() 在三种情况下返回不同值,漏掉任一态都会导致穿透漏防:
-
None→ key 确实未命中缓存,需查 DB(但此时要防并发,见下一条) -
"NULL"→ 明确是空缓存,直接返回空,不查 DB、不重试 - 其他字符串 → 正常反序列化,如
json.loads(cached)
典型错误是写成 if not cache.get(key): ...,这样会把 "NULL" 和 "" 都当成“需要查库”,等于空值缓存完全失效。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
空值写入必须和 DB 查询原子化,否则并发仍会穿透
即使加了空值缓存,如果“查 DB 返回 None”和“写 "NULL"”之间存在时间窗口,高并发请求就会反复打穿。Django 本身不提供跨操作原子性,需手动补:
- 用 Redis 分布式锁兜底:
cache.set("lock:user:999999999", "1", timeout=5, nx=True),只让一个请求走 DB 流程 - 锁超时必须 > DB 查询耗时(建议设为 5–8 秒),避免锁提前释放引发重复穿透
- 更稳妥的做法是把查 DB + 写缓存封装进一个函数,并在视图层用
@transaction.atomic(仅对 DB 操作有效)+ 锁双保险 - 不推荐 Lua 脚本:Django 生态对 Redis Lua 支持弱,调试成本高,且多数场景锁已够用
短 TTL 是硬约束,别被“防得越久越好”误导
空值缓存的过期时间不是越长越好。恶意爬虫构造的非法 ID 通常集中爆发,设太长反而撑爆 Redis 内存:
- 推荐区间:60–300 秒(1–5 分钟);冷启动或攻击高峰期可临时压到 60 秒
- 超过 600 秒(10 分钟)需警惕:监控
redis-cli --bigkeys和INFO memory中used_memory_peak_human - 别用永不过期:
cache.set(..., timeout=None)是危险操作,等同于给爬虫养长期户口 - 注意
django-redis的timeout=None行为:取决于后端驱动,Lettuce 下可能变成 0,Jedis 下可能报错
真正容易被忽略的是空值缓存和布隆过滤器的配合节奏——布隆过滤器预热不全时,空值缓存就是最后一道防线;但如果你把空值 TTL 设得太短,又没及时更新布隆,那每分钟几千次的无效请求就全靠它扛。这时候不是调参数,而是得看日志里 "NULL" 写入频次是否持续高于正常查询量的 5%。










