導讀面試
在上一篇文章《【分佈式事務】基於RocketMQ搭建生產級消息集羣?》中給你們介紹了基於RocketMQ如何搭建生產級消息集羣。由於本系列文章最終的目的是介紹基於RocketMQ的事物消息來解決分佈式系統中的數據一致性問題,因此先給你們率先介紹了RocketMQ消息集羣的搭建。數據庫
本來是想着在這篇文章中直接介紹RocketMQ的事務消息特性,可是在梳理的過程當中做者發現對於分佈式事務的概念,可能還會有不少同窗不理解或者理解得不是很深入的地方,而跳過這些基本概念直接去學習上層的實踐可能並非一件很好的事情,所以在這篇文章中,做者打算重點給你們先介紹下分佈式事務相關的基本概念,諸如分佈式事務、2PC、3PC、TCC之類的基本問題,以後再單獨去介紹RocketMQ事務消息相關的實踐。網絡
數據庫事務的概念架構
在講述分佈式事務的概念以前,咱們先來回顧下事務相關的一些概念。併發
事務的基本概念:異步
就是一個程序執行單元,裏面的操做要麼所有執行成功,要麼所有執行失敗,不容許只成功一半另一半執行失敗的事情發生。例如一段事務代碼作了兩次數據庫更新操做,那麼這兩次數據庫操做要麼所有執行成功,要麼所有回滾。分佈式
事務的基本特性:微服務
咱們知道事務有4個很是重要的特性,即咱們常說的(ACID)。性能
Atomicity(原子性):是說事務是一個不可分割的總體,全部操做要麼全作,要麼全不作;只要事務中有一個操做出錯,回滾到事務開始前的狀態的話,那麼以前已經執行的全部操做都是無效的,都應該回滾到開始前的狀態。學習
Consistency(一致性):是說事務執行先後,數據從一個狀態到另外一個狀態必須是一致的,好比A向B轉帳( A、B的總金額就是一個一致性狀態),不可能出現A扣了錢,B卻沒收到的狀況發生。
Isolation(隔離性): 多個併發事務之間相互隔離,不能互相干擾。關於事務的隔離性,可能不是特別好理解,這裏的併發事務是指兩個事務操做了同一份數據的狀況;而對於併發事務操做同一份數據的隔離性問題,則是要求不能出現髒讀、幻讀的狀況,即事務A不能讀取事務B尚未提交的數據,或者在事務A讀取數據進行更新操做時,不容許事務B率先更新掉這條數據。而爲了解決這個問題,經常使用的手段就是加鎖了,對於數據庫來講就是經過數據庫的相關鎖機制來保證。
Durablity(持久性):事務完成後,對數據庫的更改是永久保存的,不能回滾。
關於數據庫事務的基本概念你們能夠去網上搜一下,這裏只是給你們回顧下事務的基本概念及特性,諸如事務併發問題、事務隔離級別等你們若有遺忘能夠去回顧下(tips:面試常常會問到的問題哦)。
什麼是分佈式事務
以上內容咱們回顧了下事務的基本概念,那麼分佈式事務又是個什麼概念呢?它與數據庫事務之間又有什麼區別呢?
其實分佈式事務從實質上看與數據庫事務的概念是一致的,既然是事務也就須要知足事務的基本特性(ACID),只是分佈式事務相對於本地事務而言其表現形式有很大的不一樣。舉個例子,在一個JVM進程中若是須要同時操做數據庫的多條記錄,而這些操做須要在一個事務中,那麼咱們能夠經過數據庫提供的事務機制(通常是數據庫鎖)來實現。
而隨着這個JVM進程(應用)被拆分紅了微服務架構,本來一個本地邏輯執行單元被拆分到了多個獨立的微服務中,這些微服務又分別操做不一樣的數據庫和表,服務之間經過網絡調用。
舉個例子:服務A收到一筆購物下單請求後,須要調用服務B去支付,支付成功則處理購物訂單爲待發貨狀態,不然就須要將購物訂單處理爲失敗狀態。(如圖所示)
在上面這個例子中會不會出現服務B支付成功了,可是因爲網絡調用的問題沒有通知到服務A,致使用戶付了錢,可是購物訂單沒法顯示支付成功的狀態呢?
答案是這種狀況是廣泛存在的,由於服務B在處理成功後須要向服務A發送網絡請求,而這個過程是極有可能失敗的。那麼如何確保「服務A->服務B」這個過程可以組成一個事務,要麼所有成功、要麼所有失敗呢?而這就是典型的須要經過分佈式事務解決的問題。
分佈式事務是爲了解決微服務架構(形式都是分佈式系統)中不一樣節點之間的數據一致性問題。這個一致性問題本質上解決的也是傳統事務須要解決的問題,即一個請求在多個微服務調用鏈中,全部服務的數據處理要麼所有成功,要麼所有回滾。固然分佈式事務問題的形式可能與傳統事務會有比較大的差別,可是問題本質是一致的,都是要求解決數據的一致性問題。
而分佈式事務的實現方式有不少種,最具備表明性的是由Oracle Tuxedo系統提出的XA分佈式事務協議。XA協議包括兩階段提交(2PC)和三階段提交(3PC)兩種實現,接下來咱們分別來介紹下這兩種實現方式的原理。
兩階段提交(2PC)
兩階段提交又稱2PC(two-phase commit protocol),2pc是一個很是經典的強一致、中心化的原子提交協議。這裏所說的中心化是指協議中有兩類節點:一個是中心化協調者節點(coordinator)和N個參與者節點(partcipant)。
下面咱們就以一個儘可能貼近實際業務場景的操做來舉例:"假設在一個分佈式架構的系統中事務的發起者經過分佈式事務協調者(如RocketMQ,在早期RocketMQ版本不提供事務消息特性時,有些公司會本身研發一個基於MQ的可靠消息服務來實現必定的分佈式事務的特性)分別嚮應用服務A、應用服務B發起處理請求,兩者在處理的過程當中會分別操做自身服務的數據庫,如今要求應用服務A、應用服務B的數據處理操做要在一個事務裏"?
在上面這個例子中若是採用兩階段提交來實現分佈式事務,那麼其運行原理應該是個什麼樣的呢?(如👇):
第一階段:請求/表決階段(點擊放大)
既然稱爲兩階段提交,說明在這個過程當中是大體存在兩個階段的處理流程。第一個階段如👆圖所示,這個階段被稱之爲請求/表決階段。是個什麼意思呢?
就是在分佈式事務的發起方在向分佈式事務協調者(Coordinator)發送請求時,Coordinator首先會分別向參與者(Partcipant)節點A、參與這節點(Partcipant)節點B分別發送事務預處理請求,稱之爲Prepare,有些資料也叫"Vote Request"。
說的直白點就是問一下這些參與節點"這件事大家能不能處理成功了",此時這些參與者節點通常來講就會打開本地數據庫事務,而後開始執行數據庫本地事務,但在執行完成後並不會立馬提交數據庫本地事務,而是先向Coordinator報告說:「我這邊能夠處理了/我這邊不能處理」。
若是全部的參與這節點都向協調者做了「Vote Commit」的反饋的話,那麼此時流程就會進入第二個階段了。
第二階段:提交/執行階段(正常流程)
若是全部參與者節點都向協調者報告說「我這邊能夠處理」,那麼此時協調者就會向全部參與者節點發送「全局提交確認通知(global_commit)」,即大家均可以進行本地事務提交了,此時參與者節點就會完成自身本地數據庫事務的提交,並最終將提交結果回覆「ack」消息給Coordinator,而後Coordinator就會向調用方返回分佈式事務處理完成的結果。
第二階段:提交/執行階段(異常流程)
相反,在第二階段除了全部的參與者節點都反饋「我這邊能夠處理了」的狀況外,也會有節點反饋說「我這邊不能處理」的狀況發生,此時參與者節點就會向協調者節點反饋「Vote_Abort」的消息。此時分佈式事務協調者節點就會向全部的參與者節點發起事務回滾的消息(「global_rollback」),此時各個參與者節點就會回滾本地事務,釋放資源,而且向協調者節點發送「ack」確認消息,協調者節點就會向調用方返回分佈式事務處理失敗的結果。
以上就是兩階段提交的基本過程了,那麼按照這個兩階段提交協議,分佈式系統的數據一致性問題就能獲得知足嗎?
實際上分佈式事務是一件很是複雜的事情,兩階段提交只是經過增長了事務協調者(Coordinator)的角色來經過2個階段的處理流程來解決分佈式系統中一個事務須要跨多個服務節點的數據一致性問題。可是從異常狀況上考慮,這個流程也並非那麼的無懈可擊。
假設若是在第二個階段中Coordinator在接收到Partcipant的"Vote_Request"後掛掉了或者網絡出現了異常,那麼此時Partcipant節點就會一直處於本地事務掛起的狀態,從而長時間地佔用資源。固然這種狀況只會出如今極端狀況下,然而做爲一套健壯的軟件系統而言,異常Case的處理纔是真正考驗方案正確性的地方。
如下幾點是XA-兩階段提交協議中會遇到的一些問題:
性能問題。從流程上咱們能夠看得出,其最大缺點就在於它的執行過程當中間,節點都處於阻塞狀態。各個操做數據庫的節點此時都佔用着數據庫資源,只有當全部節點準備完畢,事務協調者纔會通知進行全局提交,參與者進行本地事務提交後纔會釋放資源。這樣的過程會比較漫長,對性能影響比較大。
協調者單點故障問題。事務協調者是整個XA模型的核心,一旦事務協調者節點掛掉,會致使參與者收不到提交或回滾的通知,從而致使參與者節點始終處於事務沒法完成的中間狀態。
丟失消息致使的數據不一致問題。在第二個階段,若是發生局部網絡問題,一部分事務參與者收到了提交消息,另外一部分事務參與者沒收到提交消息,那麼就會致使節點間數據的不一致問題。
既然兩階段提交有以上問題,那麼有沒有其餘的方案來解決呢?
三階段提交(3PC)
三階段提交又稱3PC,其在兩階段提交的基礎上增長了CanCommit階段,並引入了超時機制。一旦事務參與者遲遲沒有收到協調者的Commit請求,就會自動進行本地commit,這樣相對有效地解決了協調者單點故障的問題。
可是性能問題和不一致問題仍然沒有根本解決。下面咱們仍是一塊兒看下三階段流程的是什麼樣的?
第一階段:CanCommit階段
這個階段相似於2PC中的第二個階段中的Ready階段,是一種事務詢問操做,事務的協調者向全部參與者詢問「大家是否能夠完成本次事務?」,若是參與者節點認爲自身能夠完成事務就返回「YES」,不然「NO」。而在實際的場景中參與者節點會對自身邏輯進行事務嘗試,其實說白了就是檢查下自身狀態的健康性,看有沒有能力進行事務操做。
第二階段:PreCommit階段
在階段一中,若是全部的參與者都返回Yes的話,那麼就會進入PreCommit階段進行事務預提交。此時分佈式事務協調者會向全部的參與者節點發送PreCommit請求,參與者收到後開始執行事務操做,並將Undo和Redo信息記錄到事務日誌中。參與者執行完事務操做後(此時屬於未提交事務的狀態),就會向協調者反饋「Ack」表示我已經準備好提交了,並等待協調者的下一步指令。
不然,若是階段一中有任何一個參與者節點返回的結果是No響應,或者協調者在等待參與者節點反饋的過程當中超時(2PC中只有協調者能夠超時,參與者沒有超時機制)。整個分佈式事務就會中斷,協調者就會向全部的參與者發送「abort」請求。
第三階段:DoCommit階段
在階段二中若是全部的參與者節點均可以進行PreCommit提交,那麼協調者就會從「預提交狀態」-》「提交狀態」。而後向全部的參與者節點發送"doCommit"請求,參與者節點在收到提交請求後就會各自執行事務提交操做,並向協調者節點反饋「Ack」消息,協調者收到全部參與者的Ack消息後完成事務。
相反,若是有一個參與者節點未完成PreCommit的反饋或者反饋超時,那麼協調者都會向全部的參與者節點發送abort請求,從而中斷事務。
看到這裏,你是否是會疑惑"3PC相對於2PC而言到底優化了什麼地方呢?"
相比較2PC而言,3PC對於協調者(Coordinator)和參與者(Partcipant)都設置了超時時間,而2PC只有協調者才擁有超時機制。這解決了一個什麼問題呢?這個優化點,主要是避免了參與者在長時間沒法與協調者節點通信(協調者掛掉了)的狀況下,沒法釋放資源的問題,由於參與者自身擁有超時機制會在超時後,自動進行本地commit從而進行釋放資源。而這種機制也側面下降了整個事務的阻塞時間和範圍。
另外,經過CanCommit、PreCommit、DoCommit三個階段的設計,相較於2PC而言,多設置了一個緩衝階段保證了在最後提交階段以前各參與節點的狀態是一致的。
以上就是3PC相對於2PC的一個提升(相對緩解了2PC中的前兩個問題),可是3PC依然沒有徹底解決數據不一致的問題。
補償事務(TCC)
提及分佈式事務的概念,很多人都會搞混淆,彷佛好像分佈式事務就是TCC。實際上TCC與2PC、3PC同樣,只是分佈式事務的一種實現方案而已。
TCC(Try-Confirm-Cancel)又稱補償事務。其核心思想是:"針對每一個操做都要註冊一個與其對應的確認和補償(撤銷操做)"。它分爲三個操做:
Try階段:主要是對業務系統作檢測及資源預留。
Confirm階段:確認執行業務操做。
Cancel階段:取消執行業務操做。
TCC事務的處理流程與2PC兩階段提交相似,不過2PC一般都是在跨庫的DB層面,而TCC本質上就是一個應用層面的2PC,須要經過業務邏輯來實現。這種分佈式事務的實現方式的優點在於,可讓應用本身定義數據庫操做的粒度,使得下降鎖衝突、提升吞吐量成爲可能。
而不足之處則在於對應用的侵入性很是強,業務邏輯的每一個分支都須要實現try、confirm、cancel三個操做。此外,其實現難度也比較大,須要按照網絡狀態、系統故障等不一樣的失敗緣由實現不一樣的回滾策略。爲了知足一致性的要求,confirm和cancel接口還必須實現冪等。
TCC的具體原理圖如👇:
消息隊列MQ事務
在前面介紹2PC、3PC的時候咱們說沒有根本解決性能問題,而若是經過MQ的事務消息來進行異步解耦,並實現系統的數據的最終一致性的話會不會好不少呢?實際上這就是咱們下一篇文章要繼續講述的《分佈式事務之如何基於RocketMQ的事務消息特性實現分佈式系統的最終一致性?》。敬請期待!