Django的cache.get()返回None无法区分缓存未命中与数据库查无结果,应使用哨兵值(如_MISSING=object())显式标记“不存在”,并分设合理超时以防止缓存穿透。

直接说结论:Django 的 cache.get() 返回 None 是正常行为,但“安全处理不存在的查询对象”关键不在缓存层,而在如何区分「缓存未命中」和「数据库查无此对象」——否则会缓存空结果或引发 N+1 查询。
为什么 cache.get() 无法区分「没缓存」和「缓存了 None」?
Django 默认的 cache.get(key) 在 key 不存在或显式存入 None 时都返回 None。如果你在数据库查不到对象后写入 cache.set(key, None),后续就无法判断这次 None 是缓存穿透防护(故意存的),还是单纯还没缓存过。
- 典型错误模式:
obj = cache.get(key); if obj is None: obj = MyModel.objects.get(id=123); cache.set(key, obj)—— 一旦MyModel.objects.get()抛MyModel.DoesNotExist,obj仍为None,被误存进缓存,导致永久性缓存穿透 - 正确做法是捕获异常,并用特殊标记(如
cache.set(key, _MISSING, timeout=...))显式表示「查无此数据」 - 推荐用哨兵值(例如
_MISSING = object()),避免和业务中可能合法的None冲突
用 cache.get_or_set() 要小心它的「原子性假象」
cache.get_or_set(key, default_func, timeout) 看似能省掉条件判断,但它只保证「获取 + 设置」的原子性,不保证 default_func 执行本身是幂等的——尤其当 default_func 是数据库查询时,多个并发请求仍可能同时触发多次 DB 查询。
- 问题场景:高并发下,key 第一次失效,10 个请求同时调用
cache.get_or_set(..., lambda: MyModel.objects.get(id=123)),全部执行.get(),造成 DB 压力 - 缓解方案:对热点 key 加一层本地锁(如
threading.Lock或redis.lock),或改用cache.add()配合后台异步加载 - 更稳妥的写法是手动实现「先 get → 检查是否 _MISSING → 否则查 DB → 成功则 set,失败则 set _MISSING」
推荐的健壮缓存读取模式(含 Redis 特性利用)
利用 Redis 的 GET 原语天然返回 nil(对应 Python None),再配合 Django cache backend 的 get_many() 批量探查能力,可减少网络往返。但核心仍是逻辑分层清晰:
from django.core.cache import cache
<p>_MISSING = object() # 全局唯一哨兵</p><p>def get_cached_model_instance(model_class, pk):
cache_key = f"{model_class._meta.db_table}:pk:{pk}"
cached = cache.get(cache_key)
if cached is _MISSING:
return None # 明确知道 DB 里没有
if cached is not None:
return cached # 缓存命中</p><h1>缓存未命中:查 DB</h1><pre class="brush:php;toolbar:false;">try:
obj = model_class.objects.get(pk=pk)
cache.set(cache_key, obj, timeout=300)
return obj
except model_class.DoesNotExist:
cache.set(cache_key, _MISSING, timeout=60) # 短期缓存「不存在」,防穿透
return None
- 注意
_MISSING的 timeout 应显著短于有效对象的 timeout(比如 60s vs 300s),避免长期阻塞重试 - 如果用的是
django-redis,可进一步用cache.client.get_client().exists(cache_key)判断 key 是否真实存在于 Redis 中(绕过 Django cache 的抽象层),用于调试或监控缓存穿透率 - 不要把模型实例直接塞进 cache——序列化/反序列化开销大,且可能带循环引用;建议只缓存必要字段字典或使用
model_to_dict()
最易被忽略的一点:缓存「不存在」状态时,timeout 时间必须可控。设太长会导致数据实际已写入 DB 后,缓存还在拒绝响应,形成短暂但可感知的数据不一致;设太短又起不到防穿透作用。这个平衡点得结合你的更新频率和业务容忍度手工调优,没有通用值。











