億級流量架構之分佈式事務思路及方法

分佈式事務以及分佈式鎖是分佈式中難點, 分佈式事務一篇文章可能寫不完, 我的習慣時從基本概念出發, 一步一步開始介紹, 前面會先梳理事務中一些基本概念, 對基本概念十分清楚的話可以直接看 "一致性討論" 以及後面的部分。予己方便總結回顧、與他交流分享。

什麼是分佈式事務

在日常生活中, 很多事要麼全部做, 要麼全部不做, 不能只做一部分, 不然就會產生其他複雜的問題, 很多人喜歡舉轉賬的例子, 對於同一個賬號, A 在湖北往出轉 500,B 在廣東取錢 500, 那麼 A 轉出去之後要將 A 賬號的錢數目扣除, B 賬號數目增加: 事務 = (A 賬號扣除 500,B 賬號增加 500)

看到沒, 像這樣多個步驟放在一起, 就是事務, 要麼都執行, 要麼都不執行, 如果我們的數據存儲在多個數據庫中, 也就是存在跨庫調用, 由於網絡具有不安全性以及延時性, 如何保證事務分佈式執行呢? 如果執行到一半斷電又該如何處理? 在講解分佈式事務之前先簡單回顧事務的一些特點, 俗稱 ACID , 下面逐一講解:

原子性 (Atomic)

在化學中, 分子構成的物質, 分子是保持化學特性的最小單位, 如 H2O,CO2H2O,CO2 等, 由原子構成的物質, 原子保持物質特性, 像 FeFe 啥的, 意思就是不可分割, 再分成質子中子啥的就不是我們認爲的物質了, 這兒的原子性也是這個道理, 就是事務不可以再拆分, 例如上面的事務, 看着可以是由兩個過程組成的事務, 但是你拆開就不是我們認爲該有的過程, 所以, 事務不可再分, 具有原子性。

一致性(Consistency)

一致性也很好理解, 對於上面的兩個賬戶, 如果銀行想知道自己這兒被存了多少錢, 那麼這個事務執行前, A 賬號有 500 塊, B 賬號沒有錢, 銀行賬戶總共 500 塊, 事務執行後 A 賬號沒有錢, B 賬號有 500 塊, 也就是這個 500 塊是一定的, 不可能出現 A 賬號有 500 塊, B 賬號也有 500 塊, 那就數據不一致了, 這樣的話, 說明事務中某些步驟執行出現了問題, 產生中間數據, 那麼就不一致。

在分佈式中, 對於一個結果, 多處同時查詢, 得出的結果應該是一致的。

隔離性(Isolation)

一個事務在未完成時,另一個事務不會影響到它, 也就是如果 B 還給 C 轉賬 1000, 記爲事務 2:

事務 1 = (A 賬號扣除 500,B 賬號增加 500)

事務 2 = (B 賬號扣除 1000,C 賬號增加 1000)

這兩個事務之間不會產生影響, 也就是不會發生 A 轉出的 500 塊到達 C 賬號這種情況。

持久性(Durability)

持久化, 一般是意味着將數據寫入磁盤, 不會輕易改變的意思, 這兒是事務提交之後, 會影響到數據庫, 不會丟失。這也就意味着, 隨着系統越來越龐大,我們爲了提高可用性、維護性、吞吐量等等技術指標,就算改善原有架構,業務計算的問題解決後,數據庫還是會成爲整個系統中的瓶頸。

一致性的討論

ACID 本質而言都是爲了保護數據的一致性, 而數據數據持久化時會觸發數據庫操作, 造成效率低小, 所以圍繞一致性 (效率) 產生了一些討論, 分別是強一致性、弱一致性、最終一致性。

強一致性

任何一次讀都能讀到某個數據的最近一次寫的數據。系統中的所有進程,看到的操作順序,都和全局時鐘下的順序一致。簡言之,在任意時刻,所有節點中的數據是一樣的, 這就要求數據一有改變就寫到數據庫。

弱一致性

數據更新後,不要求及時寫會數據庫以及同步到所有節點, 也就是這時候數據與真實數據可能有一些出入, 對於架構而言, 如果能容忍後續的訪問只能訪問到部分或者全部訪問不到,則是弱一致性。

最終一致性

不保證在任意時刻任意節點上的同一份數據都是相同的,也就是有些節點數據可能是準確的, 有的可能是不準確的, 但是隨着時間的遷移,不同節點上的同一份數據總是在向趨同的方向變化。簡單說,就是在一段時間後,節點間的數據會最終達到一致狀態。

三種一致性中, 強一致性數據更加可靠, 但是由於時時刻刻要求所有數據庫保持數據一致, 所以效率低下, 數據沒有統一完, 請求就沒法得到響應, 高併發場景下, 體驗不太好, 所以在實際使用中, 根據不同的業務選擇是一致性也不同, 購物時賬號付錢肯定是強一致性, 但是商品庫存數據就不一定非要強一致性, 至於商品下面的評論啥的, 甚至可以選擇弱一致性。

分庫分表

前面講過集羣的 AKF 拆分原則 (Redis 集羣拆分原則之 AKF), 大概意思是硬件性能是由上限的, 當硬件沒法支撐請求流量時, 可以將流量分發到不同的服務器上, AKF 拆分之 Y 軸、Z 軸拆分是業務拆分與數據拆分, 那也就會涉及到將數據庫中的數據拆分存儲在不同的地方, 這就叫分庫分表, 不同類型數據存儲在不同數據庫中做多機存儲和負載, 這樣一來, 傳統的事務機制 ACID 便無法正常運行。

分庫分表內容是數據切分(Sharding),以及切分後對數據的定位、整合。具體來說, 數據切分就是將數據分散存儲到多個數據庫中,使得單一數據庫中的數據量變小,通過擴充主機的數量緩解單一數據庫性能問題,從而達到提升數據庫操作性能的目的。

數據切分根據其切分類型,可以分爲兩種方式:垂直(縱向)切分和水平(橫向)切分。

垂直拆分

垂直切分常見有垂直分庫和垂直分表兩種, 兩種含義類似。

垂直分庫就是根據業務耦合性,將關聯度低的不同表存儲在不同的數據庫。做法與大系統拆分爲多個小系統類似,按業務分類進行獨立劃分。與 "微服務治理" 的做法相似,每個微服務使用單獨的一個數據庫。如圖:

垂直分表類似, 例如將一張表包含一個人所有信息, 例如姓名、身份證、性別、身高、體重、省、市、區、村、專業、G 點等等, 那麼可以拆分成三個表:

第一個表只包含基本信息 (姓名、身份證、性別、身高、體重);

第二個表包含籍貫信息 (省、市、區、村);

第三個表包含學習信息 (專業、G 點)。

垂直拆分優缺點

垂直切分的優點:

垂直切分的缺點:

水平拆分

上面對數據庫垂直拆分之後, 如果某個庫還是好大, 比如存儲的數據極其龐大, 那麼可以再對數據庫進行水平的拆分:

上面的水平拆分時按照 ID 區間來切分。例如:將 userId 爲 1~10000 的記錄分到第一個庫,10001~20000 的分到第二個庫,以此類推。某種意義上,某些系統中使用的 "冷熱數據分離",將一些使用較少的歷史數據遷移到其他庫中,業務功能上只提供熱點數據的查詢,也是類似的實踐。

除了上面按照用戶 ID 區間拆分, 也可以做 Hash 運算拆分, 這兒就不詳細展開了。

水平拆分優缺點

水平拆分優點在於:

水平拆分缺點:

分庫分錶帶來的問題

分庫分表能有效的緩解單機和單庫帶來的性能瓶頸和壓力,突破網絡 IO、硬件資源、連接數的瓶頸,同時也帶來了一些問題, 前面說過, 事務包含一組子操作, 這些造作要麼全部執行, 要麼全部不執行, 但是分庫之後, 一個事務可能涉及多個數據庫或者多個表擴庫執行, 而網絡具有不穩定性, 也就是事務執行難度加大, 分表分庫後事務爲了與傳統事務做出區別, 叫做分佈式事務 (跨分片事務)。

跨分片事務也是分佈式事務,沒有簡單的方案,一般可使用 "XA 協議" 和 "兩階段提交" 處理。

分佈式事務能最大限度保證了數據庫操作的原子性。但在提交事務時需要協調多個節點,推後了提交事務的時間點,延長了事務的執行時間。導致事務在訪問共享資源時發生衝突或死鎖的概率增高。隨着數據庫節點的增多,這種趨勢會越來越嚴重,從而成爲系統在數據庫層面上水平擴展的枷鎖。

最終一致性

對於那些性能要求很高,但對一致性要求不高的系統,往往不苛求系統的實時一致性,只要在允許的時間段內達到最終一致性即可,可採用事務補償的方式。與事務在執行中發生錯誤後立即回滾的方式不同,事務補償是一種事後檢查補救的措施,一些常見的實現方法有:對數據進行對賬檢查,基於日誌進行對比,定期同標準數據來源進行同步等等。事務補償還要結合業務系統來考慮。

分佈式事務解決思路

講這個之前需要先簡單回顧 CAP 原則和 Base 理論, 因爲分佈式事務不同於 ACID 的剛性事務,在分佈式場景下基於 BASE 理論,提出了柔性事務的概念。要想通過柔性事務來達到最終的一致性,就需要依賴於一些特性,這些特性在具體的方案中不一定都要滿足,因爲不同的方案要求不一樣;但是都不滿足的話,是不可能做柔性事務的。

CAP 原則

CAP 一般人可能聽了不下一百遍了, 很多人都說 CAP 是 "三選二" 的關係, 讓人誤以爲有 AC 這種情況, 但是實際 CAP 是二選一的關係, 這個在 2012 年已經有一篇論文進行解釋: CAP Twelve Years Later: How the "Rules" Have Changed

相當於是對之前三選二說法進行修正, CAP 中 P(分區容錯性)是必須具備的, 在滿足 P 的前提下, 很難同時滿足 A(可用性)和 C(一致性), 但是在之後, 又有一篇文章: Harvest, yield, and scalable tolerant systems , 這篇論文是基於上面那篇 “CAP 12 年後” 的論文寫的,它主要提出了 Harvest 和 Yield 概念,並把上面那篇論文中所討論的東西講得更爲仔細了一些。簡單來說就是滿足 P 之後, C 和 A 在放寬約束後可以得到兼顧, 並不是非此即彼的關係, 說遠了。

爲什麼 P 是必須的?

爲什麼 CAP 原則中分區容錯性是必須的呢, 首先要理解什麼是分區容錯性, 分區, 這兒說的是網絡, 網絡集羣設計到很多的服務器, 某一瞬間網絡不穩定, 那麼相當於將網絡分成了不同的區, 假設分成了兩個區, 這時候如果有一筆交易:

對分區一發出消息: A 給 B 轉賬 100 元, 對分區二發出消息: A 給 B 轉賬 200 元

那麼對於兩個分區而言, 有兩種情況:

a)無可用性,即這兩筆交易至少會有一筆交易不會被接受;

b)無一致性,一半看到的是 A 給 B 轉賬 100 元而另一半則看到 A 給 B 轉賬 200 元。

所以, 分區容忍性必須要滿足, 解決策略是一個數據項複製到多個節點上,那麼出現分區之後,這一數據項就可能分佈到各個區裏。容忍性就提高了。

Base 理論

在很多時候,我們並不需要強一致性的系統,所以後來,人們爭論關於數據一致性和可用性時,主要是集中在強一致性的 ACID 或最終一致性的 BASE 中, BASE 是對 CAP 中一致性和可用性權衡的結果,其來源於對大規模互聯網分佈式系統實踐的總結,是基於 CAP 定律逐步演化而來。其核心思想是即使無法做到強一致性,但每個應用都可以根據自身業務特點,才用適當的方式來使系統打到最終一致性。

BASE 理論是 Basically Available(基本可用),Soft State(軟狀態)和 Eventually Consistent(最終一致性)三個短語的縮寫。

基本可用

假設系統,出現了不可預知的故障,但還是能用,相比較正常的系統而言:

  1. 響應時間上的損失 :正常情況下的搜索引擎 0.5 秒即返回給用戶結果,而基本可用的搜索引擎可以在 2 秒作用返回結果。

  2. 功能上的損失 :在一個電商網站上,正常情況下,用戶可以順利完成每一筆訂單。但是到了大促期間,爲了保護購物系統的穩定性,部分消費者可能會被引導到一個降級頁面。

這就叫基本可用

軟狀態

相對於原子性而言,要求多個節點的數據副本都是一致的,這是一種 “硬狀態”。軟狀態指的是:允許系統中的數據存在中間狀態,並認爲該狀態不影響系統的整體可用性,即允許系統在多個不同節點的數據副本存在數據延時。

最終一致性

上面說軟狀態,然後不可能一直是軟狀態,必須有個時間期限。在期限過後,應當保證所有副本保持數據一致性,從而達到數據的最終一致性。這個時間期限取決於網絡延時、系統負載、數據複製方案設計等等因素。

Base 其核心思想是:

既然無法做到強一致性(Strong consistency),但每個應用都可以根據自身的業務特點,採用適當的方式來使系統達到最終一致性(Eventual consistency)。有了 Base 理論就可以開始講述分佈式事務的處理思路了。

二階段提交協議

二階段提交 (2PC:Two-Phase Commit), 顧名思義,該協議將一個分佈式的事務過程拆分成兩個階段: 投票 和 事務提交 。爲了讓整個數據庫集羣能夠正常的運行,該協議指定了一個 協調者 單點,用於協調整個數據庫集羣各節點的運行。爲了簡化描述,我們將數據庫集羣中的各個節點稱爲 參與者 ,三階段提交協議中同樣包含協調者和參與者這兩個角色定義, 後面再說。

第一階段:投票

該階段的主要目的在於打探數據庫集羣中的各個參與者是否能夠正常的執行事務,具體步驟如下:

  1. 協調者向所有的參與者發送事務執行請求,並等待參與者反饋事務執行結果;

  2. 事務參與者收到請求之後,執行事務但不提交,並記錄事務日誌;

  3. 參與者將自己事務執行情況反饋給協調者,同時阻塞等待協調者的後續指令。

第二階段:事務提交

在經過第一階段協調者的詢盤之後,各個參與者會回覆自己事務的執行情況,這時候存在 3 種可能性:

  1. 所有的參與者都回復能夠正常執行事務。

  2. 一個或多個參與者回覆事務執行失敗。

  3. 協調者等待超時。

對於第 1 種情況,協調者將向所有的參與者發出提交事務的通知,具體步驟如下:

  1. 協調者向各個參與者發送 commit 通知,請求提交事務;

  2. 參與者收到事務提交通知之後執行 commit 操作,然後釋放佔有的資源;

  3. 參與者向協調者返回事務 commit 結果信息。

對於第 2 和第 3 種情況,協調者均認爲參與者無法成功執行事務,爲了整個集羣數據的一致性,所以要向各個參與者發送事務回滾通知,具體步驟如下:

  1. 協調者向各個參與者發送事務 rollback 通知,請求回滾事務;

  2. 參與者收到事務回滾通知之後執行 rollback 操作,然後釋放佔有的資源;

  3. 參與者向協調者返回事務 rollback 結果信息。

兩階段提交協議解決的是分佈式數據庫數據強一致性問題,實際應用中更多的是用來解決事務操作的原子性,下圖描繪了協調者與參與者的狀態轉換。

站在協調者的角度,在發起投票之後就進入了 WAIT 等待狀態,等待所有參與者回覆各自事務執行狀態,並在收到所有參與者的回覆後決策下一步是發送 commit 提交 或 rollback 回滾信息。

站在參與者的角度,當回覆完協調者的投票請求之後便進入 READY 狀態(能夠正常執行事務),接下去就是等待協調者最終的決策通知,一旦收到通知便可依據決策執行 commit 或 rollback 操作。

兩階段提交協議原理簡單、易於實現,但是缺點也是顯而易見的,包含如下:

單點問題

協調者在整個兩階段提交過程中扮演着舉足輕重的作用,一旦協調者所在服務器宕機,就會影響整個數據庫集羣的正常運行。比如在第二階段中,如果協調者因爲故障不能正常發送事務提交或回滾通知,那麼參與者們將一直處於阻塞狀態,整個數據庫集羣將無法提供服務。

同步阻塞

兩階段提交執行過程中,所有的參與者都需要聽從協調者的統一調度,期間處於阻塞狀態而不能從事其他操作,這樣效率極其低下。

數據不一致性

兩階段提交協議雖然是分佈式數據強一致性所設計,但仍然存在數據不一致性的可能性。比如在第二階段中,假設協調者發出了事務 commit 通知,但是因爲網絡問題該通知僅被一部分參與者所收到並執行了 commit 操作,其餘的參與者則因爲沒有收到通知一直處於阻塞狀態,這時候就產生了數據的不一致性。

針對上述問題可以引入 超時機制 和 互詢機制在很大程度上予以解決。

超時機制

對於協調者來說如果在指定時間內沒有收到所有參與者的應答,則可以自動退出 WAIT 狀態,並向所有參與者發送 rollback 通知。對於參與者來說如果位於 READY 狀態,但是在指定時間內沒有收到協調者的第二階段通知,則不能武斷地執行 rollback 操作,因爲協調者可能發送的是 commit 通知,這個時候執行 rollback 就會導致數據不一致。

互詢機制

此時,我們可以介入互詢機制,讓參與者 A 去詢問其他參與者 B 的執行情況。如果 B 執行了 rollback 或 commit 操作,則 A 可以大膽的與 B 執行相同的操作;如果 B 此時還沒有到達 READY 狀態,則可以推斷出協調者發出的肯定是 rollback 通知;如果 B 同樣位於 READY 狀態,則 A 可以繼續詢問另外的參與者。只有當所有的參與者都位於 READY 狀態時,此時兩階段提交協議無法處理,將陷入長時間的阻塞狀態。

三階段提交協議

三階段提交協議(3PC:Three-Phase Commit), 針對兩階段提交存在的問題,三階段提交協議通過引入一個 預詢盤 階段,以及超時策略來減少整個集羣的阻塞時間,提升系統性能。三階段提交的三個階段分別爲:預詢盤(can_commit)、預提交(pre_commit),以及事務提交(do_commit)。

第一階段:預詢盤

該階段協調者會去詢問各個參與者是否能夠正常執行事務,參與者根據自身情況回覆一個預估值,相對於真正的執行事務,這個過程是輕量的,具體步驟如下:

  1. 協調者向各個參與者發送事務詢問通知,詢問是否可以執行事務操作,並等待回覆;

  2. 各個參與者依據自身狀況回覆一個預估值,如果預估自己能夠正常執行事務就返回確定信息,並進入預備狀態,否則返回否定信息。

第二階段:預提交

本階段協調者會根據第一階段的詢盤結果採取相應操作,詢盤結果主要有 3 種:

  1. 所有的參與者都返回確定信息。

  2. 一個或多個參與者返回否定信息。

  3. 協調者等待超時。

針對第 1 種情況,協調者會向所有參與者發送事務執行請求,具體步驟如下:

  1. 協調者向所有的事務參與者發送事務執行通知;

  2. 參與者收到通知後執行事務但不提交;

  3. 參與者將事務執行情況返回給客戶端。

在上述步驟中,如果參與者等待超時,則會中斷事務。針對第 2 和第 3 種情況,協調者認爲事務無法正常執行,於是向各個參與者發出 abort 通知,請求退出預備狀態,具體步驟如下:

  1. 協調者向所有事務參與者發送 abort 通知;

  2. 參與者收到通知後中斷事務。

第三階段:事務提交

如果第二階段事務未中斷,那麼本階段協調者將會依據事務執行返回的結果來決定提交或回滾事務,分爲 3 種情況:

  1. 所有的參與者都能正常執行事務。

  2. 一個或多個參與者執行事務失敗。

  3. 協調者等待超時。

針對第 1 種情況,協調者向各個參與者發起事務提交請求,具體步驟如下:

  1. 協調者向所有參與者發送事務 commit 通知;

  2. 所有參與者在收到通知之後執行 commit 操作,並釋放佔有的資源;

  3. 參與者向協調者反饋事務提交結果。

針對第 2 和第 3 種情況,協調者認爲事務無法成功執行,於是向各個參與者發送事務回滾請求,具體步驟如下:

  1. 協調者向所有參與者發送事務 rollback 通知;

  2. 所有參與者在收到通知之後執行 rollback 操作,並釋放佔有的資源;

  3. 參與者向協調者反饋事務回滾結果。

在本階段如果因爲協調者或網絡問題,導致參與者遲遲不能收到來自協調者的 commit 或 rollback 請求,那麼參與者將不會如兩階段提交中那樣陷入阻塞,而是等待超時後繼續 commit,相對於兩階段提交雖然降低了同步阻塞,但仍然無法完全避免數據的不一致。兩階段提交協議中所存在的長時間阻塞狀態發生的幾率還是非常低的,所以雖然三階段提交協議相對於兩階段提交協議對於數據強一致性更有保障,但是因爲效率問題,兩階段提交協議在實際系統中反而更加受寵。

TCC 模式

TCC 是 Try、Confirm 和 Cancel 三個單詞首字母縮寫, 它們分別的職責是:

Try:負責預留資源(比如新建一條狀態 = PENDING 的訂單);

做業務檢查,簡單來說就是不能預留已經被佔用的資源;

隔離預留資源。

Confirm:負責落地所預留的資源

真正的執行業務使用 try 階段預留的資源, 冪等。

Cancel:負責撤銷所預留的資源

需要用戶根據自己的業務場景實現 Try、Confirm 和 Cancel 三個操作;事務發起方在一階段執行 Try 方式,在二階段提交執行 Confirm 方法,二階段回滾執行 Cancel 方法。

關於預留資源要多說兩句,資源都是有限的,因此預留資源都是有時效的,如果當預留資源遲遲得不到 Confirm——我們將這種情況稱爲 timeout——參與方會自行將其 Cancel。也就是說參與方對於資源具有自我管理能力,這樣可以避免因發起方的問題導致資源被長期佔用。

TCC 增加了業務檢查和撤銷事務的功能。同時,TCC 將 2PC 數據庫層面的動作提升到了服務層面,不同的是 TCC 的所有動作都是一個本地事務,每個本地事務都在動作完成後 commit 到數據庫:

流程步驟:

  1. 發起方 發送 Try 到所有 參與方

  2. 每個 參與方 執行 Try,預留資源

  3. 發起方 收到所有 參與方 的 Try 結果

  4. 發起方 發送 Confirm/Cancel 到所有 參與房

  5. 每個 參與方 執行 Confirm/Cancel

  6. 發起方 收到所有 參與方 的 Confirm/Cancel 結果

流程和兩階段提交非常類似。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/EJ9hPRVfzB2P4jJNtzKFYg