很多人给 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