Cloudflare KV 高效技巧：使用“版本号”实现低成本批量缓存失效

在使用 Cloudflare KV 做缓存时，开发者常遇到一个棘手的问题：如何批量失效缓存？虽然 KV 提供了 list 方法，支持按前缀查找 Key，但这个操作不仅慢（最终一致性延迟），而且贵（会产生大量的 Read/List/Delete 操作费用）。一种更聪明、更经济的解法是引入 “缓存版本号（Versioning）”。核心原理不要直接使用 posts:list:page1 作为 Key，而是在 Key 中嵌入一个版本号，例如 posts:list:v1:page1。读取时：先获取当前版本号，拼接出 Key，再去读数据。失效时：不需要删除成百上千个 Key，只需将版本号从 v1 升级到 v2。旧的 v1 数据虽然还在，但因为逻辑上不再被引用，实际上已经“失效”了。 1. 基础设施：版本管理首先，定义好缓存的命名空间，并实现版本号的获取与更新逻辑。 // types.ts export const CACHE_NAMESPACES = { POSTS_LIST: "posts:list", POSTS_DETAIL: "posts:detail", } as const; // 用字面量实现类型安全 export type CacheNamespace = (typeof CACHE_NAMESPACES)[keyof typeof CACHE_NAMESPACES]; /** * 获取缓存版本号 * 规范：Namespace 建议使用 "entity:scope" 格式，如 "posts:list" */ export async function getVersion( env: Env, namespace: CacheNamespace, ): Promise { // 统一前缀 ver:，保持视觉整洁 const key = `ver:${namespace}`; const v = await env.KV.get(key).catch((err) => console.error(`[Cache] Failed to get version ${key}:`, err), ); // 返回 "v1", "v2" 这种格式，方便直接拼到 Key 字符串里 if (v && !Number.isNaN(Number.parseInt(v))) { return `v${v}`; } return "v1"; } /** * 更新版本号（实现批量失效） */ export async function bumpVersion( env: Env, namespace: CacheNamespace, ): Promise { const key = `ver:${namespace}`; const current = await env.KV.get(key).catch((err) => console.error(`[Cache] Failed to get version ${key}:`, err), ); let next = 1; if (current) { const parsed = Number.parseInt(current); if (!Number.isNaN(parsed)) { next = parsed + 1; } } // 写入新的版本号 await env.KV.put(key, next.toString()).catch((err) => console.error(`[Cache] Failed to bump version ${key}:`, err), ); console.log(`[Cache] Bumped version ${key} to ${next}`); } 2. 实战：带版本号的写入与读取在这一步，我们将版本号注入到实际的 KV Key 中。写入缓存（存入时带版本）为了防止旧版本的垃圾数据永久占用 KV 空间，强烈建议在写入带版本的 Key 时设置 expirationTtl。这样旧版本的 Key 在被“抛弃”后会自动过期删除。 export async function savePostListCache(env: Env, page: number, data: any) { // 1. 获取当前命名空间的版本号 const version = await getVersion(env, CACHE_NAMESPACES.POSTS_LIST); // 2. 构造带版本的 Key：posts:list:v1:page:1 const cacheKey = `${CACHE_NAMESPACES.POSTS_LIST}:${version}:page:${page}`; // 3. 写入 KV，并设置过期时间（例如 1 天） // 即使我们立刻升级了版本号，旧数据也会在1天后自动物理删除，不占空间 await env.KV.put(cacheKey, JSON.stringify(data), { expirationTtl: 86400, }); console.log(`[Cache] Saved to ${cacheKey}`); } 读取缓存（读取时带版本） export async function getPostListCache(env: Env, page: number) { // 1. 动态获取当前有效的版本号 // 如果刚才执行了 bumpVersion，这里拿到的就是 "v2" const version = await getVersion(env, CACHE_NAMESPACES.POSTS_LIST); // 2. 构造 Key，此时会自动指向 v2 的数据 const cacheKey = `${CACHE_NAMESPACES.POSTS_LIST}:${version}:page:${page}`; // 3. 读取数据 const cachedData = await env.KV.get(cacheKey, "json"); if (!cachedData) { console.log(`[Cache Miss] Key: ${cacheKey}`); return null; } console.log(`[Cache Hit] Key: ${cacheKey}`); return cachedData; } 3. 如何触发批量失效？当你的博客发布了新文章，或者修改了全局配置，需要让所有列表页缓存失效时，只需要调用一次 bumpVersion： // 触发失效：所有 posts:list 下的缓存瞬间无效（逻辑上） await bumpVersion(env, CACHE_NAMESPACES.POSTS_LIST); 总结通过引入一个极小的“元数据”查询（getVersion），我们避免了昂贵的 KV 遍历操作。优点：操作原子化，O(1) 复杂度，即时生效，极大节省 KV 操作费用。注意：务必给带版本的 Key 设置 TTL（过期时间），让旧版本的“僵尸数据”自动清理，避免存储空间膨胀。