缓存:三层、86% 命中与一次 11 分钟的旧数据

权限改完十一分钟仍看到旧列表:浏览器六十秒、CDN 三百秒、应用三百秒叠成六百六十秒;静态七成二命中九成八、动态两成八命中五成五,合出八成六。本文把三条写后失效规则、每月八万七千次回源与四小时投入逐项算清,并给出八条可演练的验收动作。

缓存省下的每一次回源,都可能是一次旧数据。

一、权限改完,十一分钟还看到旧列表

周一上午十点,陈默把一位外包成员从项目里移除。对方刷新页面,项目还在;十分钟后再刷新,还在。他打开数据库确认权限行已经删除,又去 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% 更难被话术绕过去。

什么时候该停下来。 当你开始为缓存写第二套失效逻辑,或者为「万一没删掉」再加一层定时扫描时,停下来——一个人维护不了两套,第二套会在半年内失修。

关键参考与核验范围

以上链接于 2026-10-26 实测可公开访问;未核验版本、功能范围与政策更新。核验日期:2026-10-26。

范围上只讨论浏览器、CDN 与应用三层的 TTL 与写后失效,不讨论 Redis 集群拓扑、数据库物化视图与厂商计价。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元