缓存策略指南

skillgohub.com 中文指南 | 中文版

缓存策略指南

一条没缓存的数据库查询,能让一次请求卡上 40 到 80 毫秒;乘上 40 个并发用户,就是一个要画三秒多的页面。多数团队其实不是在搭建缓存策略,而是把 Redis 硬怼到应用上、失效逻辑却一团糟,然后祈祷脏数据出现的频率还能忍。如果你修过一个 bug、随后整个周末都在听"老数据还在显示",你就已经知道这代价了。这篇指南会带你过一遍缓存层次、淘汰策略、以及真实部署里最容易踩的脏数据陷阱,给的是你可以拿来规划的数字,而不是口号。

为什么你的缓存策略总失败(以及好的长什么样)

缓存不是一套系统,而是一套堆栈,每一层有不同的延迟代价。浏览器缓存对 CSS、JavaScript、图片这类静态资源是亚毫秒级命中,但你只能通过响应头间接控制它;CDN(如国内常用的阿里云 CDN、腾讯云 CDN,或国外的 Cloudflare)在边缘节点供缓存响应,通常 10 毫秒内返回,前提是你正确设置了 Cache-Control 和 Vary;应用级缓存(典型是 Redis 或 Memcached)住在内存里,0.1 到 0.5 毫秒回,比磁盘查询快几十上百倍;最后的数据库也有自己的缓冲池和查询缓存可以调,但只能间接调。理解每一层的缓存特性,可以参考中文站的 数据分析基础,把数据结构与访问模式先理清。

Cache Strategies Guide - featured image

分层缓存地图:从浏览器到数据库

最常见的错误是把这些都当成可互换的。如果你的 HTML 是逐用户个性化的,CDN 缓存那个页面可能把一个访客的会话数据泄露给另一个请求;如果静态资源缓存一年但 CSS 包名从不改变,每次发版后用户都会看到布局错乱——这正是 Web 性能优化 英文指南要抓的失败类型。你需要对每种资源明确:哪一层该拥有它、它该活多久。静态、几乎不变的 HTML 交给 CDN,让它永不打扰你的源站;会话数据进 Redis,因为它需要快速随机访问和 TTL 清理;真正热的单实例状态可以放进程内以省去网络一跳,但要接受它不跨服务器共享。

Cache Strategies Guide comparison and review

能真正听话的 Cache-Control 响应头

大部分响应头 bug 并不隐蔽,而是自相矛盾。带 Cache-Control: no-cache 的响应并不是"不要缓存",而是"缓存但使用前先向源站验证"。单是搞混这一条,坑掉的部署就比任何算法选择都多。几个基础先记牢:带 public, max-age=31536000, immutable 给加了指纹、永不再变的静态资产(如哈希过的 CSS 和图片包);HTML 会变但校验便宜就上 no-cache;任何按用户区分的响应用 private,让共享缓存跳过它、浏览器缓存仍可用;用 s-maxage 给中间缓存单独设一个寿命,让 API 响应在 CDN 上比在用户浏览器里活得更久。当你换一个加过指纹的文件,URL 变了,浏览器会自动拉新版;不指纹化就只能靠 max-age 到期,于是每次发版都变成一句"清一下缓存就好"——如果你话里有这句,说明这段头你写错了。

Cache Strategies Guide step by step guide

淘汰策略对比:LRU、LFU、TTL 与滑动过期

内存缓存容量有限,真正的问题是谁先被淘汰。最近最少使用(LRU)是 Redis 和 Memcached 的默认,丢出最久没被碰过的条目,简单、适合脉冲式访问,但有盲区:一个被某位用户频繁引用的老条目可能占着位置,而其余一千个用户需要的是别的东西。最不常使用(LFU)按时间统计命中,保护真正热的键,代价是每条目多一点记录频率的内存;Redis 两者都支持,Memcached 只有带滑动阈值的 LRU。TTL 过期严格说不算淘汰策略,但团队常拿它当淘汰用,脏数据就是这么钻进来的。给商品价格设 300 秒 TTL,意味着价格变动最多要五分钟才传播到——看博客阅读数无所谓,库存就危险。更糟的是没有 TTL 的无界缓存:键今天是对的,后台任务一更新源表,所有读缓存的就整整一周读到旧数据。把 TTL 和淘汰策略配合,并让 TTL 短到"错过一次失效也会自行过期"。想深入理解这些访问与存储的取舍,可参考英文站的 数据库设计原则

Cache Strategies Guide cost and pricing analysis

缓存失效:没人想写的那部分

淘汰管的是内存压力,失效管的是正确性。主要有三种做法,你多半会组合使用。基于时间的失效就是 TTL,也是唯一能自愈的,所以再好的系统也会在底层压一道安全 TTL。基于事件的失效在源数据变化时删除或更新键,用数据库写钩子或 Redis Pub/Sub 消息实现,能把变更记录上的"过期服务时间"压到接近零。写穿(write-through)把缓存更新和源写放进同一个事务,保证一致但把写延迟翻倍,也让缓存写入进入你的事务规划。陷阱是只靠事件失效——如果你的失效消息丢了、进了队列或在下一次读之后才被处理,就会得到一个没有任何过期机制兜底的脏缓存。生产规则因此是:事件失效管大多数读命中的"快路径",短 TTL 做兜底。跳过 TTL 的团队几乎总会被坑一次,通常发生在数据迁移时。这些正确性与事务的取舍,可结合英文站的 数据库设计基础 一起理解。

Cache Strategies Guide tools and features overview

Redis vs Memcached vs CDN vs 进程内缓存

平台 / 工具核心能力定价
Redis内存数据存储;LRU/LFU 淘汰、Pub/Sub、TTL、持久化(RDB/AOF)、多数据类型开源免费;云免费档约 30MB,付费约 ¥100-200/月
Memcached纯 KV 内存缓存;多线程、极低开销、仅 LRU开源免费;云托管小节点约 ¥100-250/月
阿里云 / 腾讯云 CDN边缘缓存、Cache-Control、图片与 HTML 优化,免费额度可观免费档;付费按流量与套餐计费
Cloudflare CDN边缘缓存,Vary 支持,Purge API 强免费档;Pro 约 ¥150/月,Business 约 ¥1450/月
进程内缓存(如 Caffeine/gcache)内嵌应用内,近零网络延迟的近缓存开源免费
Varnish源站前的 HTTP 加速器,极高吞吐,VCL 逻辑开源免费;企业版按报价

选择的不是"谁最快",而是"每次请求能容忍哪个延迟预算"。多数团队会发现最大的优化,往往在缓存层之前就把多余写入去掉。你不确定哪种契合自己的流量曲线时,先估一个有用的问题:你的读有多少比例本来就可缓存?很多团队会发现自己缓存的恰恰是每次写都在失效的内容。

脏数据陷阱:热点缓存雪崩与未命中瀑布

缓存雪崩发生在热键到期、几百个并发请求一起未命中、然后同时砸向数据库。凌晨 3 点的冷库要 800 毫秒热身,看起来就是个慢端点。修复用"stale-while-revalidate"模式:立刻返回旧缓存值,同时让一个请求在后台刷新它。Redis 的 GETDEL 或简单的条件更新锁都行,关键是同一时间只允许一个进程重建。未命中瀑布更阴险:你加了个新查询,首次读就未命中,而因为内存被 LRU 封顶,它淘汰了一个更老的热条目,后者接着未命中又淘汰别的,几分钟内整个缓存翻了个个,所有请求都直奔数据库。在仪表盘上量你的 M/R(未命中比),并在正常工作日它爬到 10% 到 15% 左右时告警——爬升的未命中率通常是你工作集不再放得进内存的第一个信号。

像对待正经系统一样给你的缓存加观测

量不到就调不了,所以每个缓存层都暴露一小撮指标:命中率、未命中数、淘汰数、按命中/未命中拆分的平均延迟。多数托管 Redis 和 CDN 后台都有,你的应用也可以在每次读时用一两行日志记下命中/未命中分支。健康的应用缓存,对稳定热数据应保持在 90% 到 99% 的命中区间;读重负载低于 80%,要么工作集对内存太大,要么 TTL 对访问模式太短。还要把失效数和淘汰数分开记账:淘汰数高但命中率正常,只说明内存紧;失效数高而源稳定,说明你的事件钩子在对从不变化的行空跑。调优时一次只改一个变量、重跑压测、给 TTL 记一份变更日志。缓存调优是迭代的,最安全的"生产变更"是能在 staging 用真实负载而非合成基准测的 TTL。整个应用观测的手法,可延伸参考中文站的 AI 办公自动化技巧 里对流程监控的思路。

常见问题

"no-cache"和"max-age=0"有什么区别?

技术上两者都强制重新验证,但"no-cache"告诉缓存必须先与源站验证才能用存好的副本,而"max-age=0"意味着存量副本立即过期、同样必须重新验证。实践中多数浏览器处理得差不多,但如果代理只缓存携带"max-age=0"的响应,某些实现可能不重新验证就直接返回。对频繁变化的 HTML,最稳妥、最明确的是"no-cache",并配好 ETag 或 Last-Modified 验证头,让重新验证很便宜。

我已经用了事件失效,还要设 TTL 吗?

一定要。事件失效可能丢消息、迟到,或在迁移中被跳过,没有 TTL 你就无限期提供脏数据。一道短的安全 TTL——比如 60 到 300 秒,视你的用户多快需要变化而定——在"每个事件钩子都失败"时兜底,并给最大脏数据时长封顶。这是缓存里最靠谱的正确性规矩之一。

怎么防止一个用户的数据透过 CDN 缓存泄露出去?

任何因用户而异的响应都发"private" Cache-Control 头让共享缓存跳过,并且根本别把按用户区分的 HTML 放进 CDN 边缘。若必须缓存基础页,只对公开部分做片段缓存,个性化区块放到服务端或浏览器渲染,绝不要走 CDN。同时设"Vary: Cookie"并让基于 cookie 的键保持作用域收敛,因为一个忽略 Vary 头的共享缓存就是一场等着的隐私事故。

为什么每次发版后命中率都掉,哪怕我没改任何设置?

如果换了静态资源文件名或插入了新的缓存键,旧键不再被使用,LRU 策略就会淘汰旧条目为新访问模式腾位置。这种回落通常是临时的,但你可以发版后预热关键键来减缓,用电热脚本或在真实流量到达前把压测指向端点。如果发版几小时后命中率仍低,说明你的工作集比分配内存大,需要更多缓存或更长的 TTL。

📌 Pinterest 🐦 Twitter 📘 Facebook