Django + Redis 缓存:模式、陷阱与真实世界经验
I’m happy to translate the article for you, but I’ll need the full text of the post. Please paste the content you’d like translated (excluding the source line you already provided), and I’ll return a Simplified‑Chinese version that preserves the original formatting, markdown, and technical terms.
为 Django 应用添加 Redis 缓存
为 Django 应用添加 Redis 缓存看起来往往是一次轻松的收获:一个慢接口、一个 Redis 实例,响应时间瞬间从秒级降到毫秒级。Django 与 django‑redis 让实现细节足够直接,以至于初级工程师可以在一天内交付一个可工作的缓存。
危险之处在于,一旦缓存“起作用”,就会产生一种已解决的错觉。实际上,Redis 只加速了你已经做出的决策——好或坏。正确性、安全性、失效以及并发仍然是你的责任。本文聚焦于 Django 已经为你解决的部分、它有意不解决的部分,以及如何像高级工程师而不是框架使用者一样思考缓存决策。
Django 与 django‑redis 已经解决的内容
Django 的缓存框架配合 django‑redis 开箱即用地提供了强大的原语:
- TTL 支持 – 缓存值可以在固定时长后自动过期。Redis 负责驱逐,Django 通过简洁的 API 暴露 TTL。
- 原子操作 –
cache.get_or_set()与 Redis 原语允许原子写入,防止在缓存填充期间出现部分更新和状态不一致。 - 分布式锁 –
cache.lock()提供基于 Redis 的锁,跨进程、跨机器工作,可用于防止缓存击穿。 - 连接池 – Redis 连接被池化并高效复用;你无需手动管理套接字或客户端。
Django 没有提供的是正确性。它无法判断哪些数据可以安全缓存、键应该如何作用域,或者何时域内数据失效。
缓存逻辑应放在哪里?(视图层 vs. 服务层)
视图层 – HTTP 相关逻辑(请求/响应、序列化)
服务层 – 业务逻辑(获取用户资料、权限、业务规则)
在第一次迭代时,缓存通常应放在 服务层,而不是视图层:
def get_user_profile(user_id):
key = f"user_profile:{user_id}"
data = cache.get(key)
if data:
return data
profile = User.objects.get(id=user_id)
data = {"id": profile.id, "name": profile.name}
cache.set(key, data, timeout=300)
return data
这样可以让缓存保持显式、可测试,并且贴近数据语义。等到模式被验证安全后,你可以考虑抽象出更高级的封装。
缓存键:正确编码上下文
“如果两个用户可以从同一端点得到不同的响应,你的缓存键必须编码该上下文——否则根本不该缓存。”
编码 意味着在缓存键中包含所有影响响应的相关上下文。
安全示例
key = f"user_profile:{user_id}"
不安全示例
key = "user_profile"
键设计不当的危害
- 陈旧数据 bug – 某用户更新了个人资料,但其他用户仍因键未反映变化而看到旧数据。
- 安全 bug – 若未编码授权上下文,某用户可能从缓存中获取到另一用户的数据。
什么可以安全缓存?
通常安全的
- 公开的、读取密集的数据
- 按用户 ID 作用域的用户拥有数据
- 失效规则简单的数据
需要谨慎的
- 权限相关数据
- 功能标记(feature flags)
- 基于角色或策略的响应
如果无法编码,就不要缓存
有些上下文过于复杂或不稳定,难以安全编码。
错误示例
key = f"dashboard:{user_id}:{user.permissions}"
key = f"features:{user_id}:{','.join(active_flags)}"
如果上下文经常变化或来源众多,缓存它会导致返回错误或未授权的数据。
TTL 值以及 TTL 泄露数据的时长
TTL 不仅关乎新鲜度——它还涉及授权的生命周期。
cache.set(key, data, timeout=3600) # 1 小时
如果权限在此期间发生变化,而缓存仍然有效,就会出现授权泄漏。
(未完,续在第 2 部分)
- 用户可能仍保有本不该拥有的访问权限。
- 被撤销的访问可能在 TTL(生存时间)到期前仍然有效。
更短的 TTL 可以降低风险,但会增加系统负载。没有通用的 TTL 值——TTL 是业务层面的决策。
缓存失效策略
失效必须是显式的,并且要与写入路径绑定。
def update_user_profile(user_id, data):
User.objects.filter(id=user_id).update(**data)
cache.delete(f"user_profile:{user_id}")
如果你无法可靠地识别 所有 变更路径,对该数据进行缓存就是不安全的。
冷缓存、缓存击穿 与 “一次 50 个请求”
冷缓存 指键尚不存在。如果 50 个请求同时命中同一个缺失的键,它们可能都会重新计算该值。这种现象称为 缓存击穿(cache stampede)或 雷鸣羊群(thundering herd)。
Django + Redis 为你提供了缓解手段:
with cache.lock(f"lock:user_profile:{user_id}", timeout=5):
data = cache.get(key)
if not data:
data = expensive_call()
cache.set(key, data, timeout=300)
缓存击穿是否 重要 取决于:
- 流量规模
- 重新计算的成本
- 后端负载容忍度
并非所有接口都需要加锁——但热点接口可能需要。
何时应该编写装饰器?
装饰器是一种 抽象。只有在它值得时才去实现。
在以下情况下构建装饰器:
- 你已经多次实现相同的模式。
- 缓存命中率始终很高。
- 延迟显著下降(例如,从秒级降到毫秒级)。
- 失效规则保持一致。
此时你可以说:
“我们已经看到这个模式成功运行了五次——把它抽取出来吧。”
受生产环境启发的事故(为何这很重要)
在许多生产系统中,仅使用 user_id 作为键来缓存仪表盘或用户特定的响应是一种常见模式。这些仪表盘通常包含来自账户状态的功能标记和权限信息。
如果用户的权限被降级,数据库会立即更新,但缓存可能仍然保存着旧的仪表盘数据。于是,在接下来的几分钟内,用户仍可能访问他们本不再拥有的功能。
问题不在于 Redis、Django 或 TTL 本身——而是缓存键未能完整编码授权上下文,而 TTL 让过期的数据保留的时间超过了可接受的范围。
此类场景说明了缓存如何放大细微错误:过时或作用域不正确的数据会迅速传播并变得更难检测,凸显了谨慎设计缓存的重要性。
测试和调试 Django 中的缓存代码
测试缓存正确性
def test_user_profile_cache_hit(mocker):
mocker.patch(
"django.core.cache.cache.get",
return_value={"id": 1}
)
result = get_user_profile(1)
assert result["id"] == 1
测试失效
def test_cache_invalidated_on_update(mocker):
delete = mocker.patch("django.core.cache.cache.delete")
update_user_profile(1, {"name": "New"})
delete.assert_called_with("user_profile:1")
在生产环境中调试
- 记录缓存命中/未命中。
- 跟踪命中率。
- 暂时降低 TTL 以暴露错误。
- 为每个环境添加前缀:
CACHE_KEY_PREFIX = "prod:"
一个看不见的缓存是隐患。
Final Takeaway
Django 和 django‑redis 让在应用中加入基于 Redis 的缓存看似非常容易,但它们有意地不为你做出艰难的决定。框架提供了可靠的原语——TTL、原子操作、锁和连接池——然而正确性、安全性和可维护性仍然完全取决于:
- 缓存键设计 – 编码正确的上下文,避免将不同的响应合并到同一个键。
- 失效策略 – 确保过期数据(尤其是授权决策)能够及时被移除。
- 测试 – 像对待正常路径一样,严格验证命中率、正确性和并发路径。
大多数真实场景下的缓存失败并不是因为 Redis 变慢或不可用,而是源于:
- 将不同的响应合并到同一个缓存键。
- 让过期的授权决策存活时间超过预期。
- 编码了与实际情况不同步的假设。
糟糕的缓存键设计既是 陈旧数据 bug 又是 安全 bug。
抽象(装饰器、通用助手)很诱人,但它们应该是经过验证后才引入的,而不是一开始就加入。除非你已经观察到真实的缓存命中率、验证了正确性,并且在实践中确认了延迟从秒级下降到毫秒级,否则请在服务层保持缓存逻辑显式,以便意图保持可见且可测试。等模式被证明可靠后,再安全地抽取出来。
安全的缓存需要纪律:
- 理解谁可以看到什么。
- 证明缓存数据随时间保持有效。
- 抵制因为“昂贵”就缓存所有东西的冲动。
- 像对待正常路径一样,仔细测试失效和并发路径。
如果思考周全,缓存会成为提升性能和弹性的强大工具;如果随意使用,它则会成为数据损坏和安全风险的隐形来源。Django 为你提供了工具——正确使用它们并非可选,而是必须。
进一步探索
- django‑redis –
- Django Caching Framework – Django 缓存框架
- Redis Caching Patterns – Redis 缓存模式
- Redis Distributed Locks – Redis 分布式锁
- Rethinking Caching in Web Apps (Martin Kleppmann) – 重新思考 Web 应用中的缓存(Martin Kleppmann)
- Cache Invalidation (Wikipedia) – 缓存失效(Wikipedia)
- Caching Patterns & Anti‑Patterns (High Scalability) – 缓存模式与反模式(High Scalability)