KV 鍵值儲存入門:在邊緣讀寫資料的第一步 | Cloudflare 完整教學
到目前為止,我們寫的 Worker 大多是「無狀態」的——處理完一個請求就忘光一切。但真實的應用需要記住資料:使用者的設定、快取、feature flag、session……這些該存在哪?這一篇,我們正式進入 CF-3 儲存資料,從 Cloudflare 最容易上手的邊緣儲存方案——Workers KV(Key-Value 鍵值儲存) 開始。你會認識什麼是全球分散、讀取最佳化的鍵值儲存、學會建立 namespace 與 binding、用
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] = value | INSERT / UPDATE |
get(key) | 讀取一個 key 的值 | dict[key] | SELECT ... WHERE key = ? |
list({ prefix }) | 列舉 key(可依前綴篩選) | dict.keys() | SELECT key ... |
delete(key) | 刪除一個 key | del dict[key] | DELETE ... WHERE key = ? |
注意 KV 沒有 SQL 那種 WHERE 條件、JOIN、ORDER 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.jsonc 的 kv_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:123、session: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 的命名結構就是你的資料組織方式。隨意命名(如 a、user1data、the_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.jsonc的kv_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 進階:一致性與快取》見。