Java資深架構師分享學習經驗及總結
1. CAP 的由來
要了解 CAP,首先我們要清楚,為何會有人提出 CAP?他提出 CAP 是為了解決什麼問題?
時間回到 1985 年,彼時,後來證明了 CAP 理論的 Lynch 教授此時給當時的 IT 界來了一記驚雷:
她通過不可辯駁的證明告訴業界的工程師們,如果在一個不穩定(消息要麼亂序要麼丢了)的網絡環境裡(分布式異步模型),想始終保持資料一緻是不可能的。
這是個什麼概念呢?就是她打破了那些既想提供超高品質服務,又想提供超高性能服務的技術人員的幻想。
這本質是在告訴大家,在分布式系統裡,需要妥協。
但是,如何妥協?分布式系統裡到底應該怎麼權衡這種 trade-off?
我們可以想象一下,在 CAP 定理提出之前,沒有這些方向性的指引,在設計和實施分布式系統時該有多麼混亂。一套分布式系統是由多個子產品組成的,這些子產品本身可能由不同的開發人員去完成。然而,對于這些人,在公共層面,竟然沒有一個原則去指導他們該怎麼完成這套功能。
比如,我們在同步兩個節點的資料時,如果發生了錯誤,到底我們應該怎麼做呢?如果沒有統一的标準和方向,那很可能在一套分布式系統中的不同子產品,會出現不同的處理情況。
假設一套系統,由 A、B 兩個子產品構成。
A 子產品的設計理念是:節點間出現了問題,它可能會選擇不斷的重試,一直等到節點通信恢複。

而 B 的設計理念是:節點間出現了問題,它斷開就是了,可能最多就記錄下狀态,等以後處理。
可是,當 A、B 之間出現了通信怎麼辦?那會出現 A 往 B 發請求,出問題會不斷重試。而 B 往 A 發請求,出問題則直接斷開的情況。
當然,在後面我們會說明,CAP 的理念在實際工程中,會允許這種不一緻。可是,那種不一緻是提前設計好和規劃好的,是根據實際資料的重要性和業務需求做的妥協,而不是這種混亂的妥協。
是以,IT 界的人們就一直在摸索,試圖找到一些綱領去指導分布式系統的設計,這一找就找了 15 年。
2000 年時,Eric Brewer 教授在 PODC 會議上提出了 CAP 理論,但是由于沒有被證明過,是以,當時隻能被稱為 CAP 猜想。這個猜想引起了巨大的反響,因為 CAP 很符合人們對設計綱領的預期。
在 2002 年後,經過 Seth Gilbert 和 Nancy Lynch 從理論上證明了 CAP 猜想後,CAP 理論正式成為了分布式系統理論的基石之一。
2. CAP 到底是什麼
CAP 定理表達了一個分布式系統裡不可能同時滿足以下的三個特性:
2.1. C:資料一緻性
什麼是資料一緻性?咋一看真的很讓人糊塗,一緻性是什麼?是指資料能一起變化,是能讓資料整齊劃一。
那麼問題又來了,資料何時會變化?資料怎麼才能被稱為一起變化?我們現在來回答這些問題,當我們搞清楚了這些問題,那麼對資料一緻性就會有了清晰的了解。
首先第一個問題,資料何時會一起變化?
答案是:僅且僅當包含資料的服務,收到資料更新請求的時候,資料才會發生變化。而資料更新請求則僅包括資料的增、删、改這三種請求,而這三種請求又被統稱為寫請求。是以,資料隻有在寫請求的時候才會發生變化。
那我們來回答第二個問題,資料要怎麼樣才能被稱為一起變化了?即誰來判斷資料是最終變化了?是伺服器對寫請求的傳回結果嗎?告訴寫請求成功,資料就一定發生一緻性變化了?
NO,資料發生變化是否一緻是需要經過讀請求來做檢驗的。那麼讀請求判斷的依據是什麼呢?
假設,我們的分布式存儲系統有兩個節點,每個節點都包含了一部分需要被變化的資料。如果經過一次寫請求後,兩個節點都發生了資料變化。然後,讀請求把這些變化後的資料都讀取到了,我們就把這次資料修改稱為資料發生了一緻性變化。
但是,這還不是完整的一緻性。因為系統不可能永久的正常運作下去。
如果系統内部發生了問題進而導緻系統的節點無法發生一緻性變化會怎麼樣呢?當我們這樣做的時候,就意味着想看到最新資料的讀請求們,很可能會看到舊資料,或者說擷取到不同版本的資料。此時,為了保證分布式系統對外的資料一緻性,于是選擇不傳回任何資料。
這裡需要注意一下,CAP 定理是在說在某種狀态下的選擇,和實際工程的理論是有差别的。上面描述的一緻性和 ACID 事務中的一緻性是兩回事。事務中的一緻性包含了實際工程對狀态的後續處理。但是 CAP 定理并不涉及到狀态的後續處理,對于這些問題,後續出現了 BASE 理論等工程結論去處理,目前,隻需要明白 CAP 定理主要描述的是狀态。
2.2. A:可用性
奧維德曾經說過:“行動被人們遺忘,結果卻将永存”。
這句話說明了結果的重要性,而可用性在 CAP 裡就是對結果的要求。它要求系統内的節點們接收到了無論是寫請求還是讀請求,都要能處理并給回響應結果。隻是它有兩點必須滿足的條件:
條件 1:傳回結果必須在合理的時間以内,這個合理的時間是根據業務來定的。業務說必須 100 毫秒内傳回,合理的時間就是 100 毫秒,需要 1 秒内傳回,那就是 1 秒,如果業務定的 100 毫秒,結果卻在 1 秒才傳回,那麼這個系統就不滿足可用性。
條件 2:需要系統内能正常接收請求的所有節點都傳回結果。這包含了兩重含義:
- 如果節點不能正常接收請求了,比如當機了,系統崩潰了,而其他節點依然能正常接收請求,那麼,我們說系統依然是可用的,也就是說,部分當機沒事兒,不影響可用性名額。
- 如果節點能正常接收請求,但是發現節點内部資料有問題,那麼也必須傳回結果,哪怕傳回的結果是有問題的。比如,系統有兩個節點,其中有一個節點資料是三天前的,另一個節點是兩分鐘前的,如果,一個讀請求跑到了包含了三天前資料的那個節點上,抱歉,這個節點不能拒絕,必須傳回這個三天前的資料,即使它可能不太合理。
Java資深架構師分享學習經驗及總結
2.3. P:分區容忍性
分布式的存儲系統會有很多的節點,這些節點都是通過網絡進行通信。而網絡是不可靠的,當節點和節點之間的通信出現了問題,此時,就稱目前的分布式存儲系統出現了分區。但是,值得一提的是,分區并不一定是由網絡故障引起的,也可能是因為機器故障。
比如,我們的分布式存儲系統有 A、B 兩個節點。那麼,當 A、B 之間由于可能路由器、交換機等底層網絡裝置出現了故障,A 和 B 通信出現了問題,但是 A、B 依然都在運作,都在對外提供服務。這時候,就說 A 和 B 發生了分區。
還有一種情況也會發生分區,當 A 出現了當機,A 和 B 節點之間通信也是出現了問題,那麼我們也稱 A 和 B 發生了分區。
綜上,我們可以知道,隻要在分布式系統中,節點通信出現了問題,那麼就出現了分區。
那麼,分區容忍性是指什麼? 它是說,如果出現了分區問題,我們的分布式存儲系統還需要繼續運作。不能因為出現了分區問題,整個分布式節點全部就熄火了,罷工了,不做事情了。
3. CAP 怎麼選擇
我們上面已經知道了,在設計分布式系統時,架構師們在 C、A、P 這三種特性裡,隻能選擇兩種。
但是,這道 CAP 的選擇題,就像别人在問你“小明的父親有三個孩子,老大叫大朗,老二叫二郎,請問老三叫什麼”一樣。在以分布式存系統為限定條件的 CAP 世界裡,P 是早已經确定的答案,P 是必須的。
因為,在分布式系統内,P 是必然的發生的,不選 P,一旦發生分區錯誤,整個分布式系統就完全無法使用了,這是不符合實際需要的。是以,對于分布式系統,我們隻能能考慮當發生分區錯誤時,如何選擇一緻性和可用性。
而根據一緻性和可用性的選擇不同,開源的分布式系統往往又被分為 CP 系統和 AP 系統。
當一套系統在發生分區故障後,用戶端的任何請求都被卡死或者逾時,但是,系統的每個節點總是會傳回一緻的資料,則這套系統就是 CP 系統,經典的比如 Zookeeper。
如果一套系統發生分區故障後,用戶端依然可以通路系統,但是擷取的資料有的是新的資料,有的還是老資料,那麼這套系統就是 AP 系統,經典的比如 Eureka。
說了這麼多,其實 CAP 定理本質很簡單,它就是一種分布式系統設計的不同理念概括,包括它說的一緻性,可用性和分區容錯性。這就類似一個大學的校訓,是極度概念化的東西。
是以,大白話來形容下 CAP 吧,CAP 就是告訴程式員們當分布式系統出現内部問題了,你要做兩種選擇:
- 要麼遷就外部服務,像外包公司。
- 要麼讓外部服務遷就你,像銀行。
遷就外部服務就是我們不能因為我們自己的問題讓外部服務的業務運作受到影響,是以要優先可用性。而讓外部服務遷就我們,就要優先一緻性。
4. 對 CAP 的常見誤解
誤解一:分布式系統因為 CAP 定理放棄了 C 或者 A 中的其中一個
很多人在沒有對 CAP 做深入了解的情況下,聽到很多人說分布式系統必須在 CAP 三個特性裡選擇兩個,就覺得一套分布式系統肯定要麼隻有可用性要麼隻有一緻性,不存在完整的可用性和一緻性功能。
這種了解是大有問題的。因為,P 這種問題發生的機率非常低,是以:
當沒有出現分區問題的時候,系統就應該有完美的資料一緻性和可用性。
你什麼時候見過一個系統,當内部沒有問題的時候,會經常讓外部請求卡一下的?要麼就冷不丁的提供陳舊的老資料?那還能叫系統嗎?
誤解二:C 和 A 之間的選擇是針對整個分布式系統的,隻能整體考慮 C 和 A 之間的選擇
這個了解也是不對的。當分區發生的時候,其實對一緻性和可用性的抉擇是局部性的,而不是針對整個系統的。
可能是在一些子系統做一些抉擇,甚至很可能隻需要對某個事件或者資料,做一緻性和可用性的抉擇而已。
比如,當我們做一套支付系統的時候,會員的财務相關像賬戶餘額,賬務流水是必須強一緻性的。這時候,你就要考慮選 C。但是,會員的名字,會員的支付設定就不必考慮強一緻性,可以選擇可用性 A。
一套分布式系統的運作,就像人生一樣,就是一次又一次的選擇。在不同階段,不同的時刻有不同的事件發生的時候,又怎麼可能會有完全一樣的選擇呢?
誤解三:CAP 的三個特性隻有是和否兩種極端選擇,而不是一個範圍
這種二進制性的了解更是極其誤導人。
CAP 理論的三種特性不是 Boolean 類型的,不是一緻和不一緻,可用和不可用,分區和沒分區的這類二選一的選項。而是這三種特性都是範圍類型。
拿可用性來說,就像我從銀行取錢。當我目的是派發壓歲錢的時候,我很可能就想全要新票子,但是,新票子很可能就還得多一個步驟,就是需要拿舊票子去換一些新票,此時,我可以多等會兒,能拿到新票子就好。而當我的目的就是做生活花銷的時候,票子是新是舊,我根本不那麼關心,快點拿到錢就行。這就是可用性的範圍需求之一,對時延性的要求。
再比如,分區容錯則由于探測機制的問題,可能還得各節點搞投票去協商分區是否存在,當某一台機器出現了問題,可能不影響業務的話,就會被機器投票認為分區不存在。然後一直等到多數機器出現了問題,才會投票确認出現了分區問題。這就好像新冠疫情,還會分低、中、高風險區呢,不是一出現通信故障就都被邏輯認定為分區問題。
5. CAP 理論的一些疑問
疑問一:在遵從 CAP 定理的系統中是否适合任意的寫請求
首先,在 CAP 定理中,關于一緻性會有多種說法,但是總的來說,都是在描述資料最新版本的可見性。而這些可見性往往代表的是讀請求傳回的資料的可見性。
那麼問題來了,當我們要求讀資料的可見性的時候,對寫資料有什麼要求嗎?
比如,我們系統有三個節點,一個用戶端給這個系統發了一個寫請求,要求系統寫入一個值為 20 的資料。那麼,如果要滿足 CAP 定理中的一緻性,就需要在寫完 20 這個資料之後,當其他用戶端請求讀取這個值為 20 的資料之後,無論請求被轉發到系統中任何節點都能傳回這個值。
這就要求寫入這個值為 20 的寫請求必須成功寫到三個節點上,此時,系統就滿足了寫一緻性的。是以,我們可以說對于讀一緻性的要求是同時限制了寫一緻性的。
其次,在 CAP 定理中,可用性本身要求對讀、寫請求都要處理。如果我們以可用性作為标準的時候,在發生分區錯誤時,由于我們對讀請求并沒有強行要求傳回完全準确的資料,是以,可能在本次讀請求之前的最近一次寫請求可能是部分失敗的。
同樣的例子,我們的分布式系統由三個節點組成,最近一次寫請求想把值為 20 的資料寫到三個節點上。但是,由于發生了分區問題,有一個節點通信故障,寫請求寫不過去,是以隻有兩個節點包含了值為 20 的資料。
此時,寫請求會傳回給用戶端一個結果,可能會告訴用戶端寫入成功了,也可能告訴用戶端寫入部分成功。
這時候,當後續的讀請求恰巧被發送到有通信故障的那個節點,系統可能隻能傳回一個空的結果。但是,由于系統處理和傳回了讀寫請求,是以,系統是滿足了 CAP 中的可用性的。
疑問二:資料分片和資料副本的分布式系統是否都遵守 CAP 定理
我們知道,在一套大規模的分布式系統裡,一定是既需要把海量資料做切分,存儲到不同的機器上,也需要對這些存儲了資料的機器做副本備份的。
那麼,如果,一個分布式系統裡隻有資料分片存儲或者隻有資料副本存儲,他們都會遵守 CAP 定理嗎?
答案是當資料分片時,也是要遵守 CAP 定理,但是,是種非常特殊的遵守。
當在一套分布式系統隻有分片存儲的時候,CAP 理論會表現成什麼樣?
比如,我們有個分布式系統,由三個節點 a、b、c 組成。其中節點 a 存放了 A 表的資料,b 存放了 B 表的資料,c 存放了 C 表的資料。
如果有一個業務,它的意圖是想往 A 表插入一條新資料,在 B 表删除一條已有資料,在 C 表更新一條老資料,這個分布式系統該怎麼處理這種業務?
技術上我們對這種一個意圖想做多件事的情況往往會包裝成一個事務。當我們包裝成一個事務以後,我們可能會通過先在 a 節點執行,然後去 b 節點執行,最後去 c 節點執行,等到都成功了,才會傳回成功。
但是,發生了分區以後怎麼辦?當在 a、b 節點都成功了,到 c 發現發生了通信故障?
此時,根據 CAP 定理,你有兩個選擇,要麼就直接傳回一個部分成功的結果給用戶端,要麼直接卡死等用戶端逾時或者傳回失敗給用戶端。當傳回部分成功的時候,這就是選擇了可用性(A),當卡死或者傳回失敗給用戶端的時候,就是選擇了一緻性(C)。
可是,我們将請求包裝成了事務,而事務是要求要麼都成功,要麼都失敗……為了遵守這種要求,對于分布式隻有分片的情況,迫于客觀條件,隻能選擇C。是以分片的分布式系統,往往都是 CP 的系統。
可選擇,但是無法選擇是分布式系統隻有分片資料存儲的情況時,遵守 CAP 定理的特殊表現。
而當分布式系統是多個節點,每個節點存儲了完整的一套資料,别的節點隻是完整資料的備份的時候,即使事務隻在一台機器上成功,當發生分區故障的時候,我們也是可以有充分的餘地選擇是單機事務的回退 or 就此認為寫成功的。
單機事務的回退,就可以對外表現為選擇了一緻性。
就此認為寫成功,則可以認為選擇了可用性。
疑問三:為何有時候區分一個系統是 AP 還是 CP 是如此之難
因為,就像我們前面講過的,由于 AP 或者 CP 的選擇,可能僅局限為整套系統的局部,甚至某些特殊的資料上,而我們又是用這種局部的特性去描述了整套系統,是以就導緻了區分的困難。而這本身其實也日漸成為了 CAP 的一個大問題,進而被人诟病。
6. CAP 的不足
- CAP 定理本身是沒有考慮網絡延遲的問題的,它認為一緻性是立即生效的,但是,要保持一緻性,是需要時間成本的,這就導緻往往分布式系統多選擇 AP 方式
- 由于時代的演變,CAP 定理在針對所有分布式系統的時候,出現了一些力不從心的情況,導緻很多時候它自己會把以前很嚴謹的數學定義改成了比較松弛的業務定義,類似于我們看到,CAP 定理把一緻性、可用性、分區容錯都變成了一個範圍屬性,而這和 CAP 定理本身這種數學定理般的稱呼是有沖突的,出現了不符合數學嚴謹定義的問題。
- 在實踐中以及後來 CAP 定理的提出者也承認,一緻性和可用性并不僅僅是二選一的問題,隻是一些重要性的差別,當強調一緻性的時候,并不表示可用性是完全不可用的狀态。比如,Zookeeper 隻是在 master 出現問題的時候,才可能出現幾十秒的不可用狀态,而别的時候,都會以各種方式保證系統的可用性。而強調可用性的時候,也往往會采用一些技術手段,去保證資料最終是一緻的。CAP 定理并沒有給出這些情況的具體描述。
- CAP 理論從工程角度來看隻是一種狀态的描述,它告訴大家當有錯的時候,分布式系統可能處在什麼狀态。但是,狀态是可能變化的。狀态間如何轉換,如何修補,如何恢複是沒有提供方向的。
7. 引申出來的 BASE
正因為 CAP 以上的種種不足,epay 的架構師 Dan Pritchett 根據他自身在大規模分布式系統的實踐經驗,總結出了 BASE 理論。BASE 理論是對 CAP 理論的延伸,核心思想是即使無法做到強一緻性(Strong Consistency),但應用可以采用适合的方式達到最終一緻性(Eventual Consitency)。
BASE 理論是實踐工程的理論,它彌補了CAP 理論過于抽象的問題,也同時解決了 AP 系統的總體工程實踐思想,是分布式系統的核心理論之一,我們将在下一篇文章裡,詳細的講解此套理論。
8. 大廠面試題
在文章最後,來幾道大廠關于 CAP 的面試真題,檢驗一下你的學習效果,hiahiahia
- 什麼是 CAP 理論?
- CAP 中的 P 是什麼意思?
- 為什麼說分布式系統,隻能在 C、A 中二選一?
- 結合實際應用,CP、AP 該怎麼選擇?
最後
給讀者們一個小福利,有需要這些資料的朋友們可以點選我,即可免費領取資料!
ActiveMQ消息中間件面試專題
- 什麼是ActiveMQ?
- ActiveMQ伺服器當機怎麼辦?
- 丢消息怎麼辦?
- 持久化消息非常慢怎麼辦?
- 消息的不均勻消費怎麼辦?
- 死信隊列怎麼辦?
- ActiveMQ中的消息重發時間間隔和重發次數嗎?
ActiveMQ消息中間件面試專題解析拓展:
redis面試專題及答案
- 支援一緻性哈希的用戶端有哪些?
- Redis與其他key-value存儲有什麼不同?
- Redis的記憶體占用情況怎麼樣?
- 都有哪些辦法可以降低Redis的記憶體使用情況呢?
- 檢視Redis使用情況及狀态資訊用什麼指令?
- Redis的記憶體用完了會發生什麼?
- Redis是單線程的,如何提高多核CPU的使用率?
Spring面試專題及答案
- 談談你對 Spring 的了解
- Spring 有哪些優點?
- Spring 中的設計模式
- 怎樣開啟注解裝配以及常用注解
- 簡單介紹下 Spring bean 的生命周期
Spring面試答案解析拓展
高并發多線程面試專題
- 現在有線程 T1、T2 和 T3。你如何確定 T2 線程在 T1 之後執行,并且 T3 線程在 T2 之後執行?
- Java 中新的 Lock 接口相對于同步代碼塊(synchronized block)有什麼優勢?如果讓你實作一個高性能緩存,支援并發讀取和單一寫入,你如何保證資料完整性。
- Java 中 wait 和 sleep 方法有什麼差別?
- 如何在 Java 中實作一個阻塞隊列?
- 如何在 Java 中編寫代碼解決生産者消費者問題?
- 寫一段死鎖代碼。你在 Java 中如何解決死鎖?
高并發多線程面試解析與拓展
jvm面試專題與解析
- JVM 由哪些部分組成?
- JVM 記憶體劃分?
- Java 的記憶體模型?
- 引用的分類?
- GC什麼時候開始?
JVM面試專題解析與拓展!