发布一篇文章后，缓存系统如何联动：CDN、KV 与搜索索引的失效编排

我之前已经分别写过两篇缓存相关的文章：《Cloudflare Workers 全栈应用：正确使用 Cache API 加速网站》《Cloudflare KV 高效技巧：使用“版本号”实现低成本批量缓存失效》两篇把技术点拆开讲清楚了，但真实业务里的缓存编排是另一件事。点下发布按钮后，摘要、公开内容、搜索索引、KV 缓存、CDN 缓存，到底谁先更新、谁后失效、谁该精准删、谁该批量 bump 版本？这篇专门讲这个。从“发布按钮”开始看整条链路先看一篇文章从发布到前台可见，背后实际发生了什么：管理员发布文章 → 触发 PostProcessWorkflow → Step 1: 检查 sync hash，内容没变就直接跳过 → Step 2: 自动生成摘要（如果还没有） → Step 3: 生成 public content snapshot（给 SSR / 阅读路径用） → Step 4: 更新搜索索引 → Step 5: 失效缓存 → 删除单篇详情 KV 缓存 → bump 列表版本号 → 删除公开标签列表缓存 → Purge CDN 页面与 API → Step 6: 写回新的 sync hash 先清缓存再更新索引，用户就可能读到半新半旧的数据。应当：先把公开内容和搜索索引准备好，再做缓存失效。失效策略分层这个项目里至少有四种不同的失效策略：对象策略原因单篇文章详情精准删除一个 KV Key 影响范围小，直接删最便宜文章列表 bump 版本号列表页组合太多，逐个删不现实标签列表删除固定 Key 这是一个单 Key 的公共缓存 CDN 页面 / API Purge URL + Prefix 让边缘节点立刻失效单篇详情为什么适合精准删除单篇文章页的 KV 缓存是带具体 slug 的，所以更新一篇文章时，我只删这一条： const version = await CacheService.getVersion(context, "posts:detail"); await CacheService.deleteKey( context, POSTS_CACHE_KEYS.detail(version, post.slug), ); 这类缓存的特点是： Key 可预测影响范围小删除成本低所以没必要为了“一致性美观”强行上版本号。首页、归档页、分页列表等都依赖“文章集合”。逐个删除缓存会遇到这些问题：你未必知道所有被影响的列表 URL 就算知道，删除操作也会越来越贵很难保证没有漏网之鱼所以这里我直接 bump posts:list 的版本号： await CacheService.bumpVersion(context, "posts:list"); 新版本号会指向新 key，旧缓存逻辑上就失效了。标签列表是一个固定 key 的公共缓存，直接删除即可： await CacheService.deleteKey({ env }, TAGS_CACHE_KEYS.publicList); 漏掉这一步，会导致文章更新了，但标签页和计数还是旧的。 CDN 层要清的，不只是文章详情页一篇文章的变更会影响好几个入口，实际要清的是这些： export async function purgePostCDNCache(env: Env, slug: string) { return purgeCDNCache(env, { urls: [ `/post/${slug}`, `/api/post/${slug}`, `/api/post/${slug}/related`, `/api/tags`, "/", ], prefixes: [ "/posts", "/api/posts", "/search", "/api/search", ], }); } 把所有可能受影响的公共入口一次清掉：文章详情页会变单篇文章 API 会变首页和列表页可能因为发布时间、摘要或标题变化而变搜索页的数据源可能会变标签页可能会变这条工作流里，搜索索引也是缓存编排的一部分搜索也是发布后需同步更新的公开读路径。所以在 PostProcessWorkflow 里，我把搜索索引更新放在缓存失效之前： // 先更新搜索索引 await step.do("update search index", async () => { return await upsertPostSearchIndex(this.env, updatedPost); }); // 再失效缓存 await step.do("invalidate caches", async () => { await invalidatePostCaches(this.env, updatedPost.slug); }); CDN 失效后，详情页、列表页和搜索结果能近乎同步变新。公开快照也是“缓存系统”的一部分高亮代码后的公开内容快照，也是面向读路径的预处理结果。不是所有层都能做到强一致业务系统里的缓存很少做到所有层同时失效。比如相关文章接口在 CDN 层会跟着文章一起 Purge，但它内部还有自己的服务端缓存策略，并不是所有读路径都统一挂在同一个版本号命名空间上。这不是 bug，是 trade-off。工程上要的是足够新、足够稳、足够便宜，不是为了理论完美把每一层都绑成同一种失效机制。未来发布时间和下架，也要走自己的分支发布的两个边界处理：未来发布时间如果文章状态已经是 published，但发布时间在未来，搜索索引不会立刻更新，而是交给定时发布那条链路处理。下架文章文章下架时，工作流会走另一条分支：从搜索索引里删除失效相关缓存删除 sync hash 这和更新文章逻辑不同。为什么我越来越觉得“缓存”本质上是在编排发布流程在真实项目里，缓存更像一套发布编排系统：什么时候生成公开快照什么时候生成摘要什么时候更新搜索索引什么时候删单 Key 什么时候 bump 版本什么时候 Purge CDN 拆开都不难，难的是让它们按正确顺序一起工作。