CAP理論的證明參見:
本文轉自:CAP 理論十二年回顧:"規則"變了
CAP 理論斷言任何基于網絡的數據共享系統,最多只能滿足數據一致性、可用性、分區容忍性三要素中的兩個要素。但是通過顯式處理分區情形,系統設計師可以做到優化數據一致性和可用性,進而取得三者之間的平衡。
自打引入 CAP 理論的十幾年里,設計師和研究者已經以它為理論基礎探索了各式各樣新穎的分布式系統,甚至到了濫用的程度。NoSQL 運動也將 CAP 理論當作對抗傳統關系型數據庫的依據。
CAP 理論主張任何基于網絡的數據共享系統,都最多只能擁有以下三條中的兩條:
- 數據一致性(C),等同于所有節點訪問同一份最新的數據副本;
- 對數據更新具備高可用性(A);
- 能容忍網絡分區(P)。
CAP 理論的表述很好地服務了它的目的,即開闊設計師的思路,在多樣化的取舍方案下設計出多樣化的系統。在過去的十幾年里確實涌現了不計其數的新系統,也隨之在數據一致性和可用性的相對關系上產生了相當多的爭論。“三選二”的公式一直存在著誤導性,它會過分簡單化各性質之間的相互關系。現在我們有必要辨析其中的細節。實際上只有“在分區存在的前提下呈現完美的數據一致性和可用性”這種很少見的情況是 CAP 理論不允許出現的。
雖然設計師仍然需要在分區的前提下對數據一致性和可用性做取舍,但具體如何處理分區和恢復一致性,這里面有不計其數的變通方案和靈活度。當代 CAP 實踐應將目標定為針對具體的應用,在合理范圍內最大化數據一致性和可用性的“合力”。這樣的思路延伸為如何規劃分區期間的操作和分區之后的恢復,從而啟發設計師加深對 CAP 的認識,突破過去由于 CAP 理論的表述而產生的思維局限。
為什么“三選二”公式有誤導性
理解 CAP 理論的最簡單方式是想象兩個節點分處分區兩側。允許至少一個節點更新狀態會導致數據不一致,即喪失了 C 性質。如果為了保證數據一致性,將分區一側的節點設置為不可用,那么又喪失了 A 性質。除非兩個節點可以互相通信,才能既保證 C 又保證 A,這又會導致喪失 P 性質(ps:本段開頭到現在實際是CAP理論的簡單證明)。一般來說跨區域的系統,設計師無法舍棄 P 性質,那么就只能在數據一致性和可用性上做一個艱難選擇。不確切地說,NoSQL 運動的主題其實是創造各種可用性優先、數據一致性其次的方案;而傳統數據庫堅守 ACID 特性(原子性、一致性、隔離性、持久性),做的是相反的事情。下文“ACID、BASE、CAP”小節詳細說明了它們的差異。
事實上,CAP 理論本身就是在類似的討論中誕生的。早在 1990 年代中期,我和同事構建了一系列的基于集群的跨區域系統(實質上是早期的云計算),包括搜索引擎、緩存代理以及內容分發系統。從收入目標以及合約規定來講,系統可用性是首要目標,因而我們常規會使用緩存或者事后校核更新日志來優化系統的可用性。盡管這些策略提升了系統的可用性,但這是以犧牲系統數據一致性為代價的。
關于“數據一致性 VS 可用性”的第一回合爭論,表現為 ACID 與 BASE 之爭。當時 BASE 還不怎么被人們接受,主要是大家看重 ACID 的優點而不愿意放棄。提出 CAP 理論,目的是證明有必要開拓更廣闊的設計空間,因此才有了“三選二”公式。CAP 理論最早在 1998 年秋季提出,1999 年正式發表3,并在 2000 年登上 Symposium on Principles of Distributed Computing 大會的主題演講4,最終確立了該理論的正確性。
“三選二”的觀點在幾個方面起了誤導作用,詳見下文“CAP 之惑”小節的解釋。首先,由于分區很少發生,那么在系統不存在分區的情況下沒什么理由犧牲 C 或 A。其次,C 與 A 之間的取舍可以在同一系統內以非常細小的粒度反復發生,而每一次的決策可能因為具體的操作,乃至因為牽涉到特定的數據或用戶而有所不同。最后,這三種性質都可以在程度上衡量,并不是非黑即白的有或無。可用性顯然是在 0% 到 100% 之間連續變化的,一致性分很多級別,連分區也可以細分為不同含義,如系統內的不同部分對于是否存在分區可以有不一樣的認知。
要探索這些細微的差別,就要突破傳統的分區處理方式,而這是一項根本性的挑戰。因為分區很少出現,CAP 在大多數時候允許完美的 C 和 A。但當分區存在或可感知其影響的情況下,就要預備一種策略去探知分區并顯式處理其影響。這樣的策略應分為三個步驟:探知分區發生,進入顯式的分區模式以限制某些操作,啟動恢復過程以恢復數據一致性并補償分區期間發生的錯誤。
ACID、BASE、CAP
ACID 和 BASE 代表了兩種截然相反的設計哲學,分處一致性 - 可用性分布圖譜的兩極。ACID 注重一致性,是數據庫的傳統設計思路。我和同事在 1990 年代晚期提出 BASE,目的是抓住當時正逐漸成型的一些針對高可用性的設計思路,并且把不同性質之間的取舍和消長關系擺上臺面。現代大規模跨區域分布的系統,包括云在內,同時運用了這兩種思路。
這兩個術語都好記有余而精確不足,出現較晚的 BASE 硬湊的感覺更明顯,它是“Basically Available, Soft state, Eventually consistent(基本可用、軟狀態、最終一致性)”的首字母縮寫。其中的軟狀態和最終一致性這兩種技巧擅于對付存在分區的場合,并因此提高了可用性。
CAP 與 ACID 的關系更復雜一些,也因此引起更多誤解。其中一個原因是 ACID 的 C 和 A 字母所代表的概念不同于 CAP 的 C 和 A。還有一個原因是選擇可用性只部分地影響 ACID 約束。ACID 四項特性分別為:
原子性(A)。所有的系統都受惠于原子性操作。當我們考慮可用性的時候,沒有理由去改變分區兩側操作的原子性。而且滿足 ACID 定義的、高抽象層次的原子操作,實際上會簡化分區恢復。
一致性(C)。ACID 的 C 指的是事務不能破壞任何數據庫規則,如鍵的唯一性。與之相比,CAP 的 C 僅指單一副本這個意義上的一致性,因此只是 ACID 一致性約束的一個嚴格的子集。ACID 一致性不可能在分區過程中保持,因此分區恢復時需要重建 ACID 一致性。推而廣之,分區期間也許不可能維持某些不變性約束,所以有必要仔細考慮哪些操作應該禁止,分區后又如何恢復這些不變性約束。
隔離性(I)。隔離是 CAP 理論的核心:如果系統要求 ACID 隔離性,那么它在分區期間最多可以在分區一側維持操作。事務的可串行性(serializability)要求全局的通信,因此在分區的情況下不能成立。只要在分區恢復時進行補償,在分區前后保持一個較弱的正確性定義是可行的。
持久性(D)。犧牲持久性沒有意義,理由和原子性一樣,雖然開發者有理由(持久性成本太高)選擇 BASE 風格的軟狀態來避免實現持久性。這里有一個細節,分區恢復可能因為回退持久性操作,而無意中破壞某項不變性約束。但只要恢復時給定分區兩側的持久性操作歷史記錄,破壞不變性約束的操作還是可以被檢測出來并修正的。通常來講,讓分區兩側的事務都滿足 ACID 特性會使得后續的分區恢復變得更容易,并且為分區恢復時事務的補償工作奠定了基本的條件。
CAP 和延遲的聯系
CAP 理論的經典解釋,是忽略網絡延遲的,但在實際中延遲和分區緊密相關。CAP 從理論變為現實的場景發生在操作的間歇,系統需要在這段時間內做出關于分區的一個重要決定:
取消操作因而降低系統的可用性,還是
繼續操作,以冒險損失系統一致性為代價
依靠多次嘗試通信的方法來達到一致性,比如 Paxos 算法或者兩階段事務提交,僅僅是推遲了決策的時間。系統終究要做一個決定;無限期地嘗試下去,本身就是選擇一致性犧牲可用性的表現。
因此以實際效果而言,分區相當于對通信的時限要求。系統如果不能在時限內達成數據一致性,就意味著發生了分區的情況,必須就當前操作在 C 和 A 之間做出選擇。這就從延遲的角度抓住了設計的核心問題:分區兩側是否在無通信的情況下繼續其操作?
從這個實用的觀察角度出發可以導出若干重要的推論。第一,分區并不是全體節點的一致見解,因為有些節點檢測到了分區,有些可能沒有。第二,檢測到分區的節點即進入分區模式——這是優化 C 和 A 的核心環節。
最后,這個觀察角度還意味著設計師可以根據期望中的響應時間,有意識地設置時限;時限設得越短,系統進入分區模式越頻繁,其中有些時候并不一定真的發生了分區的情況,可能只是網絡變慢而已。
有時候在跨區域的系統,放棄強一致性來避免保持數據一致所帶來的高延遲是非常有意義的。Yahoo 的 PNUTS 系統因為以異步的方式維護遠程副本而帶來數據一致性的問題5。但好處是主副本就放在本地,減小操作的等待時間。這個策略在實際中很實用,因為一般來講,用戶數據大都會根據用戶的(日常)地理位置做分區。最理想的狀況是每一位用戶都在他的數據主副本附近。
Facebook 使用了相反的策略:主副本被固定在一個地方,因此遠程用戶一般訪問到的是離他較近,但可能已經過時的數據副本。不過當用戶更新其頁面的時候是直接對主副本進行更新,而且該用戶的所有讀操作也被短暫轉向從主副本讀取,盡管這樣延遲會比較高。20 秒后,該用戶的流量被重新切換回離他較近的副本,此時副本應該已經同步好了剛才的更新。
CAP 之惑
CAP 理論經常在不同方面被人誤解,對于可用性和一致性的作用范圍的誤解尤為嚴重,可能造成不希望看到的結果。如果用戶根本獲取不到服務,那么其實談不上 C 和 A 之間做取舍,除非把一部分服務放在客戶端上運行,即所謂的無連接操作或稱離線模式7。離線模式正變得越來越重要。HTML5 的一些特性,特別是客戶端持久化存儲特性,將會促進離線操作的發展。支持離線模式的系統通常會在 C 和 A 中選擇 A,那么就不得不在長時間處于分區狀態后進行恢復。
“一致性的作用范圍”其實反映了這樣一種觀念,即在一定的邊界內狀態是一致的,但超出了邊界就無從談起。比如在一個主分區內可以保證完備的一致性和可用性,而在分區外服務是不可用的。Paxos 算法和原子性多播(atomic multicast)系統一般符合這樣的場景8。像 Google 的一般做法是將主分區歸屬在單一個數據中心里面,然后交給 Paxos 算法去解決跨區域的問題,一方面保證全局協商一致(global consensus)如 Chubby9,一方面實現高可用的持久性存儲如 Megastore10。
分區期間,獨立且能自我保證一致性的節點子集合可以繼續執行操作,只是無法保證全局范圍的不變性約束不受破壞。數據分片(sharding)就是這樣的例子,設計師預先將數據劃分到不同的分區節點,分區期間單個數據分片多半可以繼續操作。相反,如果被分區的是內在關系密切的狀態,或者有某些全局性的不變性約束非保持不可,那么最好的情況是只有分區一側可以進行操作,最壞情況是操作完全不能進行。
“三選二”的時候取 CA 而舍 P 是否合理?已經有研究者指出了其中的要害——怎樣才算“舍 P”含義并不明確11,12。設計師可以選擇不要分區嗎?哪怕原來選了 CA,當分區出現的時候,你也只能回頭重新在 C 和 A 之間再選一次。我們最好從概率的角度去理解:選擇 CA 意味著我們假定,分區出現的可能性要比其他的系統性錯誤(如自然災難、并發故障)低很多。
這種觀點在實際中很有意義,因為某些故障組合可能導致同時丟掉 C 和 A,所以說 CAP 三個性質都是一個度的問題。實踐中,大部分團體認為(位于單一地點的)數據中心內部是沒有分區的,因此在單一數據中心之內可以選擇 CA;CAP 理論出現之前,系統都默認這樣的設計思路,包括傳統數據庫在內。然而就算可能性不高,單一數據中心完全有可能出現分區的情況,一旦出現就會動搖以 CA 為取向的設計基礎。最后,考慮到跨區域時出現的高延遲,在數據一致性上讓步來換取更好性能的做法相對比較常見。
CAP 還有一個方面很多人認識不清,那就是放棄一致性其實有隱藏負擔,即需要明確了解系統中存在的不變性約束。滿足一致性的系統有一種保持其不變性約束的自然傾向,即便設計師不清楚系統中所有的不變性約束,相當一部分合理的不變性約束會自動地維持下去。相反,當設計師選擇可用性的時候,因為需要在分區結束后恢復被破壞的不變性約束,顯然必須將各種不變性約束一一列舉出來,可想而知這件工作很有挑戰又很容易犯錯。放棄一致性為什么難,其核心還是“并發更新問題”,跟多線程編程比順序編程難的原因是一樣的。
管理分區
怎樣緩和分區對一致性和可用性的影響是對設計師的挑戰。其關鍵是以非常明確、公開的方式去管理分區,不僅需要主動察覺分區的發生,還需要為分區期間所有可能受侵害的不變性約束預備專門的恢復過程和計劃。管理分區有三個步驟:
- 檢測到分區開始
- 明確進入分區模式,限制某些操作,并且
- 當通信恢復后啟動分區恢復過程(恢復一致性,以及補償在系統分區期間程序產生的錯誤。)
上圖可見分區的演變過程。普通的操作都是順序的原子操作,因此分區總是在兩筆操作之間開始。一旦系統在操作間歇檢測到分區發生,檢測方一側即進入分區模式。如果確實發生了分區的情況,那么一般分區兩側都會進入到分區模式,不過單方面完成分區也是可能的。單方面分區要求在對方按需要通信的時候,本方要么能正確響應,要么不需要通信;總之操作不得破壞一致性。但不管怎么樣,由于檢測方可能有不一致的操作,它必須進入分區模式。采取了 quorum 決定機制的系統即為單方面分區的例子。其中一方擁有“法定通過節點數”,因此可以執行操作,而另一方不可以執行操作。支持離線操作的系統明顯地含有“分區模式”的概念,一些支持原子多播(atomic multicast)的系統也含有這個概念,如 Java 平臺的 JGroups。
當系統進入到分區模式,它有兩種可行的策略。其一是限制部分操作,因此會削弱可用性。其二是額外記錄一些有利于后面分區恢復的操作信息。系統可通過持續嘗試恢復通信來察覺分區何時結束。
哪些操作可以執行?
決定限制哪些操作,主要取決于系統需要維持哪幾項不變性約束。在給定了不變性約束條件之后,設計師需要決定在分區模式下,是否堅持不觸動某項不變性約束,抑或以事后恢復為前提去冒險觸犯它。例如,對于“表中鍵的惟一性”這項不變性約束,設計師一般都選擇在分區期間放寬要求,容許重復的鍵。重復的鍵很容易在恢復階段檢查出來,假如重復鍵可以合并,那么設計師不難恢復這項不變性約束。
對于分區期間必須維持的不變性約束,設計師應當禁止或改動可能觸犯該不變性約束的操作。(一般而言,我們沒辦法知道操作是否真的會破壞不變性約束,因為無法知道分區另一側的狀態。)信用卡扣費等具有外部化特征的事件常以這種方式工作。適合這種情況的策略,是記錄下操作意圖,然后在分區恢復后再執行操作。這類事務往往從屬于一些更大的工作流,在工作流明確含有類似“訂單處理中”狀態的情況下,將操作推遲到分區結束并無明顯的壞處。設計師以用戶不易察覺的方式犧牲了可用性。用戶只知道自己下了指令,系統稍后會執行。
說得更概括一點,分區模式給用戶界面提出了一種根本性的挑戰,即如何傳達“任務正在進行尚未完成”的信息。研究者已經從離線操作的角度對此問題進行了一些深入的探索,離線操作可以看成時間很長的一次分區。例如 Bayou 的日歷程序用顏色來區分顯示可能(暫時)不一致的條目13。工作流應用和帶離線模式的云服務中也常見類似的提醒,前者的例子如交易中的電子郵件通知,后者的例子如 Google Docs。
在分區模式的討論中,我們將關注點放在有明確意義的原子操作而非單純的讀寫,其中一個原因是操作的抽象級別越高,對不變性約束的影響通常就越容易分析清楚。大體來說,設計師要建立一張所有操作與所有不變性約束的叉乘表格(cross product),觀察并確定其中每一處操作可能與不變性約束相沖突的地方。對于這些沖突情況,設計師必須決定是否禁止、推遲或修改相應的操作。在實踐中,這類決定還受到分區前狀態和 / 或環境參數的影響。例如有的系統為特定的數據設立了主節點,那么一般允許主節點執行操作,不允許其他節點操作。
對分區兩側跟蹤操作歷史的最佳方式是使用版本向量,版本向量可以反映操作間的因果依賴關系。向量的元素是(節點, 邏輯時間)數值對,分別對應一個更新了對象的節點和它最后更新的時間。對于同一對象的兩個給定的版本 A 和 B,當所有結點的版本向量一致有 A 的時間大于或等于 B 的時間,且至少有一個節點的版本向量有 A 的時間較大,則 A 新于 B。
如果不可能對版本向量排序,那么更新操作是并發的,而且有可能出現不一致的情況。只要知道分區兩側版本向量的沿革。系統不難判斷哪些操作的執行順序是確定的,哪些操作是并發的。最近的研究成果證明14,當設計師選擇可用性優先,一般最多只能將一致性收緊到這樣的程度。
分區恢復
(ps:提供了兩種方法)
到了某個時刻,通信恢復,分區結束。由于每一側在分區期間都是可用的,其狀態仍繼續向前進展,但是分區會推遲某些操作并侵犯一些不變性約束。分區結束的時刻,系統知道分區兩側的當前狀態和歷史記錄,因為它在分區模式下記錄了詳盡的日志。當前狀態不如歷史記錄有價值,因為通過歷史記錄,系統可以判斷哪些操作違反了不變性約束,產生了何種外在的后果(如發送了響應給用戶)。在分區恢復過程中,設計師必須解決兩個問題:
- 分區兩側的狀態最終必須保持一致,
- 并且必須補償分區期間產生的錯誤。
(ps:方法1)通常情況,矯正當前狀態最簡單的解決方法是回退到分區開始時的狀態,以特定方式推進分區兩側的一系列操作,并在過程中一直保持一致的狀態。Bayou 就是這個實現機制,它會回滾數據庫到正確的時刻并按無歧義的、確定性的順序重新執行所有的操作,最終使所有的節點達到相同的狀態15。同樣地,并發版本控制系統 CVS 在合并分支的時候,也是從從一個共享的狀態一致點開始,逐步將更新合并上去。。
大部分系統都存在不能自動合并的沖突。比如,CVS 時不時有些沖突需要手動介入,帶離線模式的 wiki 系統總是把沖突留在產生的文檔里給用戶處理16。
相反,有些系統用了限制操作的辦法來保證沖突總能合并。一個例子就是 Google Docs 將其文本編輯操作17精簡為應用樣式、添加文本和刪除文本。因此,雖然總的來說沖突問題不可解,但現實中設計師可以選擇在分區期間限制使用部分操作,以便系統在恢復的時候能夠自動合并狀態。如果要實施這種策略,推遲有風險的操作是相對簡單的實現方式。
(ps:方法2)還有一種辦法是讓操作可以交換順序,這種辦法最接近于形成一種解決自動狀態合并問題的通用框架。此類系統將線性合并各日志并重排操作的順序,然后執行。操作滿足交換率,意味著操作有可能重新排列成一種全局一致的最佳順序。不幸的是,只允許滿足交換率的操作這個想法實現起來沒那么容易。比如加法操作可以交換順序,但是加入了越界檢查的加法就不行了。
Marc Shapiro 及其 INRIA 同事最近的工作18,19對于可交換順序的操作在狀態合并方面的應用起了很大的促進作用。該團隊提出一種從理論上證明可以保證分區后合并的數據類型,稱為可交換多副本數據類型(commutative replicated data types,CRDTs)。他們介紹了如何使用此類數據結構來
- 保證分區期間進行的所有操作都是可交換順序的,或者
- 用“格(lattice)”的數學概念來表示數據,并保證相對于“格”來說,分區期間的所有操作都是單調遞增的。
用后一種方法合并狀態會匯總分區兩邊的最大集合。這種方法是對亞馬遜購物車合并算法20的形式化總結和改良,合并后的數據是兩邊購物車的并集,而并運算是一種單調的集合運算。這種策略的壞處是刪掉的購物車商品有可能再次出現。
其實 CRDTs 完全可以實現同時支持增、刪操作的分區耐受集合。此方法的本質是維護兩個集合:一個放增加的項目,一個放刪除的項目,兩集合之差即為真正的集合成員。增集合、刪集合分別合并起來都不困難,因而增刪集合之差合并起來也不困難。在某個時間點上,系統可以從兩個集合中清理掉刪除的數據項。假如按照一般的設計,像這種清理操作僅在系統沒分區的時候才可行,屬于設計師必須在分區期間禁止或推遲的特定操作,但是 CRDTs 的清理操作并不會對可用性產生外在的影響。因此通過 CRDTs 來實現狀態,設計師既保證了可用性,又保證了分區后系統自動合并狀態。
補償錯誤
比計算分區后狀態更難解決的問題是如何彌補分區期間造成的錯誤。跟蹤和限制分區模式下的操作,這兩種措施足以使設計師確知哪些不變性約束可能被違反,然后分別為它們制定恢復策略。一般系統在分區恢復期間檢查違反情況,修復工作也必須在這段時間內完成。
恢復不變性約束的方法有很多,粗陋一點的辦法如“最后寫入者勝”(因此會忽略部分更新),聰明一點的辦法如合并操作和人為跟進事態(human escalation)。人為跟進事態的例子如飛機航班“超售”的情形:可以把乘客登機看作是對之前售票情況的分區恢復,必須恢復“座位數不少于乘客數”這項不變性約束。那么當乘客太多的時候,有些乘客將失去座位,客服最好能設法補償他們。
航班的例子揭示了一個外在錯誤(externalized mistake):假如航空公司沒說過乘客一定有座位,這個問題會好解決得多。因此我們看到推遲有風險的操作的又一個理由——到了分區恢復的時候,我們才知道真實的情況。矯正此類錯誤的核心概念是“補償(compensation)”;設計師必須設立補償操作,除了恢復不變性約束,還要糾正外在錯誤。
技術上 CRDTs 只允許局部可驗證的不變性約束,所以沒有補償的必要,雖然這種限制降低了 CRDTs 方法本身的能力。用了 CRDTs 來處理狀態合并的設計方案可以允許暫時違反全局性的不變量約束,分區結束后才合并狀態,以及履行必要的補償。
恢復外在錯誤通常要求知道一些有關外在輸出的歷史信息。以“喝醉酒打電話”為例,一位老兄不記得自己昨晚喝高了的時候打過幾個電話,雖然他第二天白天恢復了正常狀態,但通話日志上的記錄都還在,其中有些通話很可能是錯誤的。撥出的電話就是這位老兄的狀態(喝高了)的外在影響。而由于這位老兄不記得打過什么電話,也就很難補償其中可能造成的麻煩。
又以機器為例,電腦可能在分區期間把一份訂單執行了兩次。如果系統能區分兩份一樣的訂單是有意的還是重復了,它就能取消掉一份重復的訂單。如果這次錯誤產生了外在影響,補償策略可以是自動生成一封電子郵件,向顧客解釋系統意外將訂單執行了兩次,現在錯誤已經被糾正,附上一張優惠券下次可以用。假如沒有完善的歷史記錄,就只好靠顧客親自去發現錯誤了。
曾經有人正式研究過將補償性事務作為處理長壽命事務(long-lived transactions)的一種手段21,22。長時間運行的事務會面臨另一種形態的分區決策:是長時間持有鎖來保證一致性比較好呢?還是及早釋放鎖向其他事務暴露未提交的數據,提高并發能力比較好呢?比如在單筆事務中更新所有的員工記錄就是一個典型例子。按照一般的方式串行化這筆事務,將導致所有的記錄都被鎖定,阻止并發。而補償性事務采取另一種方式,它將大事務拆成多個分別提交的子事務。如果要中止大事務,系統必須發起一筆新的、起糾正作用的事務,逐一撤銷所有已經提交的子事務,這筆新事務就是所謂的補償性事務。
總的來說,補償性事務的目的是避免中止其他用了未正確提交數據的事務(即不允許級聯取消)。這種方案不依賴串行化或隔離的手段來保障正確性,其正確性取決于事務序列對狀態和輸出所產生的凈影響。那么,經過補償,數據庫的狀態究竟是不是相當于那些子事務根本沒執行過一樣呢?考慮等價必須連外在行為也包括在內;舉個例子,把重復扣取的交易款退還給顧客,很難說成等于一開始就沒多收顧客的錢,但從結果上看勉強算扯平了。分區恢復也延續同樣的思路。雖然服務不一定總能直接撤銷其錯誤,但起碼承認錯誤并做出新的補償行為。怎樣在分區恢復中運用這種思路效果最好,這個問題沒有固定的答案。“自動柜員機上的補償問題”小節以一個很小的應用領域為例點出了一些思考方向。
當系統中存在分區,系統設計師不應該盲目地犧牲一致性或可用性。運用以上討論的方法,設計師通過細致地管理分區期間的不變性約束,兩方面的性質都可以取得最佳的表現。隨著版本向量和 CRDTs 等比較新的技術逐漸被納入一些簡化其用法的框架,這方面的優化手段會得到比較普遍的應用。但引入 CAP 實踐畢竟不像引入 ACID 事務那么簡單,實施的時候需要對過去的策略進行全面的考慮,最佳的實施方案極大地依賴于具體服務的不變性約束和操作細節。
自動柜員機上的補償問題
以自動柜員機(ATM)的設計來說,強一致性看似符合邏輯的選擇,但現實情況是可用性遠比一致性重要。理由很簡單:高可用性意味著高收入。不管怎么樣,討論如何補償分區期間被破壞的不變性約束,ATM 的設計很適合作為例子。
ATM 的基本操作是存款、取款、查看余額。關鍵的不變性約束是余額應大于或等于零。因為只有取款操作會觸犯這項不變性約束,也就只有取款操作將受到特別對待,其他兩種操作隨時都可以執行。
ATM 系統設計師可以選擇在分區期間禁止取款操作,因為在那段時間里沒辦法知道真實的余額,當然這樣會損害可用性。現代 ATM 的做法正相反,在 stand-in 模式下(即分區模式),ATM 限制凈取款額不得高于 k,比如 k 為 $200。低于限額的時候,取款完全正常;當超過限額的時候,系統拒絕取款操作。這樣,ATM 成功將可用性限制在一個合理的水平上,既允許取款操作,又限制了風險。
分區結束的時候,必須有一些措施來恢復一致性和補償分區期間系統所造成的錯誤。狀態的恢復比較簡單,因為操作都是符合交換率的,補償就要分幾種情況去考慮。最后的余額低于零違反了不變性約束。由于 ATM 已經把錢吐出去了,錯誤成了外部實在。銀行的補償辦法是收取透支費并指望顧客償還。因為風險已經受到限制,問題并不嚴重。還有一種情況是分區期間的某一刻余額已經小于零(但 ATM 不知道),此時一筆存款重新將余額變為正的。銀行可以追溯產生透支費,也可以因為顧客已經繳付而忽略該違反情況。
總而言之,因為通信延遲的存在,銀行系統不依靠一致性來保證正確性,而更多地依靠審計和補償。“空頭支票詐騙”也是類似的例子,顧客趕在多家分行對賬之前分別取出錢來然后逃跑。透支的錯誤過后才會被發現,對錯誤的補償也許體現為法律行動的形式。