await caches.default.put(request, response) 执行成功,不意味着全球每个边缘节点都拥有这个响应。Cloudflare Workers Cache API 的对象范围与通过 fetch() 使用的 CDN 缓存机制不同。忽略这个区别,会让开发者把跨地区未命中误诊为 TTL 失效。
本文讨论公开、可共享的响应,不用于账户数据。具体缓存资格、计费和产品能力会随配置变化,应查阅当前账户与官方文档,而不是假设任意响应都能缓存。
明确自己控制的缓存层
请求进入 Worker 后,可以让 fetch() 请求上游并使用平台缓存规则,也可以手动查询 Cache API,自己决定何时写入。后者给应用更直接的控制,但同时需要应用处理键、过期策略、异常和失效。
if (request.method !== 'GET'
|| request.headers.has('Authorization')
|| request.headers.has('Cookie')) {
return fetch(request);
}
const cache = caches.default;
const key = new Request(new URL(request.url), { method: 'GET' });
const cached = await cache.match(key);
if (cached) return cached;
const response = await fetch(request);
if (response.ok && !response.headers.has('Set-Cookie')) {
const stored = new Response(response.body, response);
stored.headers.set('Cache-Control', 'public, max-age=60');
ctx.waitUntil(cache.put(key, stored.clone()));
return stored;
}
return response;
这是语义示例,不是可直接复用的通用中间件。生产实现还必须排除身份信息、Set-Cookie、不适合缓存的内容和受条件请求影响的情况。示例将返回响应也标记为公开缓存,如果浏览器不应缓存同样时间,应分别设置返回与存储策略。
假设用户请求 Worker,Worker 查询 Cache API,未命中后用 fetch 访问原站,而 fetch 路径又经过平台 CDN。此时存在客户端缓存、Worker 手动缓存、上游 CDN 和原站四种来源。Worker 的未命中不等于原站请求;上游 CDN 命中也不等于手动缓存已经存在。若日志只记录一个“缓存状态”,同样的响应时间变化就可能被解释成不同原因。
为每层定义独立字段:手动查询结果、上游状态、回源耗时和规则版本。不要把这些全部塞进公开响应头,涉及内部地址或身份时应仅记录安全聚合。调试头可以在受控环境开启,并明确其含义。例如手动命中字段只描述代码分支,不描述对象是否刚刚从另一缓存层填充。诊断信息必须能回答问题,而不是制造一个看起来权威的 HIT 标签。
选择缓存方式前还要问为何需要应用自己写入。若只是缓存公开静态 HTML,平台标准缓存规则可能已经满足需求;若响应由多个数据源组合、键经过业务规范化、需要有限的计算结果缓存,手动 API 才可能有清晰价值。增加一个缓存层会增加失效和观察成本,不能仅因为 API 简单就叠加它。
手动缓存需要一个明确的状态转换表
一次请求可处于不具资格、查询命中、查询未命中、回源成功可写、回源成功不可写或回源失败。分别定义返回值,比把所有情况塞进一个 try/catch 更可审查。不具资格直接走正常业务;命中返回对象;未命中回源;可写响应后台存储;不可写响应只返回;回源失败按业务错误策略处理。缓存异常与原站异常不能混成同一个默认页面。
例如 cache.match 抛错后应继续回源并记录缓存故障,而 fetch 抛错后不能伪造正常二百。若业务允许陈旧内容回退,就需要先有明确陈旧对象和有效期,不是捕获异常后随便读取任何旧版本。Cache API 的过期行为与应用自建陈旧副本是不同设计,应查明实际 API 能力再实现,不假设过期对象总能被读取。
写入结果也不能用来决定当前响应是否算“发布成功”。内容发布应由部署与公开记录决定,缓存只是随后加速。若日志把 cache.put 成功计为文章发布,就会在多个地区多次记账。将业务事件与性能事件分开,既避免错误指标,也减少缓存运维对内容系统的耦合。
容量预算可以根据对象大小、变体数量和命中复用率评估。任意查询参数会制造一次性对象,超大正文会增加流复制成本;两个问题都不能靠延长 TTL 解决。先限制业务输入和缓存类型,再决定有效期。对未知内容类型默认不写入,新增类型时单独验收其头、体积和失效方式。
开发环境测试还有范围限制。本地模拟器可以验证代码分支和头处理,却不一定复现全球网络、真实清理能力和边缘位置。报告应明确哪些测试发生在本地,哪些发生在部署预览。不要把本地连续命中作为全球生产语义证据,也不要为真实测试开放所有用户数据缓存。使用合成公开资源足以验证机制,不必冒险采集私人响应。
clone 与流消费不是形式要求
响应正文是流。把同一响应直接用于写缓存与返回客户端,可能出现已经消费的正文错误。使用 clone() 创建独立读取分支,但也要留意大响应的资源消耗;不要为了方便把所有文件无条件读取成字符串再复制。
ctx.waitUntil() 允许响应返回后继续完成缓存写入,不代表写入必然成功。缓存错误应进入可观察日志,并且不能让缓存失败破坏正常回源结果。对业务而言缓存是加速层,不应成为公开页面可用性的唯一条件。
把 cache.put 放进 waitUntil 后,用户可以先收到正常响应,而写入在请求生命周期允许的后台阶段继续。若写入失败,当前用户未必受影响,但下一次请求可能仍然回源。生产代码应捕获并记录写入错误,避免未处理拒绝;同时限制日志频率,不让同一问题每秒产生数千条错误。
请求刚返回后立即再次请求,可能赶在写入完成前发生。测试如果假设“第一个请求结束等于缓存已完成”,会产生不稳定结果。可以在测试环境暴露受控完成标记,或使用有界轮询确认对象写入;不能把固定等待若干毫秒当成协议保证。测试报告应说明验证的是最终写入还是即时一致性。
并发未命中还会让多个请求同时回源。Cache API 本身不是通用互斥锁,不应把本地内存变量当成全球请求协调机制。若原站不能承受重复生成,需要专门的协调方案或平台能力;若生成成本低,允许少量重复反而更简单。先量化热点请求成本,再决定是否引入 Durable Objects 等额外状态,避免为不存在的瓶颈增加系统复杂度。
示例使用新 Response 调整头,但状态码、内容编码和正文必须仍然对应。若代码读取正文并修改文本,原来的 Content-Length 和强 ETag 可能不再正确;若仅转发流,也不要无条件复制不适合公开的头。缓存写入资格应依据完整响应判断,不仅看 response.ok。
原站可能返回 Cache-Control: private 或 no-store,应用不应仅因为状态为二百就覆盖为 public。Set-Cookie 更是明显风险信号,但没有该头不代表正文匿名:上游可能依据请求中的身份生成内容却没有重新设置 Cookie。请求资格与响应资格都要满足,任何一边无法证明公开共享就绕过。
大响应 clone 还需要关注消费者速度。返回分支与缓存写入分支并行读取时,平台流实现的缓冲行为可能影响内存。不要把任意下载文件纳入与小型 HTML 相同的缓存中间件。为体积、类型和范围请求制定明确策略,无法缓存的响应直接流式返回。性能优化的边界应包含资源上限,而不只是代码能否运行。
清理策略要与写入方式成对设计
更新内容前先问:缓存键是否版本化,是否支持按 URL 清理,手动删除作用于哪里,部署新 Worker 是否会清掉旧缓存。不要把“发布新代码”当作默认失效机制。
对静态内容,可用内容哈希资源 URL 减少主动清理需求;对 HTML 使用有限 TTL,并提供紧急清理路径。对自定义缓存键,应核对平台清理 API 是否能够准确匹配;键的可观测性与可清理性和命中率同样重要。
验收应包括缓存写入失败、上游 500、身份请求、更新后的旧对象、不同地区首次请求。把这些场景写进测试后,才有依据决定手动 Cache API 是否比平台标准缓存更适合当前需求。
某个位置调用 cache.delete 成功,只证明该调用能影响相应缓存环境,不能据此声称所有地区已清空。需要核对平台文档中的作用域和清理接口对自定义键的支持。若清理依赖原始 URL,而存储时使用改写 URL,运维脚本可能无法找到对象。记录从公开资源到内部键的映射,是恢复流程的一部分。
内容哈希适合不可变资源:新内容用新地址,旧对象自然失去引用。固定地址的 HTML 则需要明确 TTL 与清理策略。版本键能避免新代码误读旧格式,却无法阻止旧部署读取旧对象;回滚时必须检查旧键是否仍然有效。不要把部署版本自动拼入全部键而忘记每次发布都会造成冷启动。
对于紧急撤稿,可先让 Worker 在查缓存之前拒绝对应资源,再执行清理,避免清理传播期间继续返回对象。拒绝清单本身需要可靠来源和更新路径,不能只存在某个实例内存里。这个方案适合明确的紧急场景,不应该让缓存层成为日常内容权限系统。
从一次线上冷启动拆解请求时间线
设想部署后第一位读者请求文章,手动缓存未命中,fetch 从上游 CDN 获得旧版本,随后 Worker 将其存进局部对象。第二位读者命中手动缓存,延迟很低,但仍然阅读旧文。这里“手动缓存开始命中”不是成功验收,因为回源分支并不保证来源最新。部署流程必须检查完整版本链,而不能只在新 Worker 中增加版本标签,却沿用旧上游对象。
可以记录请求开始、手动查询结束、fetch 返回和后台写入完成的阶段时间,并给公开内容附上非敏感来源版本。若来源不是当前预期版本,按照明确策略不写入或继续验证,不将旧对象贴上新部署版本就当作新内容。应用版本与内容版本分开,能解释为什么新代码仍然返回旧文章。日志中只记录必要标识,避免正文、认证头和查询私人数据进入诊断。
后台写入完成后,另一个请求仍可能去到不同位置而未命中。测试报告应保留实际边缘位置,不把客户端同一网络地址看作同一缓存实例保证。对不同位置的结果不需要强行同步,只要与选定策略一致;若产品要求更新立即全球可见,就需要选择支持该要求的架构,而不是继续给局部 API 增加假想的全局语义。
同一位置发生并发写入时,旧请求比新请求更晚完成,可能覆盖更新的对象。版本键可以隔离不同版本,但固定键写入没有因此获得全局顺序保证。若数据更新频繁且顺序重要,需评估可靠的版本来源或协调机制。公开静态文章使用不可变版本资源与明确入口更新通常更简单;交易状态则不适合用这种缓存当权威来源。
对失效策略做一次有失败的桌面演练
先列出紧急删除所需步骤:确认公开资源、阻止继续写入、清理已存在对象、检查不同入口与恢复正常缓存。只清理一次而不阻止旧回源写入,某个正在执行的请求可能在清理后重新保存旧文。禁止写入与禁止读取是否同时生效,需要按实际 Worker 配置来源和部署传播范围验证,不能把内存变量修改当作分布式屏障。
演练可以在合成文章中写入标记,再让一个回源请求受控延迟。执行撤稿后释放该请求,检查标记是否重新出现。这个样本专门验证清理与写入竞争,比简单的 put 后 delete 更接近真实事故。若现有架构无法提供强撤回保证,应记录可接受的传播窗口或暂时绕过缓存,不宣称延长演练等待就解决了竞态。
清理 API 返回成功也要结合对象实际读取。对于自定义键,按公开 URL 清理可能没有命中内部存储身份;对不同缓存机制,清理能力与计费方案也可能不同。运维工具应显示将要清理的资源范围和规则版本,并限制目标域名,防止误清其他站点。操作日志记录响应结果而非令牌,保留可恢复的版本信息。
恢复缓存前,确认源内容已经正确且资格检查仍然成立。直接恢复旧规则可能让尚未到期的对象再被读取;改新版本空间可以隔离旧对象,却会造成冷启动压力。预热应控制并发,只处理批准的公开资源。安全恢复与性能恢复可以分阶段,先让正确内容可读,再逐步提高命中率,避免为了恢复指标而重新激活有问题的对象。
返回头与存储头应分别说明消费者
浏览器、共享代理和应用存储并不是同一个消费者。示例将 public, max-age 写进返回响应,是一个有意选择,不是使用 Cache API 的必需动作。若客户端必须每次验证,而边缘允许一分钟复用,应通过实际平台支持的机制分别表达,并核对浏览器最终收到什么。应用内部保存的响应头不一定等于平台标准缓存头的行为,不能用想当然的名称解释。
更新正文后旧浏览器仍显示缓存,即使 Worker 已清理也可能是预期的新鲜度行为。排障应首先确认是否发生网络请求,然后检查哪一层返回对象。测试不要一直勾选“禁用缓存”,否则无法发现错误的客户端有效期。分别保存首次访问、正常回访、强制验证与离线访问结果,说明每条路径对更新可见性的保证。
代理还可能压缩或变换响应。若应用修改正文,应重新评估内容长度、验证器和编码头;若只是流式转发,保持这些头与实际字节对应。为了手动缓存方便而完整读入大文件,会增加内存与延迟,不应作为通用路径。缓存类型白名单与大小预算帮助把小型计算结果和大文件下载分开。
一份机制对照表应回答谁负责恢复
选择标准 fetch 缓存时,平台负责其缓存规则执行,应用负责输入资格、可信源和业务失效需求;选择 Cache API 时,应用还负责查询、写入、键映射与后台错误处理。把这些责任写在机制对照表里,再比较是否真的需要额外控制。不能只比较两段代码行数,异常恢复与清理演练的成本通常更大。
若两个机制同时存在,为每个路由登记实际使用的层及其 TTL、版本来源和清理接口。路由调整时检查是否留下旧层,避免移除手动缓存代码后误以为所有响应不再缓存。一个机制的开关不控制另一个机制,这正是题目中的区别。验收通过的依据是责任明确且实际响应符合策略,而不是每层都显示 HIT。
命中测试应覆盖多个位置
同一地点连续请求命中,只能证明该路径上的缓存行为。跨区域测试应记录边缘位置、时间、键与版本标识,观察是否每个位置都需要各自回源。不要把一次全局测速变成“缓存已全球预热”的结论。
手动添加 X-Cache: HIT 可以帮助调试,但它只代表自己的代码分支,不代表浏览器或上游 CDN 的命中状态。多层缓存应分别标识,否则上游命中与 Worker 命中会混在一起,无法解释回源数量。
还要区分到期与提前缺失。对象允许存活一分钟,不意味着应用可以把缓存当成至少保存一分钟的数据库;存储和淘汰保证应按当前平台契约核对。测试让存储替身在新鲜期内返回未命中,确认程序仍能由可信源重新生成,并且业务成功计数没有被重复登记。缓存的存在不是文章是否公开的权威状态。
若可信源也不可达,恢复策略必须在设计中预先选择:受控错误、独立保存的允许陈旧版本,或其他已批准路径。不要在这时扩大键范围,拿别的语言或旧权限对象填补空缺。验收记录同时缺缓存与缺源的组合,检查错误类别能区分这两个阶段。它能说明备用路径是否真的存在,比重复测正常命中更有价值。