儲存選型:KV / D1 / R2 / DO / Hyperdrive 決策樹
這篇要解決的問題
Section titled “這篇要解決的問題”前五篇各自深入一個產品。這一篇把它們放在一起,回答唯一真正重要的問題:面對一個新需求,五分鐘內選對儲存並說得出理由。
這章沒有新實驗,是一份參考。建議看完之後把決策圖和成本表存下來 —— 本系列後面每次遇到「這個要存哪」的時候都會回頭用它。
一句話總結整個 Cloudflare 儲存生態的設計:
每個產品都在一個不同的維度上收費,而那個維度就是它的物理限制。
KV 按操作次數收費,因為它是全球複製的;D1 按掃描列數收費,因為它是單執行緒的;R2 按操作類別收費,因為列表和讀取的成本結構不同;DO 按 GB-秒收費,因為它是有生命週期的行程。看懂計費維度,就看懂了這個產品該用在哪。
一、五個產品的本質
Section titled “一、五個產品的本質”| 本質 | 一句話 | |
|---|---|---|
| Workers Cache(第 6 篇) | Worker 前面的快取 | 命中就不執行你的程式碼 |
| KV(第 8 篇) | 全球複製的唯讀快取 | 讀多寫少、可接受最終一致 |
| D1(第 9 篇) | 邊緣上的 SQLite | 唯一有關聯查詢和唯一約束的選項 |
| R2(第 12 篇) | 物件儲存 | 存位元組,不存索引 |
| Durable Objects(第 14 篇) | 全球唯一、單執行緒的 actor | 解決協調問題,不是儲存問題 |
| Hyperdrive(第 28 篇) | 既有資料庫的連線加速層 | 你已經有 Postgres/MySQL 時的橋 |
最容易被誤解的是 DO。它看起來像「有狀態的儲存」,但它真正解決的是協調:原子遞增、單一真相、即時廣播、per-entity 排程。如果你的需求不需要「同一時間只有一個執行緒在改這份狀態」,那你大概不需要 DO。
二、一致性模型
Section titled “二、一致性模型”這是選型的第一道篩子。
| 一致性 | read-your-own-write | 跨 colo | |
|---|---|---|---|
| Workers Cache | 無保證(快取) | — | 分層,非全域一致 |
| KV | 最終(可能 60 秒以上) | 不保證 | 是(最終) |
| D1(無複本) | 強 | 是 | 單一 primary |
| D1(有複本) | 最終,除非用 withSession() | 靠 bookmark | 六個區域 |
| R2 | put 強一致 | 是 | 是 |
| DO | 序列化(單執行緒) | 是 | 單一實例,全球唯一 |
| Hyperdrive | 由你的來源資料庫決定 | 是(但快取不會因寫入失效) | 無 |
三個值得單獨記住的:
- KV 的 read-your-own-write 不保證。 登出、撤銷、權限變更這類「必須立即生效」的語意,KV 給不了。
- D1 開了複本但沒用
withSession(),一致性沒變(因為全部還是打 primary),但你也沒得到延遲好處。 - Hyperdrive 的 query cache 不會被寫入失效(第 28 篇)。read-after-write 的路徑要用第二組關閉快取的設定。
三、計費維度對照
Section titled “三、計費維度對照”這是本篇最有價值的一張表。注意每一行的「計費單位」都不一樣 —— 這就是為什麼不能只比「每 GB 多少錢」。
| 計費單位 | 讀 | 寫 | 儲存 | |
|---|---|---|---|---|
| KV | 操作次數 | $0.50 / 百萬 | $5.00 / 百萬 | $0.50 / GB-月 |
| D1 | 掃描的列數 | $0.001 / 百萬列 | $1.00 / 百萬列 | $0.75 / GB-月 |
| R2 | 操作類別 | Class B $0.36 / 百萬 | Class A $4.50 / 百萬 | $0.015 / GB-月 |
| DO | GB-秒 + 請求 | $0.15/百萬請求 + $12.50/百萬 GB-s | 同左 | $0.20 / GB-月 |
| Workers Cache | 不另計費 | — | — | — |
(Paid 方案,含免費額度之後的費率,2026-07 查證。)
四個由此推出的結論
Section titled “四個由此推出的結論”① KV 的寫入比讀取貴 10 倍。 任何寫入頻繁的東西都不該放 KV —— 而且它還有每個 key 每秒 1 次的硬上限。
② D1 的成本由索引決定,不由資料量決定。 第 9 篇實測:同一個查詢,有索引 rows_read: 1,沒索引 rows_read: 2000。同一份資料、同一個查詢、2000 倍價差。
③ R2 的儲存最便宜(比 KV 便宜 33 倍),但列表最貴。 ListObjects 是 Class A,是 GetObject 的 12.5 倍。所以大檔案放 R2,清單放 D1。
④ DO 的成本是時間,不是次數。 閒置的 DO 如果用了 WebSocket hibernation(第 16 篇)就幾乎不計費;沒用的話會持續累積 GB-秒。
渲染版(可列印):
assets/13-storage-decision-tree.svg
三個路徑上的提醒:
- 「熱路徑 + 可容忍舊資料」永遠先想快取,再想 KV。 第 8 篇的成本試算:加了 Workers Cache 之後 KV 讀取從 1,000 萬次降到 100 萬次,成本從 $5.00 變 $0.50。
- 走到「單一 DO 是全域瓶頸」代表切分方式有問題。 一個 DO 大約每秒 1,000 個請求就是上限,而且沒辦法橫向擴展。
- 「簡單也是一種價值」不是玩笑。 資料量不大、流量不高的時候,全部放 D1 是完全合理的選擇。過早的儲存分層是真實的成本。
五、用 LinkForge 的六個真實需求走一遍
Section titled “五、用 LinkForge 的六個真實需求走一遍”| # | 需求 | 選擇 | 決定性理由 |
|---|---|---|---|
| 1 | slug → URL 的 redirect 查詢 | Workers Cache → KV | 極端讀多寫少、可容忍 60 秒、每秒數千次。D1 單執行緒撐不住,DO 太貴 |
| 2 | 連結的真相來源(唯一約束、租戶關聯) | D1 | 只有 D1 有 UNIQUE INDEX 和 batch() 的原子性 |
| 3 | 即時點擊計數 | Durable Object | KV 每個 key 每秒 1 寫直接判死;需要原子遞增 |
| 4 | 匯出的 CSV / OG 圖 / QR code | R2 + D1 清單 | 位元組放最便宜的地方,list() 是 Class A 所以清單放 D1 |
| 5 | 使用者 session | DO(第 27 篇) | 「登出所有裝置」需要立即失效,KV 的最終一致給不了 |
| 6 | 每租戶速率限制 | ratelimits binding(第 4 篇) | 內建、本機可跑、不需要額外儲存;要精確全域計數才升級到 DO |
注意第 4 項的拆法:同一個需求同時用了兩個產品。這是常態而不是例外 —— R2 存位元組、D1 存「有哪些位元組」。第 12 篇的成本試算顯示,如果清單改用 list(),光那一項就比所有其他操作加起來還貴。
六、反模式速查
Section titled “六、反模式速查”| 反模式 | 為什麼錯 | 改用 |
|---|---|---|
| KV 當計數器 | 沒有原子遞增;每個 key 每秒 1 寫 | DO(第 14 篇) |
| KV 當 session store(需要撤銷) | read-your-own-write 不保證 | DO 或 D1 |
| KV 當 queue | 沒有排序、沒有 ack | Queues(第 19 篇) |
| D1 在熱路徑 | 單執行緒,吞吐量 = 1 / 查詢時間 | Cache → KV |
| D1 沒建索引 | rows_read 就是帳單,實測 2000 倍差 | 加索引 + EXPLAIN QUERY PLAN |
db.transaction() on D1 | 型別會過、runtime 會死(第 10 篇) | batch() 或 DO |
R2 的 list() 當查詢 | Class A,12.5 倍價 | 清單存 D1 |
| R2 熱點寫入 | 每個 key 每秒 1 次,超過 429 | DO |
| 單一 DO 服務全部流量 | 每秒約 1,000 請求上限,無法橫向擴展 | 按實體切分 |
| DO 不用 hibernation 的 WebSocket | 持續累積 GB-秒 | ctx.acceptWebSocket()(第 16 篇) |
| Hyperdrive 讀寫同一組設定 | 快取不會因寫入失效 | 第二組 --caching-disabled |
用 r2.dev 服務 production | 限速,且繞過 WAF / Access | 自訂網域 |
七、選錯了怎麼辦
Section titled “七、選錯了怎麼辦”選型不是一次性決定。這是幾條實際的遷移路徑:
| 從 → 到 | 難度 | 做法 |
|---|---|---|
| D1 → 加一層 KV 快取 | 低 | 加投影寫入 + 讀取路徑改順序。D1 仍是真相來源 |
| KV → D1 | 中 | KV 通常沒有完整資料(value 是投影),要從別處重建 |
| D1 計數器 → DO | 中 | 需要一次資料遷移 + 雙寫過渡期 |
| 單一 DO → 多個 DO | 高 | 切分鍵一旦選錯,重新切分等於重寫 |
| R2 全掃 → D1 索引 | 低 | 一次性 list() 灌進 D1,之後只寫 D1 |
最貴的錯誤是 DO 的切分鍵。 其他都可以增量修,切分鍵錯了要重來。所以第 14 篇會花很多篇幅在「怎麼選 DO 的 id」。
一個實用的原則:先用 D1 做對,再用快取做快。 D1 有最完整的語意(約束、關聯、原子性),先確保正確性,效能問題出現時再往前加 KV 或 Cache —— 這個方向的遷移成本最低。反過來(先用 KV 圖快,後來發現需要約束)就痛苦得多。
八、一張速查表
Section titled “八、一張速查表”如果只能記一件事,記這個:
需要「同一時刻只有一個人在改」 → Durable Object需要「不能重複 / 關聯查詢」 → D1需要「存很多位元組」 → R2(清單放 D1)需要「全球讀取快、寫得少」 → KV(前面加 Workers Cache)需要「整個回應可快取」 → Workers Cache(最便宜)已經有 Postgres / MySQL → Hyperdrive以及三個成本直覺:
- KV 寫比讀貴 10 倍
- D1 的錢花在索引上,不在資料量上
- R2 的
list()比get()貴 12.5 倍
Part 2 回顧
Section titled “Part 2 回顧”六篇下來,LinkForge 的資料層定案了:
Workers Cache ← 第 6 篇:命中就不執行 Worker ↓ miss redirect ────────► KV ← 第 8 篇:slug → URL + metadata ↑ 投影 API / dashboard ──► D1 (Drizzle) ← 第 9–11 篇:真相來源 ↑ rollup 點擊事件 ────────► DO ← 第 14–17 篇(Part 3)
匯出 / OG 圖 ─────► R2 + D1 清單 ← 第 12 篇每一個箭頭都有理由,而且每個理由都可以用一個計費維度或一致性保證來解釋。
Part 2 交付物總結:packages/db 完整落地、apps/redirector 接上 KV、apps/api 接上 D1、R2 的 key 規則與下載路由、以及三份護欄(rows_read 記錄、query plan 測試、bookmark 格式驗證)。
- Cloudflare 官方的儲存選項比較
- 本系列各章的「限制與計費」小節 —— 那些數字才是決策的依據
下一篇:14. Durable Objects:邊緣上的 actor —— Part 3 開始。DO 解決的是協調問題而不是儲存問題,而選錯切分鍵是本系列最貴的錯誤。