KV 鍵值儲存入門:在邊緣讀寫資料的第一步 | Cloudflare 完整教學

2026/08/16
KV 鍵值儲存入門:在邊緣讀寫資料的第一步 | Cloudflare 完整教學

到目前為止,我們寫的 Worker 大多是「無狀態」的——處理完一個請求就忘光一切。但真實的應用需要記住資料:使用者的設定、快取、feature flag、session……這些該存在哪?這一篇,我們正式進入 CF-3 儲存資料,從 Cloudflare 最容易上手的邊緣儲存方案——Workers KV(Key-Value 鍵值儲存) 開始。你會認識什麼是全球分散、讀取最佳化的鍵值儲存、學會建立 namespacebinding、用 get / put / list / delete 讀寫資料、掌握 text / json / stream / arrayBuffer 四種 value 型別與 getWithMetadata,並釐清哪些場景最適合把資料交給 KV。

前言

上一篇《CI/CD 自動化部署》,我們把 Worker 的部署流程一路自動化到無人值守的生產流水線。但你可能注意到一件事:我們到目前為止的 Worker,幾乎都是無狀態(stateless) 的——收到請求、算完、回傳,然後把一切忘得一乾二淨。下一個請求進來,它完全不記得上一個請求發生過什麼。

真實的應用不可能這樣。你需要記住東西:網站的全域設定、某個功能的開關(feature flag)、使用者登入後的 session、剛剛從外部 API 抓回來的資料快取……這些「狀態」得存在某個地方,讓下一個請求、甚至全球其他節點的請求,都能讀得到。

Workers KV(Key-Value,鍵值儲存) 就是 Cloudflare 為此提供的第一站,也是最容易上手的儲存方案。它的資料模型簡單到不能再簡單:一個 key(鍵)對應一個 value(值),就像一本巨大的字典——你給它一個 key,它還你對應的 value。

打個比方:KV 就像遍佈全球每一家便利商店都有的一台自動販賣機。你在總部(寫入)把一罐飲料補進機器,系統會把這罐飲料複製到全世界每一家分店的販賣機裡;之後不管使用者在東京、倫敦還是台北,都能就近在最靠近他的那台機器買到(讀取),不必千里迢迢跑回總部。這就是 KV 的核心價值——把資料複製到邊緣,讓全球讀取都快。代價是「補貨」(寫入)到全球生效需要一點時間,而且不適合「每秒瘋狂改同一格」。

讀完這篇,你會掌握:

  • KV 是什麼——全球分散、讀取最佳化的鍵值儲存,它的資料模型、運作原理,以及和資料庫的根本差異
  • 建立 namespace 與 binding——用 wrangler 建立 KV namespace、在 wrangler.jsonc 綁定,讓 Worker 用 env.MY_KV 存取
  • 核心 API——get / put / list / delete 四大操作,以及 text / json / stream / arrayBuffer 四種 value 型別與 getWithMetadata
  • 適用場景與常見坑——為什麼 KV 最適合設定檔、feature flag、快取,以及把它當強一致資料庫的陷阱

核心概念

KV 是什麼:全球分散的鍵值字典

Workers KV 是 Cloudflare 提供的全球分散式、讀取最佳化(read-optimized)的鍵值儲存。它的設計目標很明確:在極低延遲下支援海量讀取

它的運作方式是這樣的——當你寫入一筆資料,KV 會把它複製(replicate)到 Cloudflare 遍佈全球的邊緣節點。之後任何一個 Worker 要讀取這筆資料時,直接從離使用者最近的節點拿,無需回到某個中央資料庫。這就是為什麼 KV 的熱讀取(hot read)延遲可以低到 5ms 以下

用一張圖表達它的分散架構:

                    ┌──────────────┐
   你寫入 (put)  →  │  Cloudflare  │
                    │   全球網路    │
                    └──────┬───────┘
              複製到全球邊緣節點 (最多 ~60 秒傳播)
        ┌───────────┬──────┴──────┬───────────┐
        ▼           ▼             ▼           ▼
    ┌───────┐   ┌───────┐     ┌───────┐   ┌───────┐
    │ 東京  │   │ 倫敦  │     │ 台北  │   │ 紐約  │
    │ 節點  │   │ 節點  │     │ 節點  │   │ 節點  │
    └───┬───┘   └───┬───┘     └───┬───┘   └───┬───┘
        ▼           ▼             ▼           ▼
     就近讀取     就近讀取       就近讀取     就近讀取
    (get < 5ms) (get < 5ms)  (get < 5ms) (get < 5ms)

這裡藏著 KV 最重要的一個特性:它採用最終一致性(eventual consistency)。意思是,你寫入一個值後,同一地區通常立即可見,但全球其他節點最多可能需要約 60 秒才會同步到新值。這是「讓全球讀取都快」必須付出的代價——本篇先讓你記住這個特性,更深入的一致性細節與快取行為,我們留到下一篇專門討論。

KV 的 API 與資料庫對照

因為資料模型是「一個 key 對一個 value」,KV 的 API 也簡單得像操作一本字典。它只有四個核心操作:

KV 操作作用類比字典類比 SQL
put(key, value)寫入/覆蓋一個鍵值dict[key] = valueINSERT / UPDATE
get(key)讀取一個 key 的值dict[key]SELECT ... WHERE key = ?
list({ prefix })列舉 key(可依前綴篩選)dict.keys()SELECT key ...
delete(key)刪除一個 keydel dict[key]DELETE ... WHERE key = ?

注意 KV 沒有 SQL 那種 WHERE 條件JOINORDER BY——你只能用 key 或 key 前綴來查詢。這是刻意的取捨:把查詢能力砍到最簡,換取全球分散與極速讀取。

關鍵術語與限制

在動手之前,先認識幾個關鍵術語,以及 KV 的重要限制:

  • Namespace(命名空間):KV 資料的容器,你可以把它想成一個獨立的「資料庫」或「一張表」。同一個 Worker 可以綁定多個 namespace(例如一個放 SESSIONS、一個放 CACHE)。
  • Binding(繫結):在 wrangler.jsonc 裡把 namespace 綁到一個變數名(如 MY_KV),Worker 程式中就能用 env.MY_KV 存取它。這和前面介紹過的其他資源綁定是同一套機制。
  • Value 型別:同一個值可以用四種型別讀出——text(字串)、json(自動解析成物件)、arrayBuffer(二進位)、stream(串流,適合大型值)。
  • Metadata(中繼資料):寫入時可附帶的一小段附註資訊(最多 1,024 bytes),用 getWithMetadata 讀取。

幾個必須記住的硬限制:

特性說明
一致性模型最終一致(寫入後全球最多約 60 秒可見)
熱讀取延遲< 5ms
每個 key 寫入速率每秒最多 1 次(超過回傳 HTTP 429)
Value 大小上限25 MiB
Key 大小上限512 bytes
Metadata 大小1,024 bytes

一句話總結:KV 讀取快、可全球分散、寫入慢且最終一致。 這決定了它適合什麼、不適合什麼——後面「常見錯誤」一節會詳談。

實作範例

概念齊了,我們從零把 KV 用起來。分三步:建立 namespace → 綁定 → 在 Worker 裡讀寫。

1. 建立 KV Namespace

wrangler 指令建立一個 namespace。假設我們要做一個放應用設定的 KV:

# 建立 production namespace(名稱自訂,這裡叫 MY_KV)
npx wrangler kv namespace create MY_KV

# 輸出範例(把這段設定複製到 wrangler.jsonc):
# { "binding": "MY_KV", "id": "abc123xyz789..." }

# (選用)建立本地開發用的 preview namespace
npx wrangler kv namespace create MY_KV --preview

指令會回傳一個 id——這是這個 namespace 的唯一識別碼,下一步要用到。

2. 在 wrangler.jsonc 綁定 namespace

把上一步拿到的 binding 名稱與 id 填進 wrangler.jsonckv_namespaces 陣列:

{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-08-01",
  "kv_namespaces": [
    {
      "binding": "MY_KV",           // Worker 程式中用 env.MY_KV 存取
      "id": "abc123xyz789...",      // production namespace 的 ID
      "preview_id": "preview456..." // 本地 wrangler dev 時使用(選用)
    }
  ]
}

同時,幫 TypeScript 補上型別定義,讓 env.MY_KV 有正確的型別提示(KVNamespace):

// worker-configuration.d.ts(或你的 Env 介面所在檔案)
interface Env {
  MY_KV: KVNamespace;
}

綁定完成後,Worker 就能透過 env.MY_KV 直接操作這個 namespace 了。

3. put:寫入資料

put 用來寫入(或覆蓋)一個鍵值。最基本的用法就是傳入 key 和 value:

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // 寫入一個純字串值
    await env.MY_KV.put("greeting", "Hello, Edge!");

    // 寫入 JSON:先自己 stringify 成字串再存
    await env.MY_KV.put("config", JSON.stringify({ theme: "dark", lang: "zh-TW" }));

    return new Response("已寫入");
  }
} satisfies ExportedHandler<Env>;

put 還支援兩個實用的選項——TTL(存活時間,自動過期)metadata(附註資訊):

// 搭配 TTL:從現在起 3600 秒(1 小時)後自動刪除,非常適合 session 與快取
await env.MY_KV.put("session:abc", token, {
  expirationTtl: 3600
});

// 搭配 metadata:附帶一小段結構化附註(最多 1,024 bytes)
await env.MY_KV.put("user:profile:123", userData, {
  metadata: { version: 2, updatedBy: "worker-auth" }
});

// 兩者一起用
await env.MY_KV.put("cache:homepage", html, {
  expirationTtl: 300,
  metadata: { cachedAt: Date.now() }
});

expirationTtl 這個特性非常好用——KV 原生支援自動過期,你不用自己寫排程去清理過期資料,時間一到 KV 會自動幫你把 key 刪掉。

4. get:讀取資料與四種 value 型別

get 用來讀取一個 key 的值。它的第二個參數決定回傳型別,這是 KV 的一大特色:

// (1) text(預設):回傳字串
const greeting = await env.MY_KV.get("greeting");
// 型別:string | null

// (2) json:KV 自動 JSON.parse,還能用泛型指定型別,省去手動 parse
interface AppConfig { theme: string; lang: string; }
const config = await env.MY_KV.get<AppConfig>("config", "json");
// 型別:AppConfig | null

// (3) arrayBuffer:回傳二進位,適合圖片、檔案等 binary 資料
const buffer = await env.MY_KV.get("image:avatar", "arrayBuffer");
// 型別:ArrayBuffer | null

// (4) stream:回傳 ReadableStream,適合大型值(不會一次載進記憶體)
const stream = await env.MY_KV.get("large-file", "stream");
// 型別:ReadableStream | null

四種型別的選擇原則:

型別適用場景回傳
text(預設)值是字串string | null
json值是 JSON 物件(自動解析)T | null
arrayBuffer二進位資料(圖片、檔案)ArrayBuffer | null
stream大型值(> 1 MB)ReadableStream | null

最重要的一件事:只要 key 不存在,get 一律回傳 null 所以讀完務必處理 null,別直接存取屬性:

const config = await env.MY_KV.get<AppConfig>("config", "json");

// ✅ 用可選鏈與預設值,避免 config 為 null 時拋出 TypeError
const theme = config?.theme ?? "light";

5. getWithMetadata:同時讀值與附註

如果你當初 put 時有附帶 metadata,可以用 getWithMetadata 一次讀出 value 和 metadata:

const { value, metadata } = await env.MY_KV.getWithMetadata<
  string,                          // value 的型別
  { version: number; updatedBy: string } // metadata 的型別
>("user:profile:123");

if (value !== null) {
  console.log(`值:${value}`);
  console.log(`版本:${metadata?.version},最後由 ${metadata?.updatedBy} 更新`);
}

比起先 get 值、再用另一個 key 存附註,getWithMetadata 讓你一次搞定,是稽核、版本管理、快取標記的好幫手。

6. list:列舉 key(支援前綴)

list 用來列舉 namespace 裡的 key。它支援前綴(prefix)篩選分頁(cursor)——因為 KV 沒有 SQL 查詢,前綴篩選就是你最主要的「查詢」手段。這也是為什麼 key 設計常用 分類:識別碼 的命名慣例(如 user:123session:abc),方便用前綴一次撈出同類資料:

// 列出所有以 "user:" 開頭的 key
const listed = await env.MY_KV.list({ prefix: "user:" });

for (const key of listed.keys) {
  console.log(key.name);        // key 名稱,例如 "user:123"
  console.log(key.metadata);    // 該 key 的 metadata(list 會一併回傳!)
  console.log(key.expiration);  // 過期時間(若有設 TTL)
}

// 資料很多時要分頁:用 cursor 逐頁讀,直到 list_complete 為 true
let cursor: string | undefined;
const allKeys: string[] = [];
let done = false;
while (!done) {
  const page = await env.MY_KV.list({ prefix: "user:", limit: 1000, cursor });
  allKeys.push(...page.keys.map(k => k.name));
  cursor = page.list_complete ? undefined : page.cursor;
  done = page.list_complete;
}
console.log(`共有 ${allKeys.length} 個使用者 key`);

注意 list一併回傳每個 key 的 metadata——這代表你可以只靠一次 list 掃描所有 key 的附註(例如篩出所有標記為「已停用」的 feature flag),而不必逐一 get 每個值,大幅節省讀取次數。

7. delete:刪除 key

delete 最單純——傳入 key 即可,即使 key 不存在也不會報錯:

await env.MY_KV.delete("session:abc");

8. 綜合範例:一個小小的設定服務

把上面的操作串起來,寫一個能讀寫應用設定的完整 Worker:

interface Env {
  MY_KV: KVNamespace;
}

interface AppConfig {
  theme: string;
  featureX: boolean;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // POST /config → 寫入設定
    if (request.method === "POST" && url.pathname === "/config") {
      const body = await request.json<AppConfig>();
      await env.MY_KV.put("app:config", JSON.stringify(body), {
        metadata: { updatedAt: new Date().toISOString() }
      });
      // ⚠️ 直接用剛寫入的本地變數回應,不要重新 get(最終一致,可能讀到舊值)
      return Response.json({ ok: true, config: body });
    }

    // GET /config → 讀取設定
    if (request.method === "GET" && url.pathname === "/config") {
      const { value, metadata } = await env.MY_KV.getWithMetadata<AppConfig, {
        updatedAt: string;
      }>("app:config", "json");

      if (value === null) {
        // 找不到時給一組安全的預設值
        return Response.json({ theme: "light", featureX: false, source: "default" });
      }
      return Response.json({ ...value, updatedAt: metadata?.updatedAt });
    }

    return new Response("Not Found", { status: 404 });
  }
} satisfies ExportedHandler<Env>;

這個小服務示範了完整的讀寫循環:put 帶 metadata 寫入、getWithMetadata 讀出值與附註、json 型別自動解析、null 有預設值兜底,以及最關鍵的——寫入後直接用本地變數回應,不重新 get。這一點,我們在下一節詳談。

常見錯誤與最佳實踐

坑一:把 KV 當成強一致的主資料庫用。

這是最常見、也最致命的誤解。KV 是最終一致的:你 put 一個值後,同一次請求內、或全球其他節點,立刻 get 可能還讀到舊值甚至 null

// ❌ 錯誤:寫入後立即讀取,可能拿到舊值或 null
await env.MY_KV.put("key", "新值");
const result = await env.MY_KV.get("key"); // 可能仍是 null 或舊值!

// ✅ 正確:直接使用你剛寫入的本地變數,不要重新 get
const newValue = "新值";
await env.MY_KV.put("key", newValue);
return new Response(newValue); // 用本地變數,不再呼叫 get

如果你的業務需要「寫完立刻讀到正確值」(如扣款、庫存、計數器),KV 不是正確工具——請改用 D1(強一致關聯式資料庫)或 Durable Objects(強一致鍵值 + 天然序列化寫入)。

坑二:高頻寫入同一個 key,觸發 429。

KV 的每個 key 每秒最多寫入 1 次,超過就回傳 HTTP 429。

// ❌ 錯誤:迴圈裡對同一 key 快速寫入,必觸發 429
for (const item of items) {
  await env.MY_KV.put("counter", String(count++)); // 429!
}

// ✅ 正確:計數器這種高頻寫入,改用 Durable Objects 做原子累加;
//        或在記憶體聚合後,一次性寫入 KV

記住 KV 的定位是讀多寫少——讀取量遠大於寫入量(理想上 > 100:1)的場景才是它的主場。

坑三:沒處理 null 回傳值。

前面強調過:key 不存在時 get 回傳 null。忘記處理就會踩到 TypeError

// ❌ 錯誤:key 不存在時 config 為 null,存取 .theme 直接爆
const config = await env.MY_KV.get("config", "json");
return new Response(config.theme); // TypeError: Cannot read properties of null

// ✅ 正確:用可選鏈 + 預設值
const config = await env.MY_KV.get<{ theme: string }>("config", "json");
return new Response(config?.theme ?? "light");

坑四:key 設計沒有前綴慣例,難以列舉與管理。

KV 只能用 key 或 prefix 查詢,所以 key 的命名結構就是你的資料組織方式。隨意命名(如 auser1datathe_config)日後很難用 list 撈出同類資料。

✅ 用「分類:識別碼」的階層式命名,方便前綴列舉:
   user:123
   user:456
   session:abc-token
   flag:new-checkout
   cache:homepage

   → list({ prefix: "user:" })  一次撈出所有使用者
   → list({ prefix: "flag:" })  一次撈出所有 feature flag

最適合 KV 的場景,正好都符合「讀多寫少、可容忍最終一致」:

場景為什麼適合 KV
應用設定檔 / 全域設定全球部署,改一次即可,無需重發 Worker
Feature flag(功能旗標)讀取量大、更新極少,改完全球邊緣就近生效
API 回應快取搭配 TTL 自動過期,靠邊緣讀取加速
使用者 session讀多寫少,expirationTtl 自動清過期
A/B 測試分組高讀取、極少更新

最佳實踐小結: 記四條——別把 KV 當強一致資料庫(要強一致改用 D1 / Durable Objects);別高頻寫同一個 key(> 1 RPS 會 429,計數改用 DO);永遠處理 null(用 ?.?? 兜底);分類:識別碼 的前綴命名(方便 list 列舉)。守住這四條,KV 就會是你邊緣應用最順手的儲存層。

小結

上一篇《CI/CD 自動化部署》,我們把 Worker 的部署流程自動化到無人值守;這一篇,我們正式踏進 CF-3 儲存資料,學會用最容易上手的 Workers KV 把資料留在邊緣:

  • KV 是什麼——全球分散、讀取最佳化的鍵值儲存,把資料複製到全球邊緣節點,熱讀取 < 5ms;代價是最終一致(寫入全球最多約 60 秒可見)與每 key 每秒 1 次寫入的限制。
  • namespace 與 binding——用 wrangler kv namespace create 建立、在 wrangler.jsonckv_namespaces 綁定,Worker 用 env.MY_KV 存取。
  • 核心 API——put 寫入(可帶 TTL / metadata)、get 讀取(text / json / stream / arrayBuffer 四種型別,不存在回 null)、getWithMetadata 同時讀值與附註、list 前綴列舉、delete 刪除。
  • 場景與坑——最適合設定檔、feature flag、快取、session 這類讀多寫少的資料;別把它當強一致主資料庫、別高頻寫同一 key、務必處理 null、用前綴命名 key。

我們在正文裡刻意「點到為止」的最終一致性,其實還有很多細節值得深挖:寫入後究竟多久全球才會同步?cacheTtl 參數怎麼影響邊緣快取?為什麼查詢不存在的 key 也會被快取(negative caching)?怎麼讓 KV 當快取層時又快又不讀到過期資料?下一篇《KV 進階:一致性與快取》,我們就把這些一致性與快取行為徹底拆開,教你把 KV 從「會用」升級到「用得又快又對」。

想先查閱官方對 Workers KV 的完整說明,可以隨時參考 Cloudflare Workers KV 官方文件。KV 入門這一課已就緒,我們下一篇《KV 進階:一致性與快取》見。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →