发布于: -/最后更新: -/4 分钟/

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

摘要

使用 Cloudflare KV 做缓存时,开发者常遇到批量失效缓存的问题。传统方法使用 list 方法按前缀查找 Key,但这种操作慢且贵。更好的方法是引入“缓存版本号(Versioning)”。核心原理是将版本号嵌入 Key 中,读取时先获取当前版本号,拼接出 Key,失效时只需将版本号升级即可。这种方法操作原子化,O(1) 复杂度,即时生效,极大节省 KV 操作费用。需要注意给带版本的 Key 设置 TTL,让旧版本的“僵尸数据”自动清理,避免存储空间膨胀。

在使用 Cloudflare KV 做缓存时,开发者常遇到一个棘手的问题:如何批量失效缓存?

虽然 KV 提供了 list 方法,支持按前缀查找 Key,但这个操作不仅慢(最终一致性延迟),而且贵(会产生大量的 Read/List/Delete 操作费用)。

一种更聪明、更经济的解法是引入 “缓存版本号(Versioning)”

核心原理

不要直接使用 posts:list:page1 作为 Key,而是在 Key 中嵌入一个版本号,例如 posts:list:v1:page1

  • 读取时:先获取当前版本号,拼接出 Key,再去读数据。

  • 失效时:不需要删除成百上千个 Key,只需将版本号从 v1 升级到 v2。旧的 v1 数据虽然还在,但因为逻辑上不再被引用,实际上已经“失效”了。

1. 基础设施:版本管理

首先,定义好缓存的命名空间,并实现版本号的获取与更新逻辑。

TypeScript
// 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<string> {
  // 统一前缀 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<void> {
  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 在被“抛弃”后会自动过期删除。

TypeScript
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}`);
}

读取缓存(读取时带版本)

TypeScript
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

TypeScript
// 触发失效:所有 posts:list 下的缓存瞬间无效(逻辑上)
await bumpVersion(env, CACHE_NAMESPACES.POSTS_LIST);

总结

通过引入一个极小的“元数据”查询(getVersion),我们避免了昂贵的 KV 遍历操作。

  • 优点:操作原子化,O(1) 复杂度,即时生效,极大节省 KV 操作费用。

  • 注意:务必给带版本的 Key 设置 TTL(过期时间),让旧版本的“僵尸数据”自动清理,避免存储空间膨胀。

正文结束