- 鳳凰架構:構建可靠的大型分布式系統
- 周志明
- 3508字
- 2021-06-24 11:30:55
3.2 全局事務
與本地事務相對的是全局事務(Global Transaction),在一些資料中也將其稱為外部事務(External Transaction),在本節里,全局事務被限定為一種適用于單個服務使用多個數據源場景的事務解決方案。請注意,理論上真正的全局事務并沒有“單個服務”的約束,它本來就是DTP(Distributed Transaction Processing,分布式事務處理)模型[1]中的概念,但本節討論的是一種在分布式環境中仍追求強一致性的事務處理方案,對于多節點而且互相調用彼此服務的場合(典型的就是現在的微服務系統)是極不合適的,當前它幾乎只實際應用于單服務多數據源的場合中,為了避免與后續介紹的放棄了ACID的弱一致性事務處理方式混淆,所以這里的全局事務的范圍有所縮減,后續涉及多服務多數據源的事務,筆者將稱其為“分布式事務”。
1991年,為了解決分布式事務的一致性問題,X/Open組織(后來并入了The Open Group)提出了一套名為X/Open XA(XA是eXtended Architecture的縮寫)的處理事務架構,其核心內容是定義了全局的事務管理器(Transaction Manager,用于協調全局事務)和局部的資源管理器(Resource Manager,用于驅動本地事務)之間的通信接口。XA接口是雙向的,能在一個事務管理器和多個資源管理器(Resource Manager)之間形成通信橋梁,通過協調多個數據源的一致動作,實現全局事務的統一提交或者統一回滾,現在我們在Java代碼中還偶爾能看見的XADataSource、XAResource都源于此。
不過,XA并不是Java的技術規范(XA提出那時還沒有Java),而是一套語言無關的通用規范,所以Java中專門定義了JSR 907 Java Transaction API,基于XA模式在Java語言中實現了全局事務處理的標準,這也是我們現在所熟知的JTA。JTA最主要的兩個接口如下。
·事務管理器的接口:javax.transaction.TransactionManager。這套接口用于為Java EE服務器提供容器事務(由容器自動負責事務管理)。JTA還提供了另外一套javax.transaction.UserTransaction接口,用于通過程序代碼手動開啟、提交和回滾事務。
·滿足XA規范的資源定義接口:javax.transaction.xa.XAResource。任何資源(JDBC、JMS等)如果想要支持JTA,只要實現XAResource接口中的方法即可。
JTA原本是Java EE中的技術,一般情況下應該由JBoss、WebSphere、WebLogic這些Java EE容器來提供支持,但現在Bittronix、Atomikos和JBossTM(以前叫Arjuna)都以JAR包的形式實現了JTA的接口,稱為JOTM(Java Open Transaction Manager,Java開源事務管理器),使得我們也能夠在Tomcat、Jetty這樣的Java SE環境下使用JTA。
現在,我們對本章的場景事例做另外一種假設:如果書店的用戶、商家、倉庫分別處于不同的數據庫中,其他條件仍與之前相同,那情況會發生什么變化呢?假如你平時以聲明式事務來編碼,那它與本地事務看起來可能沒什么區別,都是標一個@Transactional注解而已,但如果以編程式事務來實現的話,就能在寫法上看出差異,偽代碼如下所示:
public void buyBook(PaymentBill bill) { userTransaction.begin(); warehouseTransaction.begin(); businessTransaction.begin(); try { userAccountService.pay(bill.getMoney()); warehouseService.deliver(bill.getItems()); businessAccountService.receipt(bill.getMoney()); userTransaction.commit(); warehouseTransaction.commit(); businessTransaction.commit(); } catch(Exception e) { userTransaction.rollback(); warehouseTransaction.rollback(); businessTransaction.rollback(); } }
從代碼可以看出,程序的目的是做三次事務提交,但實際上代碼并不能這樣寫,試想一下,如果在businessTransaction.commit()中出現錯誤,代碼轉到catch塊中執行,此時userTransaction和warehouseTransaction已經完成提交,再去調用rollback()方法已經無濟于事,這將導致一部分數據被提交,另一部分被回滾,整個事務的一致性也就無法保證了。為了解決這個問題,XA將事務提交拆分成兩階段。
·準備階段:又叫作投票階段,在這一階段,協調者詢問事務的所有參與者是否準備好提交,參與者如果已經準備好提交則回復Prepared,否則回復Non-Prepared。這里所說的準備操作跟人類語言中通常理解的準備不同,對于數據庫來說,準備操作是在重做日志中記錄全部事務提交操作所要做的內容,它與本地事務中真正提交的區別只是暫不寫入最后一條Commit Record而已,這意味著在做完數據持久化后并不立即釋放隔離性,即仍繼續持有鎖,維持數據對其他非事務內觀察者的隔離狀態。
·提交階段:又叫作執行階段,協調者如果在上一階段收到所有事務參與者回復的Prepared消息,則先自己在本地持久化事務狀態為Commit,然后向所有參與者發送Commit指令,讓所有參與者立即執行提交操作;否則,任意一個參與者回復了Non-Prepared消息,或任意一個參與者超時未回復時,協調者將在自己完成事務狀態為Abort持久化后,向所有參與者發送Abort指令,讓參與者立即執行回滾操作。對于數據庫來說,這個階段的提交操作應是很輕量的,僅僅是持久化一條Commit Record而已,通常能夠快速完成,只有收到Abort指令時,才需要根據回滾日志清理已提交的數據,這可能是相對重負載的操作。
以上這兩個過程被稱為“兩段式提交”(2 Phase Commit,2PC)協議,而它能夠成功保證一致性還需要一些其他前提條件。
·必須假設網絡在提交階段的短時間內是可靠的,即提交階段不會丟失消息。同時也假設網絡通信在全過程都不會出現誤差,即可以丟失消息,但不會傳遞錯誤的消息,XA的設計目標并不是解決諸如拜占庭將軍一類的問題。在兩段式提交中,投票階段失敗了可以補救(回滾),提交階段失敗了則無法補救(不再改變提交或回滾的結果,只能等崩潰的節點重新恢復),因而此階段耗時應盡可能短,這也是為了盡量控制網絡風險。
·必須假設因為網絡分區、機器崩潰或者其他原因而導致失聯的節點最終能夠恢復,不會永久性地處于失聯狀態。由于在準備階段已經寫入了完整的重做日志,所以當失聯機器一旦恢復,就能夠從日志中找出已準備妥當但并未提交的事務數據,進而向協調者查詢該事務的狀態,確定下一步應該進行提交還是回滾操作。
請注意,上面所說的協調者、參與者通常都是由數據庫自己來扮演的,不需要應用程序介入。協調者一般是在參與者之間選舉產生,而應用程序對于數據庫來說只扮演客戶端的角色。兩段式提交的交互時序示意圖如圖3-2所示。

圖3-2 兩段式提交的交互時序示意圖
兩段式提交原理簡單,并不難實現,但有幾個非常顯著的缺點。
·單點問題:協調者在兩段式提交中具有舉足輕重的作用,協調者等待參與者回復時可以有超時機制,允許參與者宕機,但參與者等待協調者指令時無法做超時處理。一旦宕機的不是其中某個參與者,而是協調者的話,所有參與者都會受到影響。如果協調者一直沒有恢復,沒有正常發送Commit或者Rollback的指令,那所有參與者都必須一直等待。
·性能問題:在兩段式提交過程中,所有參與者相當于被綁定為一個統一調度的整體,期間要經過兩次遠程服務調用,三次數據持久化(準備階段寫重做日志,協調者做狀態持久化,提交階段在日志寫入提交記錄),整個過程將持續到參與者集群中最慢的那一個處理操作結束為止,這決定了兩段式提交的性能通常都較差。
·一致性風險:前面已經提到,兩段式提交的成立是有前提條件的,當網絡穩定性和宕機恢復能力的假設不成立時,仍可能出現一致性問題。宕機恢復能力這一點不必多談,1985年Fischer、Lynch、Paterson提出了“FLP不可能原理”,證明了如果宕機最后不能恢復,那就不存在任何一種分布式協議可以正確地達成一致性結果。該原理在分布式中是與CAP不可兼得原理齊名的理論。而網絡穩定性帶來的一致性風險是指:盡管提交階段時間很短,但這仍是一段明確存在的危險期,如果協調者在發出準備指令后,根據收到各個參與者發回的信息確定事務狀態是可以提交的,協調者會先持久化事務狀態,并提交自己的事務,如果這時候網絡忽然斷開,無法再通過網絡向所有參與者發出Commit指令的話,就會導致部分數據(協調者的)已提交,但部分數據(參與者的)未提交,且沒有辦法回滾,產生數據不一致的問題。
為了緩解兩段式提交協議的一部分缺陷,具體地說是協調者的單點問題和準備階段的性能問題,后續又發展出了“三段式提交”(3 Phase Commit,3PC)協議。三段式提交把原本的兩段式提交的準備階段再細分為兩個階段,分別稱為CanCommit、PreCommit,把提交階段改稱為DoCommit階段。其中,新增的CanCommit是一個詢問階段,即協調者讓每個參與的數據庫根據自身狀態,評估該事務是否有可能順利完成。將準備階段一分為二的理由是這個階段是重負載的操作,一旦協調者發出開始準備的消息,每個參與者都將馬上開始寫重做日志,它們所涉及的數據資源即被鎖住,如果此時某一個參與者宣告無法完成提交,相當于大家都做了一輪無用功。所以,增加一輪詢問階段,如果都得到了正面的響應,那事務能夠成功提交的把握就比較大了,這也意味著因某個參與者提交時發生崩潰而導致大家全部回滾的風險相對變小。因此,在事務需要回滾的場景中,三段式提交的性能通常要比兩段式提交好很多,但在事務能夠正常提交的場景中,兩者的性能都很差,甚至三段式因為多了一次詢問,還要稍微更差一些。
同樣也是由于事務失敗回滾概率變小,在三段式提交中,如果在PreCommit階段之后發生了協調者宕機,即參與者沒有等到DoCommit的消息的話,默認的操作策略將是提交事務而不是回滾事務或者持續等待,這就相當于避免了協調者單點問題的風險。三段式提交的操作時序如圖3-3所示。
從以上過程可以看出,三段式提交對單點問題和回滾時的性能問題有所改善,但是對一致性風險問題并未有任何改進,甚至是略有增加的。譬如,進入PreCommit階段之后,協調者發出的指令不是Ack而是Abort,而此時因網絡問題,有部分參與者直至超時都未能收到協調者的Abort指令的話,這些參與者將會錯誤地提交事務,這就產生了不同參與者之間數據不一致的問題。

圖3-3 三段式提交的操作時序
[1] DTP模型:https://en.wikipedia.org/wiki/Distributed_transaction。
- Windows Server 2019 Cookbook
- Google系統架構解密:構建安全可靠的系統
- Installing and Configuring Windows 10:70-698 Exam Guide
- RESS Essentials
- Mastering Reactive JavaScript
- 一學就會:Windows Vista應用完全自學手冊
- 計算機系統的自主設計
- VMware Horizon View Essentials
- Troubleshooting Docker
- Linux軟件管理平臺設計與實現
- Linux系統安全:縱深防御、安全掃描與入侵檢測
- 基于Arduino的嵌入式系統入門與實踐
- Linux指令從初學到精通
- 分布式實時處理系統:原理、架構與實現
- Mastering Spring Cloud