11.1 缓存系统 本节摘要:缓存是把"重复计算的结果"存起来下次直取。Django 提供统一的缓存框架:换后端不改代码,粒度从整页到片段到单个查询结果递细。本节配置 Redis 后端、讲三种粒度的实现与选择、缓存键设计与失效策略。缓存的难点从来不是"怎么存",而是"什么时候让它失效"。 配置后端 后端可插拔:进程内存(开发)、数据库表(无 Redis 时的兜底)、Redis(生产标配)。接口统一,换后端配置一行。生产选 Redis 的理由:过期策略成熟、支持内存上限与淘汰、网络共享让多个 worker 命中同一份缓存——进程内存后端在多 worker 拓扑下各存一份,命中率被 worker 数摊薄。 三种粒度 粒度一:低级 API,缓存"计算结果"。
本节摘要:缓存是把"重复计算的结果"存起来下次直取。Django 提供统一的缓存框架:换后端不改代码,粒度从整页到片段到单个查询结果递细。本节配置 Redis 后端、讲三种粒度的实现与选择、缓存键设计与失效策略。缓存的难点从来不是"怎么存",而是"什么时候让它失效"。
CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": "redis://127.0.0.1:6379", "TIMEOUT": 300, # 默认过期 5 分钟 "KEY_PREFIX": "moji", # 多应用共库时的命名空间 } }
后端可插拔:进程内存(开发)、数据库表(无 Redis 时的兜底)、Redis(生产标配)。接口统一,换后端配置一行。生产选 Redis 的理由:过期策略成熟、支持内存上限与淘汰、网络共享让多个 worker 命中同一份缓存——进程内存后端在多 worker 拓扑下各存一份,命中率被 worker 数摊薄。
粒度一:低级 API,缓存"计算结果"。 最灵活,墨迹博客的热门标签侧栏:
from django.core.cache import cache def hot_tags(): key = "blog:hot-tags" tags = cache.get(key) if tags is None: tags = list( Tag.objects.annotate(n=Count("articles")) .order_by("-n")[:10]) cache.set(key, tags, timeout=600) return tags
get 未命中才查库、set 写回——这个"先查缓存、miss 再算"的模式是缓存的一切基础。key 的设计要有命名空间与参数:blog:article-list:{category}:{page},参数不同键不同,绝不能一钥多义。
粒度二:模板片段缓存,缓存"页面零件"。
{% load cache %} {% cache 600 sidebar-hot-tags %} <div class="hot-tags"> {% for tag in hot_tags %}<a>{{ tag.name }} {{ tag.n }}</a>{% endfor %} </div> {% endcache %}
首请求渲染并存储,十分钟内直接吐 HTML 片段,连模板渲染都省了。适合"渲染贵、变化慢"的块:侧栏、页脚、归档列表。注意别缓存包含用户个性化内容(登录状态、用户名)的块——那会把 A 用户的状态展示给 B。
粒度三:整页缓存。 全站缓存中间件把整个响应按 URL 存取,命中时连视图都不进。威力大、限制也大:任何与用户相关的页面都不可用。内容型站点的匿名首页、RSS 输出适合;登录态齐全的页面直接排除。

缓存策略的名言是"计算机科学只有两个难题:命名和缓存失效"。三条实用路线:
路线一:短过期。 侧栏统计容忍五分钟陈旧,就设五分钟过期,最省心。适合"旧一点无所谓"的数据,内容型站点的大多数展示块都属此类。
路线二:主动失效。 写操作后删键:
def publish_article(article): article.status = Article.PUBLISHED article.save() cache.delete("blog:hot-tags") # 发布改变统计 相关缓存作废 cache.delete_pattern("blog:article-list:*") # 列表页逐键失效
及时性好,但要小心"忘了删某处"的遗漏——凡是加新缓存键的代码评审,必检查对应的失效点。
路线三:版本化键。 键里带版本号 blog:hot-tags:v7,发布或重大变更时版本号递增,旧键等过期自然死亡,免逐个清理。数据结构大改时最省事。
⚠️ 缓存穿透的两种雨天场景:其一,键里拼了用户输入的任意值(页码一万个、分类名乱拼),缓存被灌满垃圾键——参数先归一化校验再拼键;其二,缓存了查询集对象后代码继续迭代它,把整表拉进 Redis——缓存前先 list 落地,存"数据"别存"说明书"(呼应第 3 章的惰性求值)。
最后给一个判断"该不该上缓存"的框架,比技术细节更常被忽视。缓存适合的场景有三个特征:读远多于写、同样的读大量重复、数据允许短暂陈旧。墨迹博客的热门标签三项全占,是教科书式的缓存对象;反例是文章编辑页——读写相当、每次读的内容都随用户操作变化、陈旧不可容忍,硬上缓存只会换来一堆失效代码和偶发的数据错乱。遇到"要不要缓存"的犹豫时,用这三个特征逐条打分,一条都不占的答案就是明确的:先优化查询本身,缓存不是性能问题的默认答案,而是特定数据访问模式的配套方案。
缓存上线后必须回答一个问题:它到底起了多大作用?凭体感说好像快了不算数,要看两个数字:命中率与命中后的响应分位。Redis 的命中率用统计命令一查便得,健康线通常在九成上下——低于它说明键设计或过期策略有问题,大量请求绕过缓存直击数据库。把命中率纳入日常巡检后,墨迹博客发现过一个隐蔽问题:某次改版把列表页的页码参数从整数改成了字符串传入,页码二和页码二点零生成了不同的键,命中率悄悄掉了两成,而页面表现完全正常。这种无声的劣化只有度量能暴露。缓存是所有性能手段里最依赖反馈闭环的一个:上了不量,等于没上。
缓存挡住了重复读,下一章继续压测找剩下的慢点,建立完整的调优闭环。