Django 缓存怎么加才不返工:三层粒度与一个默认陷阱

作者:程序员白大力 · 2026-09-29 18:40
Django 缓存怎么加才不返工:三层粒度与一个默认陷阱

很多人给 Django 站点加缓存的过程是这样的:听说缓存能提速,搜到一行 cache_page 装饰器,贴上,重启,然后发现有的页面快了,有的没变,还有的页面登录后显示的是别人的内容。

问题不在缓存本身,而在两个没想清楚的事:缓存该加在哪一层,以及——最容易被忽略的——数据到底存在了哪里。这篇文章把这两件事拆开讲。

先说一条底线:缓存不是修慢查询的工具

缓存解决的是「重复计算」:同一个结果被反复算了很多遍,把第一次的结果存下来,后面的请求直接拿。它赚的是重复的钱。

如果页面慢是因为一条没走索引的查询,缓存只能把问题推迟到每次缓存过期那一刻——冷请求照样卡,过期瞬间还可能一波查询同时打到数据库上。正确的顺序是先用上一篇文章里的办法把查询本身修好,再考虑加缓存。

一、粒度:三个层级,从粗到细

Django 官方给了三个常用粒度,怎么选取决于「这份结果能被多少请求复用」:

层级 写法 适合场景 粒度风险
视图级 cache_page(600) 装饰视图 内容对所有访客一致的文章页、列表页 整页缓存,登录态内容混进去就出事
模板片段 {% cache 600 sidebar %} 页面主体动态、某一块很贵(如侧栏热门榜) 只缓存那一块,其余照常渲染
低层 API cache.get / cache.set 缓存查询结果、计算结果等任意对象 最灵活,键设计全靠自己

视图级缓存按 URL 存取:/articles/1/ 和 /articles/2/ 是两个键,同一 URL 的后续请求直接命中。几个页面结构相似、只有一小块贵的,用模板片段更合适。

下面是视图级的最小用法,把装饰器套在视图上,600 秒内同一 URL 的请求不再执行视图函数:

from django.views.decorators.cache import cache_page

@cache_page(600)          # 600 秒
def article_detail(request, pk):
    ...

低层 API 最常用的是 get_or_set,一行完成「取不到就算一遍并存回去」:

from django.core.cache import cache

def hot_articles():
    return cache.get_or_set(
        "hot:articles",                       # 键名要能读出含义
        lambda: list(Article.objects.order_by("-views")[:10]),
        timeout=600,
    )

注意上面那个 list():QuerySet 是惰性的,直接塞进缓存存的只是「一段还没执行的查询描述」,取出来执行时照样查库。缓存前先物化,这是低层 API 最常见的一个静默错误。

二、后端:一个大多数人踩过的默认陷阱

不配置 CACHES 时,Django 默认用的是 LocMemCache——存在当前进程的内存里。本地开发单进程跑,一切正常;上了生产、Gunicorn 起了 4 个 worker,就变成了 4 个互不相通的缓存:

  • 命中率被摊薄,同一个键可能在每个 worker 里各存一份;
  • cache.delete("hot:articles") 只删了接到这个请求的那个 worker 的键,其他 worker 继续吐旧数据;
  • LocMem 默认最多存 300 个键(MAX_ENTRIES 默认值),超了就挤掉旧的——缓存「莫名失效」往往就是这个数在起作用;
  • 进程重启,缓存全部清零。

判断标准很直接:只要生产是多进程,缓存就该放在进程外。Django 4.0 起内置了 Redis 后端,不用再装第三方包,装上 redis 这个 Python 客户端即可:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "KEY_PREFIX": "mysite",   # 多个项目共用 Redis 时防串键
        "TIMEOUT": 300,
    }
}

TIMEOUT 有三个值语义完全不同,混用是另一个高频坑:默认 300 秒;None 表示永不过期;0 表示立即过期、等于不缓存——想「存得久一点」把超时写成 0,结果缓存彻底不生效,这类问题日志里不会有任何报错。

三、两个比性能更疼的坑

坑一:整页缓存了带登录态的页面。 cache_page 按 URL 存取,官方文档明确写了它不会自动按登录状态、session、cookie 区分访客。把登录后的个人页、管理页也套上 cache_page,第一个访问的用户内容会被缓存,接下来 600 秒内所有访客看到的都是他的页面——这是功能性事故,不是性能问题。原则:只缓存对所有匿名访客都一致的内容,含登录态的页面顶多缓存其中的匿名片段。

坑二:把失效想得太简单。 缓存最难的从来不是存,而是「数据变了怎么让它变」。cache.delete(key) 只在你记得住所有键名时才有效;模板片段的键可以用 make_template_fragment_key() 算出来再删。但小站更省心的做法通常是接受短暂过期:把超时设成业务能容忍的陈旧窗口(比如热门榜 10 分钟),用「最多旧 10 分钟」换掉整套主动失效逻辑。方案越简单,越不容易在半夜出事。

结尾

加缓存之前想清楚三件事:这份结果对谁一致(决定粒度)、存在哪个进程里(决定后端)、旧了多久能忍(决定失效策略)。三件事想明白,Django 的缓存系统用最少的代码就能稳定吃到你想要的提速;想不明白,加的每一层缓存都是在给未来的自己埋雷。

如果你也在给站点做性能优化,欢迎把这篇转给正在和慢页面搏斗的同事。

参考文章

  • Django 官方文档:Django's cache framework
  • Django 官方文档:CACHES 设置(TIMEOUT 与默认后端)
  • Django Ticket #27641:Document limitations of the Locmem cache backend
  • Django Deployment Hub:Set Up Redis Caching for Django in Production
  • Google Cloud Blog:Using Memorystore for Redis to cache your Django applications
本文由 AI 辅助生成并经人工审核发布,内容仅供参考。