缓存省下的每一次回源,都可能是一次旧数据。
一、权限改完,十一分钟还看到旧列表
周一上午十点,陈默把一位外包成员从项目里移除。对方刷新页面,项目还在;十分钟后再刷新,还在。他打开数据库确认权限行已经删除,又去 CDN 控制台点了一次刷新,列表才消失。从操作到真正看不见,中间隔了十一分钟。
以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。
他先怀疑删除语句写错了,把 SQL 打印出来逐行看,没有问题;又怀疑对方没有刷新,让对方截了图,页面确实是旧的。最后他把三处配置摆在一起,才发现问题不在任何一处,而在三处叠在一起:浏览器缓存六十秒,CDN 缓存三百秒,应用缓存三百秒。
三层不是并联,而是串联。用户在浏览器拿到旧响应,就不会去问 CDN;CDN 还在有效期内,就不会回源;应用里的旧结果因此一直没有机会被替换。最坏情况下,三段等待相加,正好是十一分钟。
缓存不是一层,是三段等待的串联。 只清一层,用户看到的仍然是旧数据;只盯一个 TTL,另外两层会把省下的时间还回去。
命中率是平均数,旧数据是分布。 86% 的命中说明大多数请求很快,却不说明被命中的那条数据有多旧。权限、成员、项目状态这类字段,旧一分钟和旧十分钟是两种事故。
写后失效的难点不在删,而在删得全。 删除一个 key 是代码问题,删掉所有由这次写入派生出来的 key 是设计问题;后者需要在写入之前就想清楚。
他后来把这次事故记成一句话:能把旧数据删掉的人,和知道该删哪些 key 的人,往往不是同一个人。加缓存的是他,改权限的是他,配 CDN 的也是他;三层都经手过,却没有一张纸写清一次写入要动哪几层。
二、三层缓存与三个 TTL
这套缓存是逐步加上的:先给静态资源加 CDN,再给列表接口加应用缓存,最后为了减少重复请求,给 HTML 加了浏览器缓存。三层各自都合理,合在一起却没有一份统一的失效清单。
| 层 | 存什么 | TTL | 失效方式 |
|---|---|---|---|
| 浏览器 | 列表页 HTML 与首屏资源 | 60 s | 过期后重新验证,或换 URL |
| CDN | 匿名页面、静态资源与公共片段 | 300 s | 按 URL 或标签 purge |
| 应用 | 查询结果与权限片段 | 300 s | 写路径删除对应 key |
三层里,浏览器最难控制。应用缓存可以一次删除,CDN 可以调用接口 purge,浏览器里的副本只能等六十秒过期,或者让 URL 发生变化。所以「写后立即生效」这句话,在浏览器层从来不是真的。
TTL 不是越长越省。 把应用缓存从三百秒加到六百秒,旧数据窗口就从三百秒变成六百秒,最坏窗口多出五分钟;省下的是数据库时间,付出的是一次权限事故的排查时间。
CDN 只该缓存公共内容。 登录后的列表如果按用户缓存,必须在 key 里带用户或组织标识,并且不能被公共缓存命中;否则省下的不是回源,是一次越权。
应用缓存的 key 要能批量匹配。 用 org:42:projects:list 这样的前缀,权限变更时可以一次清掉该组织的列表、详情与计数;如果 key 是散落的哈希,失效就只能靠猜。
TTL 要写在配置里,不要散在代码里。 三个数字写在同一个配置文件,才有可能被一次检查发现;散在三个文件里的六十、三百、三百,迟早会有一个被改成三千。
这张表还漏了一列:谁能在故障时删掉它。浏览器那一层,陈默删不了;CDN 那一层,他要在控制台点;应用那一层,他只能重启。三层里只有应用缓存的失效是他能写进代码的,所以写后失效必须先从这里开始。
三、86% 的算式与三条写后失效规则
改完后的一个月,月请求是 62 万次。按静态与动态拆开,静态占 72%、单层命中 98%,动态占 28%、单层命中 55%,合起来是 86%:72% × 98% + 28% × 55% = 85.96%。
| 流量类型 | 占比 | 单层命中率 | 月命中次数 |
|---|---|---|---|
| 静态资源与匿名页 | 72% | 98% | 437,472 |
| 登录后的动态片段 | 28% | 55% | 95,480 |
| 合计 | 100% | 86% | 532,952 |
这个数字好看,但它掩盖了一个事实:动态片段的命中率只有 55%,而权限、成员、项目状态恰好都在动态片段里。缓存真正要防的事故,几乎都发生在剩下的 45% 里。
规则一:先写数据库,再删缓存。 反过来做,写库完成前的并发读会把旧值重新填回缓存,删除等于白做。删除失败要进重试队列并告警,不能吞掉。
规则二:失效按业务前缀批量做。 权限变更影响的是整个组织的列表与详情,不是某一个 URL;一次写入要清 org:{id}:*,而不是让运维逐个页面点刷新。
规则三:旧数据不可接受的路径直接 bypass。 设置页、权限校验、导出预览不进 CDN,浏览器也不缓存;这些路径的请求量只占少数,回源的成本远低于一次越权。
三条规则里,第一条是顺序,第二条是范围,第三条是例外。顺序错了会复活旧值,范围小了会漏 key,例外没设会把最不能旧的数据缓存得最久。
判断一条路径该不该缓存,不要问「它读得多不多」,要问「它旧一分钟会不会有人受损」。读得多但旧一分钟无害的,可以缓存十分钟;读得少但旧一分钟会越权的,一秒都不该缓存。
四、最坏 660 秒与四小时
把三层 TTL 相加,60 + 300 + 300 = 660 秒,也就是十一分钟。这不是说每次写入都要等十一分钟,而是说在浏览器、CDN、应用三层都刚好命中旧副本时,用户最长会看到十一分钟前的数据。
复算:
单层 TTL:浏览器 60 s + CDN 300 s + 应用 300 s
最坏陈旧窗口:60 + 300 + 300 = 660 s
换算分钟:660 ÷ 60 = 11 分钟
只删应用缓存:60 + 300 = 360 s = 6 分钟
再 purge CDN:60 s = 1 分钟
浏览器剩余:60 s(只能等过期或换 URL)
静态命中:620,000 × 72% × 98% = 437,472 次/月
动态命中:620,000 × 28% × 55% = 95,480 次/月
命中算式:72% × 98% + 28% × 55% = 70.56% + 15.4% = 85.96% ≈ 86%
命中合计:437,472 + 95,480 = 532,952 次/月
命中率:532,952 ÷ 620,000 = 85.96% ≈ 86%
回源请求:620,000 − 532,952 = 87,048 次/月(14.04%)
缓存投入:4 h
占全年工时:4 ÷ 1,920 = 0.21%
占性能预算:4 ÷ 40 = 10%
这个复算块里最有用的一行不是 660,而是 360:只要写路径同步删掉应用缓存,最坏窗口就从十一分钟降到六分钟;再成功 purge CDN,就只剩浏览器的六十秒。三层里,应用层离数据最近,也最容易删干净。
四小时的投入这样分:两小时写失效路径,一小时把三个 TTL 收进配置,一小时做一次「移除成员」演练。前两小时决定正确性,后两小时决定这件事会不会在三个月后失效。
演练要记录三个时间点:提交权限变更、应用缓存删除成功、CDN purge 返回成功。三个时间点之间的差值,才是这次写入真正的影响窗口;只看最后一个,会把前面两层漏掉。
浏览器那六十秒要用版本号解决,不要用更短的 TTL 解决。 把 max-age 降到零,回源会立刻涨上来;给 HTML 加内容摘要或版本参数,URL 一变,浏览器自然取新副本。
回源率要按月看,不要按天看。 每天的波动会被发布和爬虫放大;月度回源率稳定在 14% 左右,才说明三层缓存都在按预期工作。
缓存投入的回报不在服务器,而在事故。 四小时换不来多少 CPU,它换来的是「权限改完,对方刷新就看不见」这个确定性;没有这个确定性,每次权限变更都要人工去 CDN 点一次刷新。
五、行动清单与复算验收
- 给三层各写一条 TTL,把 60 / 300 / 300 收进同一个配置文件。
- 列出所有写路径,统一成「先写库、再删缓存」,删除失败进重试队列。
- 给缓存 key 加组织前缀,权限变更时按
org:{id}:*批量失效。 - 设置页、权限校验与导出预览标记为 bypass,不进 CDN、浏览器不缓存。
- 给列表页 HTML 加内容摘要或版本参数,不依赖六十秒浏览器 TTL。
- 在监控里分静态与动态记录回源率,动态回源率高于 50% 时告警。
- 做一次「移除成员」演练,记录从提交到对方看不见的实际秒数。
- 每月抽查一次 CDN 规则,确认没有把登录后页面缓存成公共内容。
验收标准:移除成员后最坏十一分钟内对方必然看不到该项目,月度命中率保持在 86% 左右(不低于 85.9%)、回源请求不超过 8.8 万次。
清单的顺序就是实施顺序:先删应用缓存,再 purge CDN,最后处理浏览器。反过来先改浏览器 TTL,只会让回源先涨起来,而旧数据窗口几乎不变。
八条里前三条是正确性,后五条是可持续性。只做前三条,这次事故不会再来;做完后五条,下一次加缓存的人才不会把顺序又写反。
验收时不要只看命中率。命中率高但动态片段命中率低,说明缓存主要命中的是图片和 CSS,真正贵的查询仍然每次回源;两个数字要一起看。
使用边界与实操验收
缓存不能替代索引。 如果一条查询本身要八秒,缓存只是把八秒挪到第一次;先做查询与索引,再谈缓存,否则缓存失效的瞬间会把数据库打回原形。
权限数据不适合长 TTL。 成员、角色、可见范围这类字段,宁可多回源几次;把它们缓存十分钟省下的数据库时间,抵不上一次越权排查。
单实例部署不要同时上进程内缓存与 Redis。 两者的差别不在命中率,而在重启后还在不在;先选一个,把失效逻辑写对,再考虑第二层。
与 CDN 厂商的边界要写清。 本文只讨论 TTL 与失效规则,不讨论套餐、流量计价与节点分布;那些属于账单与选型问题,换了厂商规则仍然成立。
缓存命中率不是验收指标,回源请求数才是。 命中率会随着流量结构变化,回源数直接对应数据库压力;把 87,048 次/月写进验收,比写 86% 更难被话术绕过去。
什么时候该停下来。 当你开始为缓存写第二套失效逻辑,或者为「万一没删掉」再加一层定时扫描时,停下来——一个人维护不了两套,第二套会在半年内失修。
关键参考与核验范围
- MDN: HTTP caching :用于核对浏览器与共享缓存的层次划分。
- MDN: Cache-Control
:用于核对
max-age、private与no-store的语义。 - RFC 9111: HTTP Caching :用于核对新鲜度、过期与重新验证的规范定义。
- Cloudflare: Cache-Control :用于核对边缘缓存对源站响应头的遵循方式。
以上链接于 2026-10-26 实测可公开访问;未核验版本、功能范围与政策更新。核验日期:2026-10-26。
范围上只讨论浏览器、CDN 与应用三层的 TTL 与写后失效,不讨论 Redis 集群拓扑、数据库物化视图与厂商计价。
系列导航
- 上一篇:数据库:一千二百次查询、三个索引与 8 秒的列表页
- 下一篇:后台任务:34 秒的同步导出与三次重复文件
- 回到目录:一个人的性能与账单:4.2 秒的首屏、一千二百次查询与每月 210 美元
- 相关阅读:云账单:210 美元的去处与每千次请求 0.42
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。