透過理解儲存引擎、複寫、分割、交易與一致性模型來設計資料系統。當使用者提到「資料庫選擇」、「該用哪個資料庫」、「SQL 或 NoSQL」、「複寫延遲」、「分割策略」、「一致性 vs 可用性」、「串流處理」、「ACID 交易」、「最終一致性」、「我的查詢在規模化時變慢」或「跨副本資料不一致」時使用。也適用於選擇資料儲存、設計資料管線或除錯分散式系統一致性問題。涵蓋資料模型、批次/串流處理與分散式共識。如需系統設計,請參閱 system-design。如需韌性,請參閱 release-it。
Designing Data-Intensive Applications 框架
一套有原則的方法,用於建構可靠、可擴展且可維護的資料系統。在選擇資料庫、設計綱要、架構分散式系統或推理一致性與容錯時,應用這些原則。
核心原則
資料比程式碼長壽。 應用程式會被重寫,框架來來去去,但資料會持續數十年——優先考慮資料層的長期正確性、持久性與可演化性。大多數應用程式是資料密集型的,而非計算密集型:困難的問題在於資料量、複雜度與變更速率,而明確的一致性/可用性/延遲取捨,是區分穩健系統與脆弱系統的關鍵。
評分
目標:10/10。 根據下方七個快速診斷項目為資料架構評分:每個項目若回答「是」且有證據(經過深思熟慮、有文件記錄的取捨)則給約 1.4 分,若回答「否」或未知則給 0 分。
- 9-10: 每個領域的選擇——資料模型、儲存引擎、複寫、分割、隔離、衍生資料、故障處理——都是經過深思熟慮、有文件記錄,且符合實際的讀取/寫入/一致性需求;已測試過容錯移轉。
- 5-6: 核心選擇已做出,但兩到三個診斷項目失敗——例如預設隔離層級未知、熱鍵風險未處理,或容錯移轉未測試。
- <=3: 選擇基於熟悉度而非需求;忽略了故障模式(複寫延遲、寫入偏斜、熱分割區)且意外複雜度佔主導。
報告當前分數、哪些診斷項目失敗,以及達到 10/10 所需的改進。
DDIA 框架
推理資料密集型系統的七個領域:
1. 資料模型與查詢語言
核心概念: 資料模型塑造了你對問題的思考方式。關聯式、文件式與圖形模型各自施加不同的約束,並啟用不同的查詢模式。
為何有效: 選擇錯誤的資料模型會迫使應用程式碼補償表示上的不匹配,隨著時間增加意外複雜度。
關鍵見解:
- 關聯式模型擅長多對多關係與臨時查詢;文件模型擅長一對多關係與局部性;圖形模型擅長對互連資料的遞迴遍歷
- 寫入時綱要(關聯式)能及早捕獲錯誤;讀取時綱要(文件)提供靈活性
- 多語言持久化——針對不同存取模式使用不同儲存——通常是正確答案
- 物件-關聯阻抗不匹配是真實成本;文件模型對自包含聚合體可減少此問題
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 具有巢狀資料的使用者個人資料 | 自包含聚合體的文件模型 | 個人資料、地址與偏好設定放在一個 MongoDB 文件中 |
| 社交網路連線 | 關係遍歷的圖形模型 | Neo4j Cypher:MATCH (a)-[:FOLLOWS*2]->(b) 用於朋友的朋友 |
| 需要 JOIN 的財務帳本 | 參照完整性的關聯式模型 | PostgreSQL 在帳戶、交易、分錄之間的外鍵 |
在選擇關聯式 vs 文件式 vs 圖形模型或評估讀取時綱要時,請參閱 references/data-models.md——包含完整的取捨矩陣與查詢語言比較。
2. 儲存引擎
核心概念: 儲存引擎在讀取效能與寫入效能之間取捨。日誌結構引擎(LSM 樹)最佳化寫入;頁面導向引擎(B 樹)平衡讀取與寫入。
關鍵見解:
- LSM 樹:僅附加寫入、定期壓縮、出色的寫入吞吐量、較高的讀取放大
- B 樹:就地更新、可預測的讀取延遲、頁面分裂導致的寫入放大
- 寫入放大(一次邏輯寫入導致多次實體寫入)對有限寫入週期的 SSD 很重要
- 列式儲存透過壓縮與向量化處理大幅改善分析查詢
- 記憶體資料庫之所以快,是因為避免了編碼開銷,而非因為避免磁碟
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 高寫入吞吐量 | LSM 樹引擎 | Cassandra 或 RocksDB 用於每秒 10 萬次以上寫入的時序資料擷取 |
| 混合讀寫 OLTP | B 樹引擎 | PostgreSQL B 樹索引用於交易性點查詢 |
| 分析查詢 | 列式儲存 | ClickHouse 或 Parquet 用於掃描數十億行、少數幾欄 |
當工作負載受讀取或寫入限制,或必須選擇索引時,請參閱 references/storage-engines.md——包含寫入/讀取路徑圖、壓縮策略、列式儲存,以及基於基準測試的決策程序。
3. 複寫
核心概念: 複寫將資料副本保存在多台機器上,以實現容錯、可擴展性與降低延遲。核心挑戰在於一致地處理變更。
為何有效: 每種複寫策略都在一致性、可用性與延遲之間取捨。明確做出取捨可防止僅在負載或故障下才出現的微妙異常。
關鍵見解:
- 單領導者:簡單、可能達到強一致性,但領導者是瓶頸與單點故障
- 多領導者:跨資料中心更好的寫入可用性,但複雜的衝突解決
- 無領導者:透過法定人數讀寫達到最高可用性,但需要謹慎的衝突處理
- 複寫延遲導致讀取自己寫入、單調讀取與因果違反
- 同步複寫保證持久性但增加延遲;非同步複寫在容錯移轉時有資料遺失風險
- CRDT 與最後寫入勝出以非常不同的正確性保證解決衝突
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 讀取密集的網頁應用 | 單領導者搭配唯讀副本 | PostgreSQL 主節點 + 唯讀副本,後端使用 pgBouncer |
| 多區域寫入 | 多領導者複寫 | CockroachDB 或 Spanner 搭配有限過時性 |
| 購物車可用性 | 無領導者搭配合併 | DynamoDB 搭配最後寫入勝出或應用層級的購物車合併 |
在選擇單/多/無領導者或除錯過時讀取時,請參閱 references/replication.md——包含延遲異常、法定人數計算、衝突解決與 CRDT。
4. 分割
核心概念: 分割(分片)將資料分散到多個節點,每個節點處理一部分,從而實現超越單一機器的水平擴展。
關鍵見解:
- 鍵範圍分割支援高效的範圍掃描,但可能因順序鍵而產生熱點
- 雜湊分割均勻分散負載,但破壞排序順序,使範圍查詢成本高昂
- 本地次要索引需要分散-收集查詢;全域次要索引需要跨分割更新
- 即使使用雜湊,當單一鍵極受歡迎時仍會發生熱點(名人問題)
- 重新平衡策略:固定分割數、動態分裂或與節點數成比例
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 時序資料 | 按時間+來源的鍵範圍分割 | 按 (sensor_id, date) 分割以避免當日寫入熱點 |
| 大規模使用者資料 | 基於使用者 ID 的雜湊分割 | Cassandra 對 user_id 進行一致性雜湊以均勻分佈 |
| 名人/熱鍵問題 | 鍵分割搭配隨機後綴 | 為熱鍵附加隨機數字,將讀取分散到 10 個子分割 |
在進行分片或處理熱鍵時,請參閱 references/partitioning.md——包含重新平衡策略、請求路由,以及本地 vs 全域次要索引的取捨。
5. 交易與一致性
核心概念: 交易提供安全保證(ACID),簡化應用程式碼,讓你在交易範圍內假設故障與並發不存在。
為何有效: 沒有交易,每段應用程式碼都必須處理部分失敗、競爭與並發修改。交易將該複雜度移入資料庫,一次正確處理。
關鍵見解:
- 隔離層級是一個光譜:讀取未提交、讀取已提交、快照隔離、可序列化
- 大多數資料庫預設為讀取已提交或快照隔離——而非可序列化——因此你必須了解這允許哪些異常
- 寫入偏斜:兩個交易讀取相同資料、做出決定並寫入不同記錄——沒有列鎖可以防止
- 可序列化快照隔離(SSI)樂觀地提供完整可序列化性:無阻塞,但衝突時中止;兩階段鎖定在競爭下會阻塞與死結
- 分散式交易(兩階段提交)昂貴且脆弱;改為設計單分割操作
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 帳戶餘額轉帳 | 可序列化交易 | BEGIN; UPDATE accounts ... -100 WHERE id=1; UPDATE accounts ... +100 WHERE id=2; COMMIT; |
| 庫存預留 | SELECT FOR UPDATE 以防止寫入偏斜 | 在遞減前執行 SELECT stock FROM items WHERE id = X FOR UPDATE |
| 跨服務操作 | Saga 而非分散式交易 | 扣款、預留庫存;失敗時執行補償退款 |
在設定隔離層級或追蹤並發錯誤時,請參閱 references/transactions.md——包含各隔離層級的異常表、寫入偏斜範例、2PL vs SSI,以及分散式交易陷阱。
6. 批次與串流處理
核心概念: 批次處理大量轉換有界資料集;串流處理持續轉換無界事件串流。兩者都計算衍生資料。
為何有效: 將記錄系統與衍生資料(快取、索引、具體化視圖)分離,讓各自能獨立最佳化,並在需求變更時從來源重建。
關鍵見解:
- MapReduce 概念簡單但操作麻煩;資料流引擎(Spark、Flink)以任意 DAG 將其泛化
- 變更資料捕獲(CDC)將資料庫寫入轉換為下游系統可消費的串流
- 串流-表格二元性:串流是表格的變更日誌;表格是串流的具體化狀態
- 恰好一次語義需要冪等操作或交易性輸出
- 時間視窗(翻轉、跳躍、會話)對於聚合無界串流至關重要
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 每日分析管線 | 使用 Spark 的批次處理 | 從 S3 讀取當日事件、聚合、寫入倉儲 |
| 即時詐欺偵測 | 使用 Flink 的串流處理 | Kafka 支付事件、基於 5 秒翻轉視窗的規則 |
| 同步搜尋索引 | 變更資料捕獲 | Debezium 捕獲 PostgreSQL WAL,Kafka 饋送 Elasticsearch |
| 稽核軌跡/事件重播 | 事件溯源 | 儲存 OrderPlaced、OrderShipped 事件;透過重播重建狀態 |
在設計管線或從記錄系統衍生資料時,請參閱 references/batch-stream.md——包含資料流引擎、CDC 接線、視窗化與恰好一次技術。
7. 可靠性與容錯
核心概念: 故障無可避免;失效則非必然。可靠的系統即使個別元件故障,仍能持續正確運作。為故障設計,而非對抗故障。
關鍵見解:
- 故障是單一元件偏離規格;失效是整個系統停止——容錯防止前者變成後者
- 硬體故障隨機且獨立;軟體故障相關且系統性(更危險)
- 人為錯誤是停機的主要原因——最小化犯錯機會,最大化復原能力
- 超時是基本的故障偵測器,但調整困難:太短導致誤判,太長延遲復原
- 安全性質(壞事永不發生)必須始終成立;活躍性(好事最終發生)可能暫時違反
- 拜占庭容錯在區塊鏈之外很少需要;假設崩潰-停止或崩潰-復原
程式碼應用:
| 情境 | 模式 | 範例 |
|---|---|---|
| 服務通訊 | 超時 + 重試與退避 | retry(max=3, backoff=exponential(base=1s, max=30s)) 搭配抖動 |
| 領導者選舉 | 共識演算法(Raft/Paxos) | etcd 或 ZooKeeper 用於分散式鎖與領導者選舉 |
| 優雅降級 | 斷路器 | Resilience4j:在 10 秒視窗內失敗率達 50% 時開啟電路 |
在調整超時/重試或加入共識時,請參閱 references/fault-tolerance.md——包含故障分類、超時調整數學、Raft/Paxos 機制,以及安全性/活躍性保證。
常見錯誤
| 錯誤 | 為何失敗 | 修正 |
|---|---|---|
| 根據流行度選擇資料庫 | 引擎有根本不同的取捨 | 將儲存引擎匹配實際讀寫模式 |
| 忽略複寫延遲 | 過時讀取、幻讀、遺失更新 | 實作讀取自己寫入與單調讀取保證 |
| 到處使用分散式交易 | 2PC 慢且脆弱;協調者是單點故障 | 設計單分割操作;跨服務使用 saga |
| 對所有東西使用雜湊分割 | 破壞範圍查詢能力 | 時序資料使用鍵範圍分割;複合鍵實現局部性 |
| 假設可序列化隔離 | 預設較弱;寫入偏斜出現在生產環境 | 檢查實際預設值;在需要處使用顯式鎖定 |
| 混淆批次與串流 | 錯誤工具增加延遲或浪費複雜度 | 將處理模型匹配資料有界性與延遲需求 |
| 將所有故障視為可復原 | 損壞與拜占庭故障需要不同處理 | 分類故障;為每類設計復原策略 |
快速診斷
| 問題 | 若否 | 行動 |
|---|---|---|
| 你能解釋為何選擇這個資料庫而非其他嗎? | 選擇基於熟悉度而非需求 | 評估資料模型契合度、讀寫比、一致性需求、擴展路徑 |
| 你知道資料庫的預設隔離層級嗎? | 潛在的並發錯誤 | 查閱文件;測試寫入偏斜與幻讀 |
| 你的複寫策略是否明確選擇? | 隱含的一致性/持久性假設 | 記錄同步 vs 非同步、容錯移轉行為、延遲容忍度 |
| 你的系統能否處理熱分割鍵? | 一個熱門實體可能拖垮叢集 | 為熱鍵加入鍵分割或負載捨棄 |
| 你是否將記錄系統與衍生資料分離? | 每次變更需要遷移所有東西 | 引入 CDC 或事件溯源以解耦 |
| 超時與重試是否經過調整而非使用預設值? | 串聯故障或不必要的延遲 | 測量 p99;將超時設在 p99 以上、串聯閾值以下 |
| 你是否在生產條件下測試過容錯移轉? | 復原計畫僅是理論 | 執行混亂實驗:殺死領導者、分割網路、填滿磁碟 |
延伸閱讀
如需包含詳細圖表與研究參考的完整論述:
- 《Designing Data-Intensive Applications》 by Martin Kleppmann
關於作者
Martin Kleppmann 是劍橋大學的分散式系統研究員,曾任職於 LinkedIn 與 Rapportive,以 CRDT 與本地優先軟體的研究聞名。他的著作《Designing Data-Intensive Applications》(2017 年)是建構資料系統工程師的權威參考,因使分散式系統概念易於理解且實用而備受讚譽。






