常見一致性演算法

程式碼的壞味道發表於2022-01-06

後臺服務架構經過了集中式、SOA、微服務和服務網格四個階段,目前網際網路界大都使用微服務和服務網格。服務從集中式、中心化向分散式、去中心化不斷演進,服務也變得更靈活,能夠自動擴縮容、快速版本迭代等。但是分散式架構也將集中式下一些問題放大,比如通訊故障、請求三態(成功、失敗、超時)、節點故障等,這些問題會導致一系例資料不一致的問題,也是計算機領域的老大難問題

分散式理論指導

一、ACID,CAP理論和BASE理論

ACID 是資料庫事務完整性的理論,CAP 是分散式系統設計理論,BASE 是 CAP 理論中 AP 方案的延伸

ACID

ACID是傳統資料庫常用的設計理念,追求強一致性模型。
關聯式資料庫的ACID模型擁有 高一致性 + 可用性 很難進行分割槽:

  • Atomicity原子性:一個事務中所有操作都必須全部完成,要麼全部不完成。
  • Consistency一致性. 在事務開始或結束時,資料庫應該在一致狀態。
  • Isolation隔離層. 事務將假定只有它自己在運算元據庫,彼此不知曉。(事務隔離級別)
  • Durability. 一旦事務完成,就不能返回。

ACID,是指在資料庫管理系統(DBMS)中事務所具有的四個特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、永續性(Durability)。
問題一:Mysql怎麼保證一致性的?
OK,這個問題分為兩個層面來說。
從資料庫層面,資料庫通過原子性、隔離性、永續性來保證一致性。也就是說ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔離性)、D(永續性)是手段,是為了保證一致性,資料庫提供的手段。資料庫必須要實現AID三大特性,才有可能實現一致性。例如,原子性無法保證,顯然一致性也無法保證。

但是,如果你在事務裡故意寫出違反約束的程式碼,一致性還是無法保證的。例如,你在轉賬的例子中,你的程式碼裡故意不給B賬戶加錢,那一致性還是無法保證。因此,還必須從應用層角度考慮。

從應用層面,通過程式碼判斷資料庫資料是否有效,然後決定回滾還是提交資料!

問題二: Mysql怎麼保證原子性的?
OK,是利用Innodb的undo log。
undo log名為回滾日誌,是實現原子性的關鍵,當事務回滾時能夠撤銷所有已經成功執行的sql語句,他需要記錄你要回滾的相應日誌資訊。
例如

(1)當你delete一條資料的時候,就需要記錄這條資料的資訊,回滾的時候,insert這條舊資料
(2)當你update一條資料的時候,就需要記錄之前的舊值,回滾的時候,根據舊值執行update操作
(3)當年insert一條資料的時候,就需要這條記錄的主鍵,回滾的時候,根據主鍵執行delete操作
undo log記錄了這些回滾需要的資訊,當事務執行失敗或呼叫了rollback,導致事務需要回滾,便可以利用
問題三: Mysql怎麼保證永續性的?
OK,是利用Innodb的redo log。
正如之前說的,Mysql是先把磁碟上的資料載入到記憶體中,在記憶體中對資料進行修改,再刷回磁碟上。如果此時突然當機,記憶體中的資料就會丟失。
怎麼解決這個問題?
簡單啊,事務提交前直接把資料寫入磁碟就行啊。
這麼做有什麼問題?

只修改一個頁面裡的一個位元組,就要將整個頁面刷入磁碟,太浪費資源了。畢竟一個頁面16kb大小,你只改其中一點點東西,就要將16kb的內容刷入磁碟,聽著也不合理。
畢竟一個事務裡的SQL可能牽涉到多個資料頁的修改,而這些資料頁可能不是相鄰的,也就是屬於隨機IO。顯然操作隨機IO,速度會比較慢。
於是,決定採用redo log解決上面的問題。當做資料修改的時候,不僅在記憶體中操作,還會在redo log中記錄這次操作。當事務提交的時候,會將redo log日誌進行刷盤(redo log一部分在記憶體中,一部分在磁碟上)。當資料庫當機重啟的時候,會將redo log中的內容恢復到資料庫中,再根據undo log和binlog內容決定回滾資料還是提交資料。

採用redo log的好處?
其實好處就是將redo log進行刷盤比對資料頁刷盤效率高,具體表現如下

redo log體積小,畢竟只記錄了哪一頁修改了啥,因此體積小,刷盤快。
redo log是一直往末尾進行追加,屬於順序IO。效率顯然比隨機IO來的快。
ps:不想具體去談redo log具體長什麼樣,因為內容太多了。

問題四: Mysql怎麼保證隔離性的?
OK,利用的是鎖和MVCC機制。還是拿轉賬例子來說明,有一個賬戶表如下
表名t_balance

id user_id balance
1 A 200
2 B 0
其中id是主鍵,user_id為賬戶名,balance為餘額。還是以轉賬兩次為例

至於MVCC,即多版本併發控制(Multi Version Concurrency Control),一個行記錄資料有多個版本對快照資料,這些快照資料在undo log中。
如果一個事務讀取的行正在做DELELE或者UPDATE操作,讀取操作不會等行上的鎖釋放,而是讀取該行的快照版本。
由於MVCC機制在可重複讀(Repeateable Read)和讀已提交(Read Commited)的MVCC表現形式不同,就不贅述了。
但是有一點說明一下,在事務隔離級別為讀已提交(Read Commited)時,一個事務能夠讀到另一個事務已經提交的資料,是不滿足隔離性的。但是當事務隔離級別為可重複讀(Repeateable Read)中,是滿足隔離性的。

cap

CAP定理又被成為布魯爾定理,是加州大學電腦科學家埃裡克·布魯爾提出來的猜想
2000年7月,加州大學伯克利分校的Eric Brewer教授在ACM PODC會議上提出CAP猜想。2年後,麻省理工學院的Seth Gilbert和Nancy Lynch從理論上證明了CAP。之後,CAP理論正式成為分散式計算領域的公認定理

  • C(Consistency 一致性):所有的節點上的資料時刻保持同步
  • A(Availability 可用性):每個請求都能接受到一個響應,無論響應成功或失敗
  • P(Partition tolerance分割槽容錯):系統應該能持續提供服務,即使系統內部有訊息丟失(分割槽)

存在問題

常見一致性演算法

CAP理論證明在分散式系統中不能同時滿足這三個特徵,只能滿足其中兩點

高可用、資料一致是很多系統設計的目標,但是分割槽又是不可避免的事情:
CA without P:如果不要求P(不允許分割槽),則C(強一致性)和A(可用性)是可以保證的。但其實分割槽不是你想不想的問題,而是始終會存在,因此CA的系統更多的是允許分割槽後各子系統依然保持CA。
CP without A:如果不要求A(可用),相當於每個請求都需要在Server之間強一致,而P(分割槽)會導致同步時間無限延長,如此CP也是可以保證的。很多傳統的資料庫分散式事務都屬於這種模式。
AP wihtout C:要高可用並允許分割槽,則需放棄一致性。一旦分割槽發生,節點之間可能會失去聯絡,為了高可用,每個節點只能用本地資料提供服務,而這樣會導致全域性資料的不一致性。現在眾多的NoSQL都屬於此類。
困惑一

CP:為了實現一致性和分割槽容忍性必須放棄可用性。

實際應用中,比如系統存在A、B兩個節點(或網路分割槽),資料寫入A後,網路發生抖動導致A、B通訊中斷,資料無法寫入B,為了保證資料一致性,系統將無法繼續提供服務直到B完成寫入,AB達到資料一致的狀態。如果A、B通訊永遠不恢復,系統將永遠不可用,這在實際應用中不可接受。

困惑二

CA:為了實現一致性和可用性放棄分割槽容忍性。

實際應用中,比如系統存在A、B兩個節點(或網路分割槽),資料寫入A後,網路發生抖動導致A、B通訊中斷,資料無法寫入B,為了保證資料一致性,系統將無法繼續提供服務直到B完成寫入,AB達到資料一致的狀態。如果A、B通訊永遠不恢復,系統將永遠不可用,這在實際應用中不可接受。

是的,實際應用中CP、CA本質上沒有區別,同樣是面對網路抖動問題,為了滿足一致性,都會導致系統無法使用。這是因為實際應用中,節點A沒有辦法判斷節點B是掛掉了、還是無法與其正常通訊。於是CA和CP在不引入第三方元件的情況下都是無法達到的。而這完全是由C(一致性)導致,所以如果可以在業務上避免對分散式系統的一致性要求那是極好的!
實際應用分析

只求AP

放棄一致性,保證系統高可用、高擴充套件性、分割槽容忍性。系統設計將極大簡化,同時保證高可用、高擴充套件,系統的運維也很方便,最終的結果是從設計、開發、測試、上線、運維都極大的降低了成本。就如Amazon SQS,的確是不錯。

尋求A、C折中

網路抖動時,保留一致性,放棄可用性。在一定時間視窗內(比如5s),如果服務依然沒有恢復,就放棄一致性,恢復對外提供服務。這種策略在實際應用比較普遍,根據具體的業務場景,設定一個時間視窗,保證系統在可容忍的時間內儘可能保持一致性,如果實在保證不了,那就放棄一致性,繼續提供服務。畢竟更多時候,不能提供服務造成的損失要比部分資料不一致造成的損失大很多。更多的時候,我們自然而然的把failover邏輯放在系統的終端實現,就如ActiveMQ的failover、RocketMQ的rebalance

不適用資料庫事務架構,

BASE

BASE理論解決CAP理論提出了分散式系統的一致性和可用性不能兼得的問題。
BASE理論是對CAP理論的延伸,思想是即使無法做到強一致性(CAP的一致性就是強一致性),但可以採用適當的採取弱一致性,即最終一致性。
BASE是指基本可用(Basically Available)、軟狀態( Soft State)、最終一致性( Eventual Consistency)。

  • 基本可用(Basically Available)
    基本可用是指分散式系統在出現故障的時候,允許損失部分可用性,即保證核心可用。
    電商大促時,為了應對訪問量激增,部分使用者可能會被引導到降級頁面,服務層也可能只提供降級服務。這就是損失部分可用性的體現。(削峰填谷,延遲響應,服務降級)

  • 軟狀態( Soft State)
    軟狀態是指允許系統存在中間狀態,而該中間狀態不會影響系統整體可用性。分散式儲存中一般一份資料至少會有三個副本,允許不同節點間副本同步的延時就是軟狀態的體現。mysql replication的非同步複製也是一種體現。

  • 最終一致性( Eventual Consistency)
    最終一致性是指系統中的所有資料副本經過一定時間後,最終能夠達到一致的狀態。弱一致性和強一致性相反,最終一致性是弱一致性的一種特殊情況。(寫時修復,讀時修復)

是對 CAP 中 AP 方案的一個補充
BASE模型是傳統ACID模型的反面,不同與ACID,BASE強調犧牲高一致性,從而獲得可用性,資料允許在一段時間內的不一致,只要保證最終一致就可以了

分散式一致性的 3 種級別:

  1. 強一致性 :系統寫入了什麼,讀出來的就是什麼。
  2. 弱一致性 :不一定可以讀取到最新寫入的值,也不保證多少時間之後讀取到的資料是最新的,只是會儘量保證某個時刻達到資料一致的狀態。
  3. 最終一致性 :弱一致性的升級版。,系統會保證在一定時間內達到資料一致的狀態
    業界比較推崇是最終一致性級別,但是某些對資料一致要求十分嚴格的場景比如銀行轉賬還是要保證強一致性。**

常見一致性演算法

服務註冊中心,是選擇AP還是選擇CP ?

服務註冊中心解決的問題
在討論CAP之前先明確下服務註冊中心主要是解決什麼問題:一個是服務註冊,一個是服務發現。
服務註冊:例項將自身服務資訊註冊到註冊中心,這部分資訊包括服務的主機IP和服務的Port,以及暴露服務自身狀態和訪問協議資訊等。
服務發現:例項請求註冊中心所依賴的服務資訊,服務例項通過註冊中心,獲取到註冊到其中的服務例項的資訊,通過這些資訊去請求它們提供的服務。

常見一致性演算法

zookeeper選擇CP
zookeep保證CP,即任何時刻對zookeeper的訪問請求能得到一致性的資料結果,同時系統對網路分割具備容錯性,但是它不能保證每次服務的可用性。從實際情況來分析,在使用zookeeper獲取服務列表時,如果zk正在選舉或者zk叢集中半數以上的機器不可用,那麼將無法獲取資料。所以說,zk不能保證服務可用性。
eureka選擇AP
eureka保證AP,eureka在設計時優先保證可用性,每一個節點都是平等的,一部分節點掛掉不會影響到正常節點的工作,不會出現類似zk的選舉leader的過程,客戶端發現向某個節點註冊或連線失敗,會自動切換到其他的節點,只要有一臺eureka存在,就可以保證整個服務處在可用狀態,只不過有可能這個服務上的資訊並不是最新的資訊。
zookeeper和eureka的資料一致性問題
先要明確一點,eureka的建立初心就是為一個註冊中心,但是zk更多是作為分散式協調服務的存在,只不過因為它的特性被dubbo賦予了註冊中心,它的職責更多是保證資料(配置資料,狀態資料)在管轄下的所有服務之間保持一致,所有這個就不難理解為何zk被設計成CP而不是AP,zk最核心的演算法ZAB,就是為了解決分散式系統下資料在多個服務之間一致同步的問題。
更深層的原因,zookeeper是按照CP原則構建,也就是說它必須保持每一個節點的資料都保持一致,如果zookeeper下節點斷開或者叢集中出現網路分割(例如交換機的子網間不能互訪),那麼zk會將它們從自己的管理範圍中剔除,外界不能訪問這些節點,即使這些節點是健康的可以提供正常的服務,所以導致這些節點請求都會丟失。
而eureka則完全沒有這方面的顧慮,它的節點都是相對獨立,不需要考慮資料一致性的問題,這個應該是eureka的誕生就是為了註冊中心而設計,相對zk來說剔除了leader節點選取和事務日誌極致,這樣更有利於維護和保證eureka在執行的健壯性。
————————————————

常見一致性演算法

小結:服務註冊應該選擇AP還是CP
對於服務註冊來說,針對同一個服務,即使註冊中心的不同節點儲存的服務註冊資訊不相同,也並不會造成災難性的後果,對於服務消費者來說,能消費才是最重要的,就算拿到的資料不是最新的資料,消費者本身也可以進行嘗試失敗重試。總比為了追求資料的一致性而獲取不到例項資訊整個服務不可用要好。
所以,對於服務註冊來說,可用性比資料一致性更加的重要,選擇AP。

分散式鎖,是選擇AP還是選擇CP ?
這裡實現分散式鎖的方式選取了三種:

  • 基於資料庫實現分散式鎖

常見一致性演算法

利用表的 UNIQUE KEY idx_lock (method_lock) 作為唯一主鍵,當進行上鎖時進行insert動作,資料庫成功錄入則以為上鎖成功,當資料庫報出 Duplicate entry 則表示無法獲取該鎖。

不過這種方式對於單主卻無法自動切換主從的mysql來說,基本就無法現實P分割槽容錯性,(Mysql自動主從切換在目前並沒有十分完美的解決方案)。可以說這種方式強依賴於資料庫的可用性,資料庫寫操作是一個單點,一旦資料庫掛掉,就導致鎖的不可用。

  • 基於redis實現分散式鎖
    redis單執行緒序列處理天然就是解決序列化問題,用來解決分散式鎖是再適合不過。
    為了解決資料庫鎖的無主從切換的問題,可以選擇redis叢集,或者是 sentinel 哨兵模式,實現主從故障轉移,當master節點出現故障,哨兵會從slave中選取節點,重新變成新的master節點。
    哨兵模式故障轉移是由sentinel叢集進行監控判斷,當maser出現異常即複製中止,重新推選新slave成為master,sentinel在重新進行選舉並不在意主從資料是否複製完畢具備一致性。
    所以redis的複製模式是屬於AP的模式

  • 基於zookeeper實現分散式鎖

分散式事務,是怎麼從ACID解脫,投身CAP/BASE
如果說到事務,ACID是傳統資料庫常用的設計理念,追求強一致性模型,關聯式資料庫的ACID模型擁有高一致性+可用性,所以很難進行分割槽,所以在微服務中ACID已經是無法支援,我們還是回到CAP去尋求解決方案,不過根據上面的討論,CAP定理中,要麼只能CP,要麼只能AP,如果我們追求資料的一致性而忽略可用性這個在微服務中肯定是行不通的,如果我們追求可用性而忽略一致性,那麼在一些重要的資料(例如支付,金額)肯定出現漏洞百出,這個也是無法接受。所以我們既要一致性,也要可用性。

常見一致性演算法

本作品採用《CC 協議》,轉載必須註明作者和本文連結
cfun

相關文章