一個簡單的例子,電商系統中,下單介面,一般會有扣庫存,扣積分,然後生成訂單。而一般來說,這三個系統都是不同的服務,我們本地不能控制其他服務的事務,此時如果訂單服務發生了錯誤進行了回滾,但遠端的服務,如扣庫存已經呼叫完成,不能進行回滾了。 也就是說下單介面的成功與否,不僅取決於本地的 db 操作,而且依賴第三方系統的結果, 這時候分散式事務就保證這些操作要麼全部成功,要麼全部失敗。本質上來說,分散式事務就是為了保證不同資料庫的資料一致性。
在講分散式事務之前,先回顧下本地事務的知識點。
一、本地事務
嚴格意義上的事務實現應該是具備原子性( Atomicity )、一致性( Consistency )、隔離性( Isolation )和永續性(Durability),簡稱 ACID。
-
原子性:一系列操作整體不可拆分,要麼都執行,要麼都不執行。
-
一致性: 事務的執行不能破壞資料庫資料的完整性和一致性,一個事務在執行之前和執行之後,資料庫都必須處於一致性狀態
-
隔離性:事務之間相互隔離, 指的是多個事務併發執行的時候不會互相干擾,即一個事務內部的資料對於其他事務來說是隔離的。
-
永續性: 一旦事務提交,那麼它對資料庫中的對應資料的狀態的變更就會永久儲存到資料庫中
通俗意義上事務就是為了使得一些更新操作要麼都成功,要麼都失敗。
隔離性中還有一個隔離級別的概念,總共有4個事務隔離級別,不同的隔離級別對事務的處理不同,分別是:未提交讀,已提交讀, 可重複讀,序列化。這裡面又牽扯到三個概念,髒讀、不可重複讀 ,幻讀,我們先理解這三個概念
-
髒讀:所謂的髒讀,其實就是讀到了別的事務回滾前的髒資料
-
不可重複讀:當前事務先進行了一次資料讀取,然後再次讀取到的資料是別的事務修改成功的資料,導致兩次讀取到的資料不匹配
-
幻讀:當前事務讀第一次取到的資料比後來讀取到資料條目少
而事務的隔離級別其實就是如何避免這三種
-
未提交讀: 該隔離級別允許髒讀取,其隔離級別最低,也就是啥都沒避免。
-
已提交讀 :一個事務可以讀取已提交的事務,保證了一個事務不會讀到另一個並行事務已修改但未提交的資料。但是不保證可重複讀,也就是不保證多次讀取資料都相同。
-
可重複讀:保證多次讀取一個資料時都跟開始讀取的時候一樣, 因此該事務級別禁止不可重複讀取和髒讀取,但是有可能出現幻讀資料。
-
序列化: 是最嚴格的事務隔離級別,它要求所有事務被序列執行,即事務只能一個接一個的進行處理,不能併發執行。 該隔離級別能防止髒讀、不可重複讀、幻讀。
Mysql預設級別是可重複讀,在編寫程式碼時是可以進行設定的。
而在Spring中七種事務傳播行為事務的傳播行為概念:
-
PROPAGATION_REQUIRED: 如果當前存在事務,則加入該事務,如果當前不存在事務,則建立一個新的事務。
-
PROPAGATION_SUPPORTS: 如果存在一個事務,支援當前事務。如果沒有事務,則非事務的執行。
-
PROPAGATION_MANDATORY: 如果已經存在一個事務,支援當前事務。如果沒有一個活動的事務,則丟擲異常。
-
PROPAGATION_REQUIRES_NEW: 重新建立一個新的事務,如果當前存在事務,延緩當前的事務。
-
PROPAGATION_NOT_SUPPORTED: 以非事務的方式執行,如果當前存在事務,暫停當前的事務。
-
PROPAGATION_NEVER: 總是非事務地執行,如果存在一個活動事務,則丟擲異常。
-
PROPAGATION_NESTED: 如果沒有,就新建一個事務;如果有,就在當前事務中巢狀其他事務。
這裡有個點,Spring的事務實現是通過代理類實現的,所以同一個物件內事務呼叫是預設失效的, 預設只有在外部呼叫事務才會生效 。
二、分散式事務
回顧完本地事務,讓我們回到分散式事務的學習,分散式事務的實現是建立在很多概念之上的,讓我們先來理解下基礎概念吧。
2.1 CAP定理
CAP是分散式當中一個非常重要的理論,指的是在一個分散式系統中一致性 (Consistency)、可用性 (Availability)、分割槽容錯性(Partition tolerance),三者不可得兼。
-
一致性:在分散式系統中的所有資料備份,在同一時刻是否同樣的值。(等同於所有節點訪問同一份最新的資料副本)
-
可用性:在叢集中一部分節點故障後,叢集整體是否還能響應客戶端的讀寫請求。(對資料更新具備高可用性)
-
分割槽容錯性:分散式系統在遇到任何網路分割槽故障的時候,仍然需要能夠保證對外提供滿足一致性和可用性的服務,除非整個網路環境都發生故障。
CAP理論是指一個分散式系統不可能同時滿足一致性,可用性和分割槽容錯性這個三個基本需求,最多隻能同時滿足其中兩項。
舉個例子:有A、B、C三個服務,都儲存一份資料是7,而當A服務將這個資料改成8,同步到B時,正常,但同步至C時出現了異常,此時C仍然是7,如果此時依舊要保持一致性,那麼C服務就不能可用。
-
放棄P(CA):如果希望能夠避免系統出現分割槽容錯性問題,一種較為簡單的做法就是將所有的資料(或者是與事物先相關的資料)都放在一個分散式節點上,這樣雖然無法保證100%系統不會出錯,但至少不會碰到由於網路分割槽帶來的負面影響。但是這樣其實就不是分散式系統了,
-
放棄A(CP):其做法是一旦系統遇到網路分割槽或其他故障時,那受到影響的服務需要等待一定的時間,應用等待期間系統無法對外提供正常的服務,即不可用
-
放棄C(AP):這裡說的放棄一致性,並不是完全不需要資料一致性,是指放棄資料的強一致性,保留資料的最終一致性。
大多數時候我們是選擇AP,也就是選擇放棄一致性,但是這不是絕對的,在一些業務中,例如,轉賬業務,是放棄了可用性。
2.2 BASE理論
BASE 理論指的是基本可用 Basically Available,軟狀態 Soft State,最終一致性 Eventual Consistency,核心思想是即便無法做到強一致性,但應該採用適合的方式保證最終一致性。
-
BA: Basically Available 基本可用,分散式系統在出現故障的時候,允許損失部分可用性,即保證核心可用。
-
S:Soft State 軟狀態,允許系統存在中間狀態,而該中間狀態不會影響系統整體可用性。
-
E: Eventual Consistency 最終一致性,系統中的所有資料副本經過一定時間後,最終能夠達到一致的狀態。
這裡需要解釋下強一致性,弱一致性和最終一致性。
強一致性:任何一次讀都能讀到某個資料的最近一次寫的資料。系統中的所有程式,看到的操作順序,都和全域性時鐘下的順序一致。簡言之,在任意時刻,所有節點中的資料是一樣的。
弱一致性: 資料更新後,如果能容忍後續的訪問只能訪問到部分或者全部訪問不到,則是弱一致性。
最終一致性: 不保證在任意時刻任意節點上的同一份資料都是相同的,但是隨著時間的遷移,不同節點上的同一份資料總是在向趨同的方向變化。簡單說,就是在一段時間後,節點間的資料會最終達到一致狀態。
三、分散式事務的幾種方案
上面都是一些理論,用這些理論作為指導,分散式事務有以下一些解決方案
3.1 2PC/XA
兩階段提交,顧名思義就是要分兩步提交。存在一個負責協調各個本地資源管理器的事務管理器,本地資源管理器一般是由資料庫實現,事務管理器在第一階段的時候詢問各個資源管理器是否都就緒?如果收到每個資源的回覆都是 yes,則在第二階段提交事務,如果其中任意一個資源的回覆是 no, 則回滾事務。
階段1:提交事務請求
事務管理器向所有本地資源管理器發起請求,詢問是否是 ready 狀態,所有參與者都將本事務能否成功的資訊反饋發給協調者;
-
事務詢問:協調者向所有的參與者傳送事務內容,詢問是否可以執行事務提交操作,並開始等待各參與者的響應
-
執行事務:各參與者節點執行事務操作,並將Undo和Redo資訊記入事務日誌中
-
如果參與者成功執事務操作,就反饋給協調者Yes響應,表示事物可以執行,如果沒有成功執行事務,就反饋給協調者No響應,表示事務不可以執行
-
二階段提交一些的階段也被稱為投票階段,即各參與者投票票表明是否可以繼續執行接下去的事務提交操作
二階段: 執行事務提交
-
假如協調者從所有的參與者或得反饋都是Yes響應,那麼就會執行事務提交。
-
傳送提交請求:協調者向所有參與者節點發出Commit請求
-
事務提交:參與者接受到Commit請求後,會正式執行事務提交操作,並在完成提交之後放棄整個事務執行期間佔用的事務資源
-
反饋事務提交結果:參與者在完成事物提交之後,向協調者傳送ACK訊息
-
完成事務:協調者接收到所有參與者反饋的ACK訊息後,完成事務
中斷事務
-
假如任何一個參與者向協調者反饋了No響應,或者在等待超時之後,協調者尚無法接收到所有參與者的反饋響應,那麼就中斷事務。
-
傳送回滾請求:協調者向所有參與者節點發出Rollback請求
-
事務回滾:參與者接收到Rollback請求後,會利用其在階段一種記錄的Undo資訊執行事物回滾操作,並在完成回滾之後釋放事務執行期間佔用的資源。
-
反饋事務回滾結果:參與則在完成事務回滾之後,向協調者傳送ACK訊息
-
中斷事務:協調者接收到所有參與者反饋的ACk訊息後,完成事務中斷
這種解決方案優點是實現簡單,缺點也很明顯:
同步阻塞:當參與事務者存在佔用公共資源的情況,其中一個佔用了資源,其他事務參與者就只能阻塞等待資源釋放,處於阻塞狀態。
單點故障:一旦事務管理器出現故障,整個系統不可用
資料不一致:在階段二,如果事務管理器只傳送了部分 commit 訊息,此時網路發生異常,那麼只有部分參與者接收到 commit 訊息,也就是說只有部分參與者提交了事務,使得系統資料不一致。
不確定性:當協事務管理器傳送 commit 之後,並且此時只有一個參與者收到了 commit,那麼當該參與者與事務管理器同時當機之後,重新選舉的事務管理器無法確定該條訊息是否提交成功。
還有3PC提交,是對2PC提交做了一些改進
-
與兩階段提交不同的是,三階段提交有兩個改動點。引入超時機制。同時在協調者和參與者中都引入超時機制。在第一階段和第二階段中插入一個準備階段。保證了在最後提交階段之前各參與節點的狀態是一致的。
-
三階段提交就有CanCommit、PreCommit、DoCommit三個階段。
3.2 TCC
TCC 指的是Try - Confirm - Cancel
。
-
Try 指的是預留,即資源的預留和鎖定,注意是預留。
-
Confirm 指的是確認操作,這一步其實就是真正的執行了。
-
Cancel 指的是撤銷操作,可以理解為把預留階段的動作撤銷了。
TCC 事務機制相比於上面介紹的 XA,解決了其幾個缺點:
-
解決了協調者單點,由主業務方發起並完成這個業務活動。業務活動管理器也變成多點,引入叢集。
-
同步阻塞:引入超時,超時後進行補償,並且不會鎖定整個資源,將資源轉換為業務邏輯形式,粒度變小。
-
資料一致性,有了補償機制之後,由業務活動管理器控制一致性
TCC(Try Confirm Cancel) Try 階段:嘗試執行,完成所有業務檢查(一致性), 預留必須業務資源(準隔離性) Confirm 階段:確認執行真正執行業務,不作任何業務檢查,只使用 Try 階段預留的業務資源,Confirm 操作滿足冪等性。要求具備冪等設計,Confirm 失敗後需要進行重試。 Cancel 階段:取消執行,釋放 Try 階段預留的業務資源 Cancel 操作滿足冪等性 Cancel 階段的異常和 Confirm 階段異常處理方案基本上一致。
在 Try 階段,是對業務系統進行檢查及資源預覽,比如訂單和儲存操作,需要檢查庫存剩餘數量是否夠用,並進行預留,預留操作的話就是新建一個可用庫存數量欄位,Try 階段操作是對這個可用庫存數量進行操作。 基於 TCC 實現分散式事務,會將原來只需要一個介面就可以實現的邏輯拆分為 Try、Confirm、Cancel 三個介面,所以程式碼實現複雜度相對較高。
3.3 本地訊息表
本地訊息表其實就是利用了 各系統本地的事務來實現分散式事務。
-
當系統 A 被其他系統呼叫發生資料庫表更操作,首先會更新資料庫的業務表,其次會往相同資料庫的訊息表中插入一條資料,兩個操作發生在同一個事務中
-
系統 A 的指令碼定期輪詢本地訊息往 mq 中寫入一條訊息,如果訊息傳送失敗會進行重試
-
系統 B 消費 mq 中的訊息,並處理業務邏輯。如果本地事務處理失敗,會在繼續消費 mq 中的訊息進行重試,如果業務上的失敗,可以通知系統 A 進行回滾操作
-
消費者與生成者的介面都要支援冪等
-
生產者需要額外的建立訊息表
-
需要提供補償邏輯,如果消費者業務失敗,需要生產者支援回滾操作
容錯機制:
-
步驟 1 失敗時,事務直接回滾
-
步驟 2、3 寫 mq 與消費 mq 失敗會進行重試
-
步驟 3 業務失敗系統 B 向系統 A 發起事務回滾操作
此方案的核心是將需要分散式處理的任務通過訊息日誌的方式來非同步執行。訊息日誌可以儲存到本地文字、資料庫或訊息佇列,再通過業務規則自動或人工發起重試。人工重試更多的是應用於支付場景,通過對賬系統對事後問題的處理。
4.4 可靠訊息最終一致性
-
A 系統先向 mq 傳送一條 prepare 訊息,如果 prepare 訊息傳送失敗,則直接取消操作
-
如果訊息傳送成功,則執行本地事務
-
如果本地事務執行成功,則想 mq 傳送一條 confirm 訊息,如果傳送失敗,則傳送回滾訊息
-
B 系統定期消費 mq 中的 confirm 訊息,執行本地事務,併傳送 ack 訊息。如果 B 系統中的本地事務失敗,會一直不斷重試,如果是業務失敗,會向 A 系統發起回滾請求
-
mq 會定期輪詢所有 prepared 訊息呼叫系統 A 提供的介面查詢訊息的處理情況,如果該 prepare 訊息本地事務處理成功,則重新傳送 confirm 訊息,否則直接回滾該訊息
該方案與本地訊息最大的不同是去掉了本地訊息表,其次本地訊息表依賴訊息表重試寫入 mq 這一步由本方案中的輪詢 prepare 訊息狀態來重試或者回滾該訊息替代。其實現條件與餘容錯方案基本一致。目前市面上實現該方案的只有阿里的 RocketMq。
5.5 盡最大努力通知
最大努力通知是最簡單的一種柔性事務,適用於一些最終一致性時間敏感度低的業務,且被動方處理結果 不影響主動方的處理結果。
這個方案的大致意思就是:
-
系統 A 本地事務執行完之後,傳送個訊息到 MQ;
-
這裡會有個專門消費 MQ 的服務,這個服務會消費 MQ 並呼叫系統 B 的介面;
-
要是系統 B 執行成功就 ok 了;要是系統 B 執行失敗了,那麼最大努力通知服務就定時嘗試重新呼叫系統 B, 反覆 N 次,最後還是不行就放棄。
理論到這裡,下一篇,我們實戰阿里推出的Seata