自在学

我们与你共同进步

  • 分类课程
  • 文章
  • 工作台
  • 订阅

  • 关于我们
  • 隐私政策
  • 使用条款

探索

  • 分类课程
  • 文章
  • 工作台
  • 订阅

网站信息

  • 关于我们
  • 隐私政策
  • 使用条款

加入社区

自在学学习社区微信二维码

微信扫码,交流学习

株洲市自在学教育科技有限公司© 2025 - 2026 版权所有

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

湘公网安备43020302000292号|湘ICP备2025148919号-1
分类课程工作台文章订阅
分类课程工作台文章价格

Web后端基础原理

  1. 01HTTP 概览
  2. 02URL
  3. 03HTTP消息
  4. 04连接管理
  5. 05Web 服务器
  6. 06代理
  7. 07缓存
  8. 08集成点
  9. 09Web认证
  10. 10Secure HTTP
  11. 11HTTP实体
  12. 12国际化
  13. 13负载均衡
正在加载课程章节内容
课程编程Web后端基础原理缓存

明明已经发布了,用户为什么还在看旧页面:HTTP 缓存

你刚把首页标题从“春季活动”改成“夏季活动”,线上服务器也确认部署成功了。自己打开页面,看到的是新标题;同事刷新后,却仍然看到旧标题。更奇怪的是,他在开发者工具里明明能看到一次请求,响应体却是空的,页面照样完整显示了出来。

这通常不是服务器“随机回滚”,而是同一个 URL 的响应可能已经被放在多个位置:浏览器的私有缓存、公司代理、CDN 的共享缓存,甚至 Service Worker 自己管理的 Cache Storage。刷新时也不一定重新下载正文。缓存可能直接交出一份仍然新鲜的副本,也可能只拿着一个版本标识去服务器核对,得到 304 Not Modified 后继续使用本地正文。

所以,理解缓存不能只问“有没有缓存”。真正要问的是四件事:这份响应能不能存、谁可以存、存下后多久能直接使用、过期后怎样确认它还能不能用。

浏览器、共享缓存与源站之间的多层缓存路径

页面“没有重新下载”至少有三种可能:新鲜的 HTTP 缓存直接命中;过期副本通过 304 验证后被复用;浏览器从历史快照或 Service Worker 恢复页面。它们的处理规则不同,排查时不要混成一个“浏览器缓存问题”。


先把存储、复用和验证分开

缓存不是收到响应后就无条件保存,也不是保存后就永远可以直接返回。一次正常的 HTTP 缓存决策,可以拆成三个动作。

能不能存下来

缓存先检查请求方法、响应状态、认证信息和响应头。常见的 GET 响应最容易被缓存;200、301、404 等部分状态在满足条件时也可以缓存。POST 并非协议上绝对不能缓存,但需要明确的新鲜度信息,而且实际实现支持有限,业务代码不应默认它会命中缓存。

如果响应里有 Cache-Control: no-store,缓存不应存储这次请求和响应。这个指令针对的是“从现在起不要存”,并不负责删除同一 URL 以前已经存下的副本。

当前请求能不能复用这份副本

存下来了,不代表下一个请求一定能用。缓存会先找“匹配的缓存键”。最基本的缓存键来自请求方法和目标 URI;如果响应含有 Vary,还要比较被列出的请求头。比如一个 URL 会按 Accept-Language 返回中文或英文,缓存就必须把语言差异算进匹配条件。

找到候选副本后,还要判断它是新鲜还是过期。新鲜副本通常可以直接返回,源站甚至看不到这次请求;过期副本通常要验证,或在明确允许的过期窗口内暂时使用。

谁在复用

浏览器 HTTP 缓存是私有缓存,只服务于当前用户环境。代理和 CDN 是共享缓存,一份副本可能服务很多用户。两者都理解 HTTP 缓存字段,但承担的风险不同:浏览器存下一份个人资料,通常只是这个用户再次看到自己的资料;共享缓存误存个人资料,可能把甲的内容交给乙。

private 用来阻止共享缓存存储完整响应,但仍允许浏览器存储;public 明确表示共享缓存可以存储响应,即使它在其他条件下通常不会被共享缓存接受。这里的 public 不是“页面对公众开放”的权限声明,它只是在描述缓存资格。


Cache-Control 到底控制了什么

Cache-Control 看起来像一串逗号分隔的单词,其实每个指令回答的问题不同。把它们按“存储范围、直接使用时间、过期后的动作”分类,比背诵一长串定义更可靠。

控制存储与共享范围

  • no-store:不要存储这次请求和响应。适合密码找回链接、一次性令牌、极敏感账户数据。
  • private:完整响应只允许私有缓存存储,不能进入共享缓存。适合个性化页面。
  • public:允许共享缓存存储响应。它常用于明确开放共享缓存,但不能替代正确的授权设计。

控制多久可以直接使用

  • max-age=秒数:响应从生成或成功验证后,可以保持新鲜的时间。浏览器和共享缓存都能使用。
  • s-maxage=秒数:只对共享缓存生效,并覆盖共享缓存看到的 max-age 和 Expires。浏览器忽略它。它同时带有 proxy-revalidate 的约束:一旦过期,共享缓存必须成功验证后才能复用。
  • immutable:在响应仍然新鲜时,告诉客户端这个 URL 对应的内容不会变化,连用户刷新时的多余验证也可以省掉。它应和内容哈希 URL 一起使用,不要给会原地更新的文件贴上这个标签。
  • Expires:用绝对日期描述过期时间。若同时存在适用的 max-age,后者优先,因为相对时间更不容易受时钟差异影响。

控制过期后怎么办

  • no-cache:可以存,但每次复用前都必须先向源站成功验证。这个名字最容易骗人,它不是“不要缓存”。
  • must-revalidate:新鲜时仍可直接使用;一旦过期,必须成功验证后才能复用。无法联系源站时,也不能自行拿旧内容顶上。
  • proxy-revalidate:只约束共享缓存,含义与 must-revalidate 对应。
  • stale-while-revalidate=秒数:过期后的指定时间内,可以先返回旧副本,同时在后台更新。
  • stale-if-error=秒数:过期后的指定时间内,如果联系源站或验证时遇到错误,可以退回旧副本。

Cache-Control 指令分别控制存储范围、新鲜期与过期行为

指令组合不是从左到右执行

下面这行配置不是“先缓存一分钟,再公开,再验证”的程序:

http
Cache-Control: public, max-age=60, stale-while-revalidate=30

它表达的是一组同时成立的约束:浏览器和共享缓存的新鲜期都是 60 秒,支持相应扩展的缓存还可以在过期后的 30 秒内一边返回旧内容一边更新。

若指令冲突,不能靠书写顺序解决。缓存应采用更严格的含义。例如 max-age=600, no-cache 不能让响应直接使用十分钟,因为 no-cache 要求复用前验证。s-maxage 对共享缓存同时意味着过期后必须验证,因此不应再期待共享缓存执行 stale-while-revalidate 或 stale-if-error。重复、非法或彼此冲突的新鲜度值,也可能让实现把响应直接视为过期。

几组常用配置

内容建议起点实际效果
带内容哈希的 JS、CSS、图片public, max-age=31536000, immutableURL 不变期间长期直接命中;内容变化时换 URL
经常更新的 HTMLno-cache可以保存正文,但每次使用前验证,未变化时只传 304
可公开、允许短暂旧值的列表public, max-age=30, stale-while-revalidate=30浏览器与共享缓存短缓存,更新期间可以短暂返回旧内容
浏览器短缓存、CDN 较长缓存的列表public, max-age=30, s-maxage=300浏览器新鲜 30 秒,共享缓存新鲜 300 秒,过期后必须验证
个性化但不高度敏感的页面private, no-cache只在私有缓存保存,并在复用前验证
一次性令牌或极敏感响应private, no-store不允许 HTTP 缓存存储这次响应

这些只是起点。库存、价格、权限变化能容忍几秒延迟,策略就可以更积极;支付结果、权限校验、一次性凭证不能接受旧值,就应牺牲一部分缓存收益。


新鲜不等于源站没有变化

假设浏览器最终收到:

http
HTTP/1.1 200 OK
Date: Fri, 14 Aug 2026 04:00:00 GMT
Cache-Control: public, max-age=120, s-maxage=600
Age: 90
Content-Type: text/html; charset=utf-8

浏览器看见 max-age=120,共享缓存看见 s-maxage=600。Age: 90 表示这份响应从源站生成或验证后,估计已经过去 90 秒;它不是“刚到浏览器,所以年龄从零开始”。经过多层缓存时,之前驻留的时间和网络传输时间都要算进去。

缓存的核心判断可以简化为:

text
新鲜 = 当前年龄 < 新鲜寿命

“新鲜”只表示服务器承诺的直接复用时间还没用完,不表示缓存知道源站此刻有没有改。服务器若在第 10 秒改了内容,而浏览器拿到的副本仍有 110 秒新鲜期,浏览器完全可能继续展示旧内容。这不是缓存违反配置,恰恰是配置在正常工作。

新鲜寿命的优先级

缓存确定新鲜寿命时,按以下顺序选第一个适用值:

  1. 对共享缓存,优先使用 s-maxage。
  2. 否则使用 max-age。
  3. 否则使用 Expires 与 Date 的差值。
  4. 都没有时,符合条件的缓存才可能估算一个启发式新鲜期。

响应年龄沿浏览器、CDN 与源站路径累积的时间线

启发式缓存为什么会制造意外

没有 Cache-Control 不等于“肯定不缓存”。对于本身允许启发式缓存的响应,一些缓存会参考 Last-Modified 推测内容能保持多久。常见实现可能取“从最后修改到现在这段时间”的一小部分,例如约一成,但这个比例不是协议规定的统一算法。

这会带来一个很不舒服的结果:你没写缓存策略,系统却替你猜了一套,而且浏览器、代理、CDN 的猜法未必一致。对更新节奏有要求的内容,应该明确发送 Cache-Control,不要把一致性预算交给启发式算法。


过期后为什么没有重新下载正文

缓存过期只说明“不能再不问服务器就直接用”,并不等于旧正文已经作废。如果服务器给过验证器,缓存可以发一个条件请求:先问版本是否变化,再决定要不要下载完整内容。

ETag 与 If-None-Match

ETag 是服务器为某个资源表示生成的版本标识。它是一个不透明字符串,客户端不应猜它是哈希、数据库版本号还是发布时间。

第一次响应可能是:

http
HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "article-8f32"
Content-Type: text/html; charset=utf-8
Content-Length: 42810

下次复用前,浏览器带上旧标识:

http
GET /article HTTP/1.1
Host: example.test
If-None-Match: "article-8f32"

内容没变时,服务器只回答:

http
HTTP/1.1 304 Not Modified
Cache-Control: no-cache
ETag: "article-8f32"

304 没有消息正文。浏览器把缓存里的 42810 字节正文拿出来,结合这次响应更新相关元数据,然后显示页面。这就是“Network 面板里有请求,却根本没下载正文”的常见原因。

Last-Modified 与 If-Modified-Since

Last-Modified 用资源的修改时间充当验证器。缓存会在条件请求里发送 If-Modified-Since。如果资源在该时间之后没有修改,服务器也可以返回 304。

它容易生成,但时间精度有限。一个资源在同一秒内连续变化,或者多台服务器的修改时间不一致,都可能让判断不够可靠。ETag 可以直接表达业务版本,通常更精确。

服务器可以同时提供两者。如果请求同时带有 If-None-Match 和 If-Modified-Since,缓存验证应优先依据 If-None-Match;不能在 ETag 已经变化时,又因为修改时间没变而错误返回 304。

强 ETag 与弱 ETag

强 ETag 表示两个表示在字节层面一致;弱 ETag 以 W/ 开头,表示语义等价但字节未必完全相同。缓存用 If-None-Match 验证是否需要重新传输时,可以使用弱比较。断点续传、并发更新保护等要求字节级一致性的场景,则不能把弱 ETag 当作强验证器。

ETag 条件请求返回 304 或 200 的完整分支

一个可以直接运行的验证服务器

下面的示例只使用 Node.js 内置模块。它按 Accept-Language 生成中文或英文表示,发送对应的 ETag,并正确处理两个验证器的优先级。

js
// cache-server.mjs
import http from 'node:http';
import { createHash } from 'node:crypto';
 
const port = 3000;
const lastModified = new Date('2026-08-14T04:00:00Z');
 
function createETag(body) {
  const digest = createHash('sha256').update(body).digest('hex').slice(0, 16);
  return `"${digest}"`;
}
 
function weakTagValue(tag) {
  return tag.trim().replace(/^W\//, '');
}
 
function ifNoneMatchMatches(headerValue, currentETag) {
  if (!headerValue) return false;
  if (headerValue.trim() === '*') return true;
 
  return headerValue
    .split(',')
    .map(weakTagValue)
    .includes(weakTagValue(currentETag));
}
 
function isNotModified(request, currentETag) {
  const ifNoneMatch = request.headers['if-none-match'];
 
  // 两者同时存在时,只依据 If-None-Match 判断缓存验证。
  if (ifNoneMatch !== undefined) {
    return ifNoneMatchMatches(ifNoneMatch, currentETag);
  }
 
  const ifModifiedSince = request.headers['if-modified-since'];
  if (!ifModifiedSince) return false;
 
  const timestamp = Date.parse(ifModifiedSince);
  return Number.isFinite(timestamp) && lastModified.getTime() <= timestamp;
}
 
const server = http.createServer((request, response) => {
  if (request.method !== 'GET' || request.url !== '/article') {
    response.writeHead(404, {
      'Content-Type': 'text/plain; charset=utf-8',
      'Cache-Control': 'no-store',
    });
    response.end('页面不存在');
    return;
  }
 
  const acceptsChinese = request.headers['accept-language']?.includes('zh');
  const language = acceptsChinese ? 'zh-CN' : 'en';
  const body = acceptsChinese
    ? '<!doctype html><meta charset="utf-8"><h1>缓存验证成功</h1>'
    : '<!doctype html><meta charset="utf-8"><h1>Cache validation works</h1>';
  const etag = createETag(body);
  const headers = {
    'Cache-Control': 'public, max-age=10, must-revalidate',
    'Content-Language': language,
    'Last-Modified': lastModified.toUTCString(),
    ETag: etag,
    Vary: 'Accept-Language',
  };
 
  if (isNotModified(request, etag)) {
    response.writeHead(304, headers);
    response.end();
    return;
  }
 
  response.writeHead(200, {
    ...headers,
    'Content-Type': 'text/html; charset=utf-8',
    'Content-Length': Buffer.byteLength(body),
  });
  response.end(body);
});
 
server.listen(port, () => {
  console.log(`缓存实验服务器已启动:http://127.0.0.1:${port}/article`);
});

保存为 cache-server.mjs 后运行:

bash
node cache-server.mjs
 
curl -i \
  -H 'Accept-Language: zh-CN' \
  http://127.0.0.1:3000/article
 
curl -i \
  -H 'Accept-Language: zh-CN' \
  -H 'If-None-Match: "把第一次响应中的 ETag 填在这里"' \
  http://127.0.0.1:3000/article

第二次请求的 ETag 匹配时会得到 304。把语言改为 en 后,正文和 ETag 都会变化;这也正好引出下一个容易漏掉的字段。


Vary:同一个 URL 不一定是一份内容

假设 /article 会读取 Accept-Language。第一位用户请求中文,CDN 缓存了中文响应;第二位用户请求英文。如果缓存键里只有 URL,第二位用户也会收到中文。

服务器需要明确告诉缓存,选择响应时用到了哪些请求头:

http
Vary: Accept-Language

从此,缓存会把不同的语言请求看成不同变体。压缩通常对应 Vary: Accept-Encoding,跨域响应如果随请求的 Origin 改变,则需要正确处理 Vary: Origin。

Vary 按语言和压缩格式拆分同一 URL 的缓存键

Vary 解决的是表示选择,不是访问控制

把 Cookie 放进 Vary,理论上能按整段 Cookie 区分缓存条目,实践中却很容易制造海量缓存键,让命中率快速下降。更严重的是,只要响应逻辑还有一个遗漏的个性化维度,就可能串数据。

用户资料、购物车、账户页面应优先使用 Cache-Control: private 阻止共享缓存,而不是试图用 Vary: Cookie 枚举每位用户。授权仍然必须在后端执行,缓存键不能充当权限系统。

Vary: * 表示响应取决于无法用一组请求头描述的因素。这样的已存响应不能在不验证的情况下匹配后续请求,通常会让缓存收益接近消失。Vary 列出的字段越多,变体数量也越多;只列真正影响响应内容的字段。


过期内容也可以有可控价值

我们通常把“旧内容”理解成错误,但现实里还要问一句:旧多久,发生在什么业务里?一篇文章晚 20 秒更新,可能比让所有读者等待源站恢复更好;库存和支付状态晚 20 秒,就可能直接造成业务事故。

stale-while-revalidate:先快,再更新

http
Cache-Control: public, max-age=60, stale-while-revalidate=30

前 60 秒,缓存副本是新鲜的。接下来的 30 秒内,支持该指令的缓存可以立即返回旧副本,并在后台发起验证。后台更新完成后,后续请求再得到新版本。

代价很明确:用户可能在 30 秒窗口内看到旧内容。好处是第一个撞上过期时刻的请求不用承担完整回源延迟,热门资源也不容易在同一秒把大量验证请求压到源站。缓存实现还可能合并同时到来的验证请求,但应用不能假定所有产品都会用完全相同的方式处理并发更新。

stale-if-error:新鲜度换可用性

http
Cache-Control: public, max-age=60, stale-if-error=86400

副本过期后,如果回源遇到网络故障或服务器错误,支持该指令的缓存可以在允许的 86400 秒窗口内交付旧副本。它很适合文档、公共内容页和变化不频繁的目录数据。

这不是“发生任何问题都吞掉”。不同缓存产品对错误类型和后台更新方式的支持可能不同。协议层面,must-revalidate、proxy-revalidate 以及适用于共享缓存的 s-maxage 都明确禁止过期副本在未成功验证时复用,因此不要把它们和 stale 指令配在一起期待“严格验证”和“返回旧内容”同时发生。部署前还要按实际 CDN 和代理行为测试,不能只看响应头觉得配置已经生效。

新鲜期、后台更新窗口与故障兜底窗口的时间轴

权限、余额、库存扣减、支付状态等内容不适合随意使用过期副本。缓存提高可用性的代价,是主动接受一段时间的不一致。先写清业务能容忍的最长旧值,再决定 stale 窗口,而不是反过来从某个常见配置抄一个秒数。


内容更新后,怎样让缓存主动让路

最棘手的情况是:资源还在新鲜期内,服务器却必须立刻发布新版本。对已经进入用户浏览器的响应,源站无法主动敲门要求删除。解决办法通常分成两类。

可变 URL:缩短缓存并提供验证器

HTML、API 列表这类 URL 通常要原地更新。它们适合短 max-age,或使用 no-cache 搭配 ETag、Last-Modified。这样每次使用前可以确认版本,内容没变时只付出一次往返和少量头部,内容变了才下载完整响应。

CDN 可以提供 purge 或 invalidate 接口,主动删除边缘节点中的缓存条目。但一次清理是否覆盖所有区域、所有变体和所有缓存层,取决于产品配置。更重要的是,CDN 清理通常触碰不到用户浏览器里仍然新鲜的副本。

不可变 URL:内容变化就换地址

构建产物更适合把内容哈希放进文件名:

text
/assets/app.4f83c1a2.js
/assets/site.91b76e40.css

这些 URL 一旦发布就不再原地修改,可以放心使用:

http
Cache-Control: public, max-age=31536000, immutable

新版本上线时,HTML 改为引用新的哈希 URL。浏览器看到的是一个从未请求过的地址,自然会下载新文件;旧文件继续留在缓存里也不会污染新页面。

带内容哈希的静态资源发布与旧版本共存流程

发布顺序会决定是否出现白屏

哈希 URL 解决了失效问题,也带来部署约束。正确顺序通常是先上传新静态资源,再发布引用这些资源的新 HTML,并让旧资源保留一段时间。若先发布 HTML,边缘节点拿到新页面时,新 JS 可能还不存在;若部署完成后立刻删除旧资源,仍缓存着旧 HTML 的用户会请求已经消失的文件。

查询参数也能形成不同 URI,例如 /app.js?v=42,但中间缓存是否忽略、保留或规范化查询参数可能受产品策略影响。内容哈希文件名更容易看出“这个 URL 是否不可变”,也更适合自动化构建。

一次成功的 POST、PUT 或 DELETE 可能让处理该请求的缓存失效相关 URI,但它不会自动广播到所有浏览器、所有 CDN 节点和应用内部缓存。业务上的“数据已更新”,不能等同于“全球所有副本已删除”。


敏感内容先守住共享边界

缓存事故里最严重的通常不是“页面旧了”,而是“用户看到了别人的页面”。这种问题常出现在 CDN 被强制缓存所有 200 响应,或者后端把个性化响应错误标成 public。

Cookie 不是禁止共享缓存的开关

响应里有 Set-Cookie,请求里有 Cookie,都不应被当成可靠的“自动私有化”策略。不同缓存产品还能通过规则覆盖源站头部。只要响应与用户身份有关,就明确发送 private;如果内容不应落入 HTTP 缓存,再加 no-store。

http
Cache-Control: private, no-store

no-store 也不是完整的安全方案。它不能撤回已经泄露的响应,不能替代 HTTPS,不能修复日志里记录的令牌,也不能阻止应用代码把数据写进其他存储。缓存策略只负责缓存边界内的行为。

用白名单决定哪些动态响应进入 CDN

与其默认缓存所有动态路由,再逐个排除敏感页面,更稳妥的做法是只允许经过审查的公共路由进入共享缓存。审查时至少确认:响应是否随身份变化、是否包含权限相关数据、缓存键是否覆盖所有表示维度、旧值最多能容忍多久、出错时是否允许返回旧副本。

如果 CDN 规则会覆盖 Cache-Control,就把规则也纳入代码评审和发布检查。源站响应头正确,不代表边缘最终执行的策略一定正确。


调试时别只按刷新键

缓存问题之所以难查,是因为一次请求可能在浏览器、Service Worker、公司代理、CDN 或源站中的任何一层结束。按下面的顺序观察,通常比反复清空一切更快找到责任层。

先确定响应来自哪里

打开浏览器 Network 面板,保留日志并重新访问目标 URL。观察状态码、Transferred Size 和响应来源提示:

  • 没有网络请求记录,可能是 HTTP 缓存直接命中,也可能是历史页面恢复;继续检查页面导航类型和 Service Worker。
  • 显示来自 memory cache 或 disk cache,说明浏览器直接使用了本地副本。
  • 出现 304,说明请求到达了某个能够验证的服务器,但正文来自本地缓存。
  • 出现 200 也不能证明到达源站,CDN 完全可以用缓存副本返回 200。

开发者工具里的“Disable cache”通常只在工具打开时禁用浏览器 HTTP 缓存,用它可以模拟首次访问。但它不会替你删除 CDN 副本,也不能自动绕过 Service Worker 自己的缓存逻辑。

沿链路检查响应头

先看这些标准字段:

text
Cache-Control
Age
Date
ETag
Last-Modified
Vary
Expires
Via

再看 CDN 自己的命中状态头和请求追踪 ID。不同供应商的字段名不一样,但通常会告诉你是 HIT、MISS、BYPASS、STALE 还是正在更新。

可以用命令行把浏览器因素暂时移开:

bash
curl -i http://127.0.0.1:3000/article
 
curl -i \
  -H 'Cache-Control: no-cache' \
  http://127.0.0.1:3000/article
 
curl -i \
  -H 'If-None-Match: "article-8f32"' \
  http://127.0.0.1:3000/article

请求中的 Cache-Control: no-cache 表示客户端希望沿途缓存先成功验证再回答,但中间产品配置仍可能有自己的约束。若要定位 CDN,可以对比正常域名、明确的源站测试入口以及不同节点的响应;不要在生产环境随意暴露可绕过网关的源站地址。

把几个“缓存”分开清理

浏览器 HTTP 缓存、Service Worker Cache Storage、历史快照、应用内数据缓存和 CDN 缓存是不同系统。清空浏览器缓存后问题仍在,说明线索可能在更上游;禁用 CDN 后仍旧出现,可能是 Service Worker 或应用状态。

强制刷新适合诊断,不适合成为发布流程。一个版本必须依靠用户“多按几次刷新”才能生效,说明 URL 版本化、缓存头或失效流程仍有缺口。


把缓存看成一份一致性预算

缓存真正做的事情,是用一段可接受的不一致换取更少的延迟、带宽和源站压力。max-age 决定你愿意多久不问源站,验证器决定过期后能否只核对版本,Vary 决定哪些请求可以共享副本,stale 指令决定源站变慢或出错时能否暂时相信旧值。

设计时可以从一个具体问题开始:如果这个响应旧了 10 秒,最坏会发生什么?答案只是文章晚更新,就可以大胆利用共享缓存;答案涉及越权、超卖或重复支付,就应缩短甚至取消复用窗口。

当浏览器、CDN、网关和应用缓存一起出现时,每一层都成了系统的集成点。下一步要关注的已经不只是“这一层能不能命中”,而是这些组件之间如何传递身份、版本、错误和失效信号。缓存配置最终是否可靠,就取决于这些边界能不能对得上。

上一章代理下一章集成点