Django 中如何使用缓存?缓存失效和一致性问题怎么处理?
简化版
Django 提供统一缓存框架,可以接本地内存、文件、数据库、Memcached、Redis 等后端。常见用法包括页面缓存、视图缓存、模板片段缓存和低层 API 缓存;真正难点不在 cache.get/set,而在 key 设计、过期时间、主动失效、防穿透、防击穿和数据一致性取舍。
详细版
Django 缓存通常在 settings.py 的 CACHES 中配置,然后通过 django.core.cache.cache 使用。简单场景可以缓存热门配置、列表页结果、权限信息、聚合统计;页面级缓存适合内容变化不频繁的页面,低层 API 更适合精细控制。
from django.core.cache import cache
def get_site_config():
key = "site:config:v1"
data = cache.get(key)
if data is None:
data = load_config_from_db()
cache.set(key, data, timeout=300)
return data
缓存策略要配合业务一致性:读多写少可以设置较长 TTL,并在写入后删除相关 key;强一致要求高的核心交易数据不应只依赖缓存。面试里要说清:缓存能提升性能,但会引入脏数据、雪崩、击穿、穿透和失效维护成本。
完整版教学
一、Django 缓存框架解决什么问题
Web 应用里很多数据读多写少,例如站点配置、首页推荐、商品分类、用户权限、聚合统计。如果每次请求都查数据库、调用外部接口、重新渲染复杂模板,延迟和数据库压力都会升高。缓存的目的就是把昂贵结果暂存起来,让后续请求更快返回。
假设一个热门接口每秒 1000 次请求,原始数据库查询耗时 30ms。如果完全打到数据库,数据库每秒要处理 1000 次同类查询;命中缓存后,绝大多数请求可能 1ms 内返回,数据库只在缓存过期或失效时承受少量回源。
无缓存:
请求 -> Django -> DB -> 响应
有缓存:
请求 -> Django -> Cache 命中 -> 响应
|
+-> 未命中才查 DB
Django 的缓存框架把不同后端统一成一套 API,业务代码不必直接绑定 Redis 或 Memcached 客户端。
二、缓存后端和缓存层级怎么选
Django 支持多种缓存后端。本地内存缓存配置简单,但多进程、多机器之间不共享;Redis/Memcached 适合多实例部署;文件或数据库缓存更偏特殊场景,一般高并发接口不首选。
| 后端 | 优点 | 风险/限制 |
|---|---|---|
| LocMemCache | 简单,无外部依赖 | 多进程不共享,重启丢失 |
| Redis | 功能强,TTL、分布式部署成熟 | 需要运维,网络调用有成本 |
| Memcached | 简洁高性能 | 数据结构能力较弱 |
| FileBasedCache | 配置容易 | I/O 和清理成本 |
| DatabaseCache | 无额外组件 | 给数据库增加压力 |
缓存层级也有差别:全站缓存最粗,视图缓存按接口粒度,模板片段缓存按页面局部,低层 API 则由业务控制 key、TTL 和失效。越精细越灵活,但维护成本也越高。
三、key 设计为什么是核心
缓存 key 必须把影响结果的因素编码进去。用户、语言、分页、筛选条件、权限、版本号都可能影响结果。如果 key 太粗,不同用户会拿到彼此的数据;如果 key 太细,命中率又会很低。
def article_list_key(user_id: int, page: int, tag: str | None):
tag_part = tag or "all"
return f"article:list:v2:user:{user_id}:tag:{tag_part}:page:{page}"
版本号是很实用的技巧。比如列表结构改了,旧缓存不兼容,可以把 v1 改成 v2,让新代码自然读新 key,旧 key 等 TTL 到期自动消失。
article:list:v1:page:1 -> 旧结构
article:list:v2:page:1 -> 新结构
缓存 key 不是随便起名,它是“结果由哪些条件决定”的工程化表达。
四、TTL 和主动失效怎么配合
缓存失效有两类思路:被动过期和主动删除。被动过期靠 TTL,简单但可能在过期前读到旧数据;主动删除是在数据写入成功后删除相关缓存,能更快反映变化,但需要准确知道哪些 key 受影响。
def update_article(article_id, data):
article = Article.objects.get(id=article_id)
...
article.save()
cache.delete(f"article:detail:{article_id}")
cache.delete("article:list:v2:page:1")
这里最难的是列表缓存。修改一篇文章可能影响多个分页、标签、搜索条件下的列表。工程上常用短 TTL、版本号、按业务维度粗粒度失效、异步重建等方式降低复杂度。
| 策略 | 优点 | 代价 |
|---|---|---|
| 只用 TTL | 简单 | 过期前可能旧 |
| 写后删除 key | 更新快 | key 依赖难维护 |
| 版本号失效 | 批量切换容易 | 旧 key 等待过期 |
| 异步重建 | 高峰更稳 | 架构复杂 |
五、缓存穿透、击穿、雪崩怎么防
缓存穿透是请求的数据根本不存在,每次都打到数据库;击穿是某个热点 key 过期瞬间大量请求同时回源;雪崩是大量 key 同时过期或缓存服务故障,导致流量压垮数据库。
可以用数字理解:一个热点商品详情每秒 5000 请求,缓存过期后如果 5000 个请求同时查 DB,数据库瞬间压力会飙升。加互斥锁或 singleflight,只允许一个请求回源重建,其余等待或返回旧值,就能缓解击穿。
穿透: 查不存在 id -> cache miss -> DB miss -> 下次还 miss
击穿: 热点 key 过期 -> 大量请求同时回源
雪崩: 大量 key 同时过期/缓存故障 -> DB 被整体打爆
常用手段包括:空值缓存、布隆过滤器、热点 key 加互斥重建、TTL 加随机抖动、多级缓存、限流降级。Django 项目里这些通常要结合 Redis 和业务代码实现,不是框架自动解决。
六、一致性边界怎么回答
缓存和数据库之间很难做到简单又强一致。多数 Web 业务选择最终一致:写数据库成功后删除缓存,下一次读取未命中再回源。这个模式仍有短暂不一致窗口,但实现简单、可接受范围广。
更稳的写法是“先写数据库,再删缓存”。如果先删缓存再写数据库,写库期间另一个请求可能读旧数据库并回填旧缓存。先写库再删缓存也不是绝对完美,但风险更小,必要时可以延迟双删或用消息队列补偿。
推荐基本顺序:
写 DB 成功 -> 删除 cache -> 后续读 miss -> 回源新 DB -> set cache
核心交易数据要谨慎使用缓存作为判断依据。例如余额、库存扣减、支付状态,不能只看缓存决定最终结果;缓存可以辅助读性能,最终一致性和并发控制仍要落在数据库事务、锁或业务状态机上。
七、常见误区与追问
- 误区:用了缓存就一定更快。 key 命中率低、序列化大对象、网络调用频繁时,缓存可能收益有限甚至更慢。
- 误区:TTL 设长一点就行。 TTL 长会增加旧数据时间窗口,必须结合业务一致性要求。
- 误区:写数据库后更新缓存比删除缓存更好。 更新缓存容易漏掉相关 key 和并发覆盖;很多场景删除缓存更简单稳妥。
- 追问:缓存穿透怎么处理? 空值缓存、参数校验、布隆过滤器、限流都可以降低不存在 key 反复打 DB。
- 追问:热点 key 过期瞬间怎么办? 加互斥重建、逻辑过期、后台刷新或返回短暂旧值。
- 追问:Django 多进程能用本地内存缓存吗? 可以用于简单开发或单进程场景,多 worker/多机器不共享,生产通常用 Redis/Memcached。
- 追问:如何避免不同用户拿到同一份缓存? key 中必须包含用户、权限、语言、租户等影响结果的维度。
八、加强记忆
Django 缓存不要只记 API,要记“后端选择、key 设计、TTL/主动失效、三大缓存事故、一致性边界”。缓存命中能把 30ms 的数据库查询压到 1ms 级别,但它同时引入旧数据和失效治理成本。面试回答时先说 Django 缓存层级,再讲 key 和失效,最后补穿透/击穿/雪崩与先写 DB 后删缓存,这样才像真正做过线上缓存。