扩展挑战 第3部分:Cache统治一切
Source: Dev.to
请提供您希望翻译的具体文本内容,我将为您翻译成简体中文。
它像所有虚假的黎明一样,以好消息开始。
Postgres Pete 很镇定。
团队在庆祝。有人做了个 meme。
但有点不对劲。
- 不是“应用宕机”那种坏事。只是…有点软绵绵。
- 已登录用户的延迟会出现峰值。
- 报表生成仍然占用了大量 CPU。
- 每当有人访问主页时,你的服务器本周已经第 400,000 次运行同样的查询。
你不再是慢了,而是浪费了资源。
欢迎来到第 3 章: 你的架构终于足够快,以至于可以显露出你到底重复做了多少工作。
战争故事:把一切都熔化的排行榜
几年前我们推出了一个游戏化活动:排行榜、徽章、每日奖励。
关键点?
我们在 每一次请求 时都重新计算排行榜。
- 每一次点击都会在数百万行数据上进行一次大规模的排序、连接和过滤。
- 刚上线时不算什么——也许每小时 100 名用户。
- 当它走红时?一天 90,000 次点击。
我们的数据库没有崩溃,但性能糟透了。加入一个 60 秒的 Redis 缓存 后,响应时间从 912 ms → 38 ms,查询负载下降了 99.7 %。
Postgres Pete 给我们写了一封感谢信。
第三章:缓存不是作弊
- 在小规模时,缓存是可选的。
- 在大规模时,缓存就是一切。
错觉是只要让数据库快、查询高效,你就没问题。
当 1,000 名用户访问同一路径 并且你生成 1,000 条相同的响应——恭喜,你把浪费优化到了极致。
缓存 是让你不再是快餐厨师……而成为拥有备餐站的厨师。
缓存层次
让我们按层而不是工具来拆解。
你不需要因为有人写了博客就使用 Redis。你需要为合适的工作选择合适的缓存类型。
1. 页面与片段缓存
| 何时使用 | 位置 |
|---|---|
| 对每个用户都不变的完整页面响应 | WordPress、SSR 框架、营销页面、未登录视图 |
| CDN 边缘缓存(Cloudflare、Fastly)用于静态资源 | 静态 HTML 快照、组件级别的片段缓存(Next.js getStaticProps 或 getServerSideProps 配合 revalidate) |
2. 查询结果缓存
| 何时使用 | 位置 |
|---|---|
| 返回可预测结果的高消耗查询 | 报表、排行榜、统计页面 |
| 将查询结果缓存到 Redis,30–300 秒 | 在关键数据变更时失效或更新;使用确定性的缓存键,例如 leaderboard:daily:2025-07-06 |
3. 对象缓存
| 何时使用 | 位置 |
|---|---|
| 访问频繁且不经常变更的实体 | 用户设置、价格表、内容元数据 |
| 首次访问时将对象加载到缓存 | TTL + 写穿/读穿模式;使用命名空间键(user:42:profile)避免污染 |
4. 边缘缓存 & CDN
| 何时使用 | 位置 |
|---|---|
| 静态资源、使用安全 GET 的 API、提升区域延迟 | Next.js、Shopify headless、任何全球提供静态内容的网站 |
| 示例 | GET /products?category=fitness → 缓存。POST /checkout → 不缓存。 |
| 失效 | 使用代理键(surrogate keys),例如 product:updated → purge /products |
Redis 示例:旁路缓存模式
// Basic cache-aside pattern
const getCachedUser = async (userId) => {
const cached = await redis.get(`user:${userId}`);
if (cached) return JSON.parse(cached);
const user = await db.users.findById(userId);
await redis.setex(`user:${userId}`, 300, JSON.stringify(user));
return user;
};
具体示例:主页延迟
| 指标 | 之前 | 之后 |
|---|---|---|
| 首页加载时间 | 680 ms | 112 ms |
| 在 DB & API 上的占比 | 90 % | — |
| Redis 命中率 | 12 % | 89 % |
| DB 查询减少 | — | 87 % |
仅缓存三个组件(精选产品、博客预览、客户评价)就产生了最大的影响。
缓存失效:被遗忘的缓存一半
写入缓存很容易。
正确地使缓存失效可以将成熟系统与抱有希望的实验区分开来。
+------------------------+
| Does the data |
| change often? |
+------------------------+
|
+----------------+----------------+
| | |
Yes No
| |
+--------------+ +--------------------+
| Can you hook | | Use long TTL with |
| into writes? | | fallback refresh |
+------+-------+ +--------------------+
|Yes
|
+--------------+
| Event-driven |
| invalidation |
+--------------+
|No
|
+--------------+
| Use low TTL |
| w/ polling |
+--------------+
缓存失效方法
基于时间(TTL)
- 易于理解。
- 容忍一定的陈旧性。
- 对仪表盘、统计、定价等场景“足够好”。
事件驱动
- 数据更新时使缓存失效。
- 在 ORM 中使用钩子或使用发布/订阅系统。
- 管理更困难,但更精准。
// In your product update handler:
await redis.del(`product:${product.id}`);
await redis.del(`category:${product.category}:featured`);
依赖追踪(高级)
- 追踪哪些数据驱动哪些缓存条目。
- 仅重建受影响的部分。
- 需要纪律性和工具支持(否则会自讨苦吃)。
标志您的缓存是否正常工作
| 健康的缓存 | 缓存出现问题 |
|---|---|
| 热路径的缓存命中率 > 80 % | 用户看到陈旧数据或不一致 |
| 在负载下首字节时间保持低位 | 命中率低;失效策略过于激进 |
| Redis/Memcached 使用可预测 | 缓存条目是巨大的二进制块 |
| 缓存冲突(应用 vs CDN) |
Cache Debugging: What to Watch
- Hit‑rate metrics (Redis
INFO stats→keyspace_hits / keyspace_misses)。 - Latency of cache reads vs DB reads。
- TTL distribution – 键是否过早过期或根本不失效?
- Invalidation logs – 确保每一次应该使键失效的写操作都真正执行了。
- Memory pressure – 关注
used_memory和驱逐策略。
TL;DR
- 在正确层级缓存(页面、查询、对象、边缘)。
- 为每种数据类型选择合适的 TTL 或事件驱动策略。
- 监控命中率和延迟;在数据陈旧或出现问题前进行调整。
通过有纪律的缓存策略,你可以将浪费的“够快”转变为真正高效、可扩展的性能。
# Cache Health Checklist
✅ 检查 INFO 统计信息
- 查找
keyspace_hits与keyspace_misses。
📈 记录慢速/未命中查询
- 标记缓存静默失败的路由。
⚡️ 检测缓存击穿
- 识别何时大量用户同时请求同一未缓存的项目。
- 考虑使用锁定或 stale‑while‑revalidate 策略。
⏰ 跟踪 TTL 过期
- 验证 TTL 是否与实际使用模式保持一致。
高级模式(当你准备好时)
Stale‑While‑Revalidate
- 立即提供过期数据。
- 在后台获取新数据并替换缓存中的内容。
- 减少用户等待时间和感知延迟。
- 实现方式:
- HTTP 头部:
Cache-Control: stale-while-revalidate=60 - 在应用程序中使用自定义中间件。
- HTTP 头部:
Soft TTL + Refresh
- 项目有 TTL,但在接近过期时在后台刷新。
- 防止冷启动并保持热点项目活跃。
- 适用于访问频繁但更新不频繁的数据。
- 实现方式:
- 异步作业队列
- 中间件钩子
Sharded or Namespaced Caches
- 使用键前缀来分隔缓存作用域。
- 示例:
tenant-42:user:profile、locale-en:settings
- 示例:
- 防止键冲突并简化批量失效。
- 采用结构化键命名约定,以支持未来的自动化。
缓存反模式
- 在全局缓存用户特定或敏感数据 – 正在酝酿的 GDPR 违规。
- 为每日变化的数据硬编码长 TTL – 快,但错误。
- 缓存所有内容而没有清除策略 – 你最终会得到一个第二个、未受管理的数据库。
TL;DR – 有意缓存
- 不要优化慢的东西 – 避免反复执行它们。
- 为正确的问题选择合适的缓存。
- 在上线前设计好你的缓存失效策略 在上线前。
- 监控 命中率,而不仅仅是缓存大小。
- 缓存不是作弊;它是系统扩展的方式。
- 你的应用不仅现在快——而且高效。
- 但不要放松太久……你需要开始将读写工作负载分离。
敬请期待。
接下来: 在不对你的灵魂进行分片的情况下扩展读取