← 返回题目列表

Django 中如何使用缓存?缓存失效和一致性问题怎么处理?

高频 中等 第 9 / 27 题 更新于 2026/07/31
Django缓存Redis缓存失效

简化版

Django 提供统一缓存框架,可以接本地内存、文件、数据库、Memcached、Redis 等后端。常见用法包括页面缓存、视图缓存、模板片段缓存和低层 API 缓存;真正难点不在 cache.get/set,而在 key 设计、过期时间、主动失效、防穿透、防击穿和数据一致性取舍。

详细版

Django 缓存通常在 settings.pyCACHES 中配置,然后通过 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 后删缓存,这样才像真正做过线上缓存。