beio Logobeio

【Cloudflare Workers 全端架構師之路 03】設定篇:KV 快取架構與分層策略

發布於

01. 前言:不要把 KV 當 Redis 用

在設計 Cloudflare Workers 架構時,最常見的錯誤就是:「我需要一個快取,那就用 KV 吧,反正它就像 Redis。」

這是一個危險的誤區。

雖然它們都是 Key-Value Store,但在分散式系統的 CAP 定理中,它們佔據完全不同的位置。

  • Redis: 強調 CPCA (視設定而定)。寫入快,讀取快,通常集中在單一 Region。
  • Workers KV: 強調 AP (Availability & Partition Tolerance) 與 P (Performance)。讀取極快 (Read-heavy),但寫入有顯著延遲 (Write-slow),且遵循最終一致性 (Eventual Consistency)

這一篇,我們將學習如何「正確」使用 KV,並利用它來構建一個Feature Flags (功能開關) 服務

02. 架構比較:KV vs. Redis

為了讓你更清楚何時該用 KV,我們來看這張對比表:

特性Workers KVRedis (如 AWS ElastiCache)
部署範圍Global (自動複製到全球節點)Regional (通常需手動設定 Cross-Region Replica)
讀取延遲極低 (從邊緣節點讀取)中等 (需跨越網路回到 Region)
寫入速度慢 (需數秒至 60 秒同步全球)極快 (毫秒級)
一致性最終一致性 (剛寫入可能讀到舊值)強一致性 (Strong Consistency)
適用場景設定檔、HTML 片段、路由規則、靜態資源計數器、Session Store、即時聊天訊息

架構師筆記: 如果你要做「計數器 (Counter)」或「防重複提交 (Rate Limiter)」,千萬不要用 KV。因為在同步的 60 秒內,全球的節點可能會重複計算。這種場景請使用 Durable Objects (我們將在 Part 7 介紹)。

03. 實作:建立全球設定檔服務 (Feature Flags)

我們將建立一個 API,用來控制應用程式的功能開關 (例如:開啟維護模式、A/B 測試比例)。

步驟 1: 建立 Namespace

在終端機執行:

# 建立生產環境 KV
npx wrangler kv:namespace create APP_CONFIG

# 建立預覽環境 KV (避免本地開發汙染線上資料)
npx wrangler kv:namespace create APP_CONFIG --preview

複製輸出的 ID,填入 wrangler.toml

[[kv_namespaces]]
binding = "APP_CONFIG"
id = "你的_PROD_ID"
preview_id = "你的_PREVIEW_ID"

步驟 2: 定義 TypeScript 型別

src/index.ts 中定義資料結構:

type Bindings = {
  APP_CONFIG: KVNamespace;
}

type Config = {
  maintenance_mode: boolean;
  beta_feature_enabled: boolean;
  promo_banner_text: string;
}

// 預設值 (Fallback)
const DEFAULT_CONFIG: Config = {
  maintenance_mode: false,
  beta_feature_enabled: false,
  promo_banner_text: "Welcome!"
}

04. 進階實作:Tiered Caching (分層快取)

直接讀取 KV 雖然快,但 KV 的讀取操作是有免費額度限制的(且付費版也有成本)。為了極致優化,我們應該在 Worker 記憶體中再加一層快取。

Cloudflare KV 的 get 方法支援 cacheTtl 參數,這會告訴 Cloudflare:「在接下來的 N 秒內,不要去查底層資料庫,直接給我邊緣節點緩存的值。」

import { Hono } from 'hono'
const app = new Hono<{ Bindings: Bindings }>()

app.get('/config', async (c) => {
  // 策略:Stale-While-Revalidate 的變體
  // 設定 cacheTtl: 60 代表 60 秒內這個節點的請求都直接回傳快取
  // 這能大幅降低 KV 讀取費用 (Billable Read Ops)
  const configJson = await c.env.APP_CONFIG.get('global_settings', { cacheTtl: 60 });
  
  let config: Config;
  
  if (configJson) {
    try {
      config = JSON.parse(configJson);
    } catch (e) {
      console.error("Config parse error", e);
      config = DEFAULT_CONFIG;
    }
  } else {
    // 雖然 KV 沒資料,但為了系統穩定性,回傳預設值
    config = DEFAULT_CONFIG;
  }

  // 加上 Browser Cache Control
  // 告訴瀏覽器:你可以快取 30 秒,但在 60 秒內雖然過期了,還是可以用舊的 (stale),同時背景去更新
  c.header('Cache-Control', 'public, max-age=30, stale-while-revalidate=60');

  return c.json(config);
})

export default app

05. 寫入策略:如何處理最終一致性?

因為 KV 寫入慢,我們寫一個 Admin API 來更新設定。這裡要注意的是,更新後你不會立刻在 GET API 看到變更。

app.post('/admin/config', async (c) => {
  const body = await c.req.json();
  
  // 寫入 KV
  // 注意:這裡沒有 cacheTtl,因為 put 操作是針對底層儲存
  await c.env.APP_CONFIG.put('global_settings', JSON.stringify(body));

  return c.json({ 
    success: true, 
    message: "Config updated. Changes will propagate globally within 60 seconds." 
  });
})

架構師觀點 - 寫入延遲的應對策略: 在 UI 設計上,當管理員按下「儲存」後,不要立刻重新讀取 /config API 來刷新畫面,因為他極機率會讀到舊的。你應該直接在前端更新 UI 狀態,並顯示一個提示:「設定已儲存,正在同步至全球節點」。

06. 本地測試與驗證

  1. 啟動開發環境: npm run dev
  2. 寫入設定: 使用 Postman POST /admin/config
  3. 讀取設定: GET /config
  4. 觀察快取: 如果你在 60 秒內連續重新整理瀏覽器,你會發現 Cloudflare 的 Console Log 不會每次都跳出 KV 讀取的紀錄。這證明了 cacheTtl 正在發揮作用,保護你的錢包。

07. 小結與下一步

我們今天學到了:

  1. KV 不是 Redis:它適合讀多寫少、對一致性要求不高的場景。
  2. 快取策略:利用 cacheTtl 和 HTTP Cache-Control 來平衡即時性與成本。
  3. 容錯設計:永遠準備好 DEFAULT_CONFIG,防止 KV 服務異常或資料損壞。

現在我們有了設定檔,但如果我們要存取結構化的商業數據(如:使用者資料、訂單紀錄),KV 的 Key-Value 結構就顯得力不從心,而且無法做複雜查詢 (SQL)。

在下一篇 Part 4: 數據篇,我們將引入 Cloudflare 的殺手級武器 —— D1 Database (SQLite)。 我們將打破「SQLite 不適合做 Server DB」的迷思,並結合 Drizzle ORM 來展示如何優雅地處理關聯式資料。


Ken Huang

關於作者

Ken Huang

熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。

最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。

這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。

Android APP DevelopmentiOS APP DevelopmentBePTT CreatorFull Stack Learner