CAP 定理
C、A、P 是一致性(Consistency)、可用性(Availability)、分區容錯(Partition tolerance)。網路分區一定會發生,發生的時候只能選一個:拒絕請求以保住一致性,或繼續服務但兩邊的資料可能分歧。常聽到的「三取二」,其實是這個取捨的簡化說法。
做法
- C一致性(Consistency)
- 每次讀取都拿到最新一次成功寫入的值,好像整個系統只有一份資料。
- A可用性(Availability)
- 每個送到正常節點的請求都會得到回應,而不是錯誤或一直等。
- P分區容錯(Partition tolerance)
- 節點之間的網路斷掉、訊息遺失時,系統仍然繼續運作。
正確讀到錯的值被拒絕上排實心:累加;下排空心:讀取
有回應的請求
90%
讀到錯的值
0 / 75
遺失的累加
0 / 67
平均延遲 A / B 區
2 / 162 ms
網路斷掉的 4 秒裡,B 區只剩 3 個副本中的 1 個、湊不到多數,15 個請求被拒絕(整體 90% 有回應)。A 區有 2 個、照常服務。沒有任何一次讀到錯的值:一致性是靠犧牲少數那一側的可用性換來的。
延遲模型:跨區單程 80 ms、區內 1 ms。B 區的 CP 請求計一次跨區往返加兩次區內傳遞(162 ms);平均值不含被拒絕的請求。CP 假設已有領導者及共用的已提交日誌;只確認連得到多數副本,不足以讓獨立副本具有線性一致性。未模擬選主、日誌複製細節、排隊或儲存延遲。
亮起來的是這一步執行的程式碼
// CP sketch: one shared committed value, assuming an established leader/log.// This shows the availability gate only; it is not a consensus protocol.// Independent instances do not replicate or become linearizable by this check.class MajorityReplica { private value = 0; constructor(private replicas: number) {} increment(reachable: number): number { if (reachable * 2 <= this.replicas) throw new Error("unavailable"); this.value += 1; return this.value; } read(reachable: number): number { if (reachable * 2 <= this.replicas) throw new Error("unavailable"); return this.value; }} // AP, last writer wins: one value and its timestamp per replica.type Versioned = { value: number; ts: number; site: string }; class LwwReplica { state: Versioned = { value: 0, ts: -1, site: "" }; constructor(private site: string) {} increment(now: number): Versioned { this.state = { value: this.state.value + 1, ts: now, site: this.site }; return this.state; } merge(remote: Versioned): void { const s = this.state; if (remote.ts > s.ts || (remote.ts === s.ts && remote.site > s.site)) { this.state = remote; } }} // AP, CRDT: each replica counts only its own increments; merging keeps// the largest count seen per replica, and the value is the sum.class GCounter { counts: Record<string, number> = {}; constructor(private site: string) {} increment(): void { this.counts[this.site] = (this.counts[this.site] ?? 0) + 1; } merge(remote: Record<string, number>): void { for (const [site, n] of Object.entries(remote)) { this.counts[site] = Math.max(this.counts[site] ?? 0, n); } } value(): number { return Object.values(this.counts).reduce((a, b) => a + b, 0); }}模型假設與範圍
- 這是可重現的教學模型;延遲、容量、故障率與工作負載是設定或樣本,不能直接當作正式系統的效能承諾。
- 固定三副本、兩個區域與分割時段;一致性指模型裡的讀取語意,可用性是非故障節點是否回覆。不是把延遲或一般可靠性歸成 CAP 選項。
什麼時候用
- 選 CP(Consistency+Partition tolerance,分區時保住一致性):絕不能讀到錯的值或重複執行的時候——庫存扣減、帳戶餘額、分散式鎖、選主。寧可在分區時拒絕少數那一側的請求。
- 選 AP(Availability+Partition tolerance,分區時保住可用性):永遠要能回應、資料可以事後合併的時候——購物車、按讚數、動態、DNS。要先想好怎麼合併(CRDT,Conflict-free Replicated Data Type;或保留多個版本讓應用決定)。
- 沒有分區的平常日子也要選:要低延遲還是強一致。這是 PACELC(Partition → Availability/Consistency,Else → Latency/Consistency)補上的那一半,而且是比較常發生的那一半。
和其他主題的關係
- 延伸閱讀
- 一致性與 Quorum資料庫複製
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| CP:遠端區域的一次讀寫 要等多數派確認;分區時湊不到多數就直接失敗(✕) | 1 RTT | ✕ |
| AP:一次讀寫 在本地完成,不等其他區域 | O(1) | O(1) |
| 最後寫入勝出的合併 | O(1) | O(1) |
| CRDT 計數器的合併 R 是副本數:每個副本一格 | O(R) | O(R) |
空間:O(R),CRDT 計數器每個副本記一個數字;最後寫入勝出只要一個值和時間戳
和其他做法比
| 網路 | 有回應 | 讀到錯的值 | 遺失的累加 | B 區平均延遲 | |
|---|---|---|---|---|---|
| CP:多數決 | 正常 | 100% | 0 / 75 | 0 / 75 | 162 ms |
| CP:多數決(分區) | 斷 4 秒 | 90% | 0 / 75 | 0 / 67 | 162 ms |
| AP:最後寫入勝出 | 正常 | 100% | 70 / 75 | 15 / 75 | 1 ms |
| AP:最後寫入勝出(分區) | 斷 4 秒 | 100% | 70 / 75 | 30 / 75 | 1 ms |
| AP:CRDT 計數器 | 正常 | 100% | 20 / 75 | 0 / 75 | 1 ms |
| AP:CRDT 計數器(分區) | 斷 4 秒 | 100% | 42 / 75 | 0 / 75 | 1 ms |
同一串 150 個請求(60% 來自 A 區、一半讀一半累加),兩區之間單程 80 ms、區內 1 ms、時鐘一致。「讀到錯的值」是讀到的數字不等於當下所有已確認累加的次數;「遺失的累加」是回報成功、但所有副本最後都看不到的累加。
真實世界裡的它
- CP:ZooKeeper、etcd、Consul 用多數決(ZAB〔ZooKeeper Atomic Broadcast〕、Raft);Spanner、CockroachDB 預設強一致。
- AP:Cassandra、DynamoDB(一致性可調)、Riak(內建 CRDT 型別)、DNS。
- Amazon 的 Dynamo 論文讓購物車永遠能加東西,衝突時保留兩個版本、合併時取聯集。
取捨與陷阱
- 「三取二」是誤導:網路分區不是可以不選的選項,所謂的 CA(同時要 Consistency 與 Availability)只存在於沒有網路的單機。真正的選擇只有一個:分區發生時要 C 還是 A。
- 最後寫入勝出會悄悄丟資料:回報成功的寫入事後消失,連錯誤訊息都沒有(上面的計數器)。時鐘不同步會讓它更糟。
- CAP 的 C 是「線性一致性」(每次讀都看到最新寫入),和 ACID 的 C(資料符合約束)是兩回事。
- 很多系統的一致性是可調的(每次請求選 quorum 大小),不是整個系統只能貼一個 CP 或 AP 的標籤。