分佈式事務簡談

    前幾天線上因爲一些緣故,出現了分佈式事務的問題。現在問題基本解決了,也應該來回顧總結一下了。


事務

    在聊分佈式事務之前,先看看事務是什麼。

    關於這個問題,Stack Overflow上有一個高票答案,是這麼說的:


“A transaction is a unit of work that you want to treat as "a whole." It has to either happen in full or not at all.

“A classical example is transferring money from one bank account to another. To do that you have first to withdraw the amount from the source account, and then deposit it to the destination account. The operation has to succeed in full. If you stop halfway, the money will be lost, and that is Very Bad.”


    簡單翻譯一下,他是這麼說的:

    “事務是你期望當成‘整體’來處理的一個工作單元。一個事務要麼全部完成、要麼全不完成。事務的一個典型例子是從銀行賬戶A向銀行賬戶B進行轉賬。要完成這筆轉賬,首先要從賬戶A中支取一筆金額,然後將它存入賬戶B。這兩個操作必須都成功了(轉賬纔算成功)。如果半途而止,這筆錢就丟失了——這是非常糟糕的結果。(後略)”

    其中的核心就是:事務就是必須“All or Nothing”的一個或一組操作(通常是一組)。如果不能保證這一點,這些操作所處理的業務或者數據就會出現很糟糕的問題。例如前面所說銀行轉賬:如果從賬戶A中支取金額的操作成功了,但是向賬戶B中存入金額的操作失敗了,那麼這筆錢就憑空消失了。無論對賬戶A、賬戶B、還是對銀行來說,這都是無法接受的。


    “All or Nothing”,這就是事務的核心屬性之一:原子性。一般我們會說,事務有四個重要屬性:原子性、一致性、隔離性和持久性,也就是聞名遐邇的事務的ACID屬性:

  • 原子性(Atomic)

    事務中的操作要麼全執行、要麼全不執行,永遠不會出現“執行一半”這種問題。有些文章中提到原子性是“變更瞬間完成”,但這不是重點。事務有可能因爲一些原因而執行很長時間,例如在發生鎖競爭時,數據庫事務可能要等待很久才能完成。


  • 一致性(Consistent)

    一致性是原子性所產生的結果:系統必須從一個正確的狀態遷移到另一個正確的狀態。所謂“正確的”狀態,也就是一個事務中的所有操作全部成功、或者全不成功的狀態。如果出現了部分成功——例如只從賬戶A轉出、沒有向賬戶B轉入——那麼系統實際上就處於一個“中間狀態”,也就出現了不一致問題。


  • 隔離性(Isolated)

    隔離性是對一致性的一個補充。原子性強調的是事務執行完畢後的一致性——全部,或者全不。隔離性強調的是事務執行過程中的一致性——即使事務還沒有執行完成,系統也必須保持一致性,不能出現“中間狀態”。


  • 持久性(Durable)

    持久性說的是事務的結果必須是永久性的。這是對一致性的又一補充:即使系統崩潰,系統也必須處於一個一致狀態之下。但是並不是所有事務都嚴格支持這一點——如果Redis或者MQ沒有開啓持久化支持,那麼系統一旦崩潰,此前提交的事務就全部丟失了。不過,儘管會全部丟失,系統也仍然處在一致性狀態下——不過是一個沒有提交過任何事務的一致性狀態。

    持久性這個屬性暗示了事務一定更新系統數據或狀態。如果一個業務只需要從系統中讀取、而完全不需要寫入任何數據,那麼這個業務就沒有必要開啓事務。

640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1


    ACID這四個屬性,最核心的是原子性,但是我們最關注的是一致性,因爲一致性是最容易度量的一個屬性。


    在技術層面上,有數據庫事務、MQ事務、Redis事務等各種事務。但是在業務層面上,一個業務事務可能會包含多個技術事務——例如在轉賬之前藉助redis來爲兩個賬戶加鎖,完成轉賬操作之後,向MQ中發送一條消息以通知短信系統,並操作redis以釋放鎖、並更新兩個賬戶的緩存等。

    ps,嚴格來說,redis事務不是指“數據庫事務完成後操作緩存”,不過這裏簡化一下描述。

    數據庫操作是系統中最常見的一種操作。數據庫被應用最廣、受關注最高的一種事務,也是對ACID研究最深入、支持最到位的一種事務。也因爲這些原因,本地數據庫事務成爲了業務事務的核心,其它事務都要與數據庫事務的結果保持一致。例如,如果數據庫事務成功提交了,那麼MQ消息必須保證發送成功、緩存數據也必須保證正確更新;而數據庫事務一旦回滾,則MQ消息一定不能發送出去,緩存數據也不能被更新。

    MQ事務一般是指消息收/髮結果與本地數據庫事務保持一致。技術上來講,MQ事務也包含了同一個connection中收/發的消息一定全部成功或全部失敗的語義。例如,如果系統從MQ接收到一條消息、然後又用同一個connection向同一個MQ發送了一條消息,那麼這兩條消息要麼都處理成功、要麼都處理失敗。可以參見《Jms的MessageListener中的Jms事務 - 技術部博客 - Confluence.note》。

    Redis事務除了與本地數據庫事務保持一致之外,還通過MULTI 、 EXEC 、 DISCARD 和 WATCH 等命令來保證多條命令全部或全不執行。其中,MULTI相當於數據庫事務中的BEGIN,EXEC相當於COMMIT,DISCARD相當於ROLLBACK,WATCH相對複雜一些:它是一種樂觀鎖的實現機制,有點類似MySQL中的行鎖。不過,Redis事務雖然能保證原子性和隔離性,但是對一致性和持久性的支持不太好。


分佈式事務

    其實,在介紹前面幾種事務時,我們就已經聊到了分佈式事務了,也就是業務系統與MQ、Redis等系統之間的事務。MQ、Redis等系統雖然強大,但是就操作類型、數據關係而言,卻未必比我們的業務系統複雜。因而,在分佈式事務的控制上,業務系統其實還更麻煩些。

    如前所述,在事務的ACID屬性中,最容易度量、也最受人關注的就是一致性。由於分佈式系統的事務一致性不像單體系統那麼簡單,而是按照達成一致性狀態的時間,被分爲了三種:強一致性、弱一致性和最終一致性。


強一致性

    強一致性是指分佈式系統的一致性像單體系統一樣,在事務完成之後能夠立即達成一致。當然由於網絡延遲等原因,這裏的立即還是會比單體系統慢那麼一點。但是對整個系統和事務而言,這一點延遲可以忽略。分佈式系統要達成強一制性會非常困難,而且付出的成本也太高。所以現在很少有分佈式系統會追求強一致性。如果業務事務就要求強一致性,那我們就應當認真的考慮一下分佈式第一法則的建議:能不分佈就不分佈。

    保證分佈式系統強一致性的方法有很多。

    早期的方法是藉助XA分佈式事務管理協議,藉助事務管理器和本地資源管理器,直接在數據庫層面上管理分佈式事務。XA事務的機制很好理解,它有兩階段和三階段兩種協議。就兩階段而言,一個XA事務分爲預備和提交兩個階段。在預備階段下,各系統中的本地資源管理器檢查自己的數據庫是否可以執行,如果發現不能執行——例如不能得到必要的鎖等情況,則返回失敗,此時整個XA事務全部回滾;如果可以執行,則返回就緒,並準備進入第二階段。在所有系統全部準備就緒之後,事務管理器發起提交操作,各本地資源管理器進入第二階段。由於在預備階段已經做過檢查,提交階段的操作一定會成功,操作完成後,XA事務就提交完成了。三階段協議主要是引入了預提交和超時等機制,在一定程度上提高了XA事務的性能和可靠性。

    顯然,XA事務的思路是簡單地把單體系統的事務擴展到分佈式系統中。這種方式確實能夠很好地保證分佈式事務的ACID,並且只要本地事務提交或回滾,整個業務事務可以很快達到一致性狀態。因而,XA事務能夠很好地保證強一致性。而且,由於Oracle、MySQL和DB2等主流數據都實現了XA事務的接口,在業務系統中使用XA事務所需要的工作量也不大,應用成本較低。    

    但是,XA事務有一個致命的缺點:性能很差。分佈式系統中的一個業務操作相比本地事務來說,耗時常常要高出幾倍、甚至幾十倍。而XA事務卻要求在數據庫從預備階段開始鎖定相關數據、甚至長時間佔用數據庫鏈接。這就極大地降低了數據庫操作的性能,使得本來就容易形成瓶頸的數據庫更加不堪重負。此外,MySQL雖然支持XA事務,但是支持得並不算好,在主從數據庫切換時可能出錯。這都導致了XA事務在實踐中應用得並不算廣泛。


    XA事務的思路並沒有錯,只是它在數據庫層面來處理預備階段的做法不太可取。TCC事務機制延續了同樣的思路、但是把預備階段放到了應用層來處理。

    TCC是指Try-Confirm-Cancel,其中的Try就類似XA事務中的預備階段。不過,在這一階段,由應用系統、而非數據庫來檢查事務是否可以成功提交。例如,一些賬戶、或賬務系統會通過分佈式鎖來鎖定賬戶或者鎖定一定的餘額,以供後續操作。如果鎖定成功,則分佈式事務繼續執行。此後,當事務成功提交時,調用try接口對應的confirm接口來提交事務;如果事務回滾了,則調用cancel接口來撤銷try階段加的鎖。

640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1


    TCC與XA事務一脈相承,對強一致性的支持也比較好。而且,由於Try接口是在業務層、而非數據庫層進行鎖定,TCC事務機制對數據庫的壓力較小,相對的性能也會更好一些。

    但是,也正因爲TCC機制是在業務層進行事務控制,因此,儘管實際業務只需要Confirm接口,但我們還是要編寫Try和Cancel接口。此外,調用Try、Confirm和Cancel接口時的重試機制等也需要額外的處理。這會帶來非常多的額外工作量。因爲這個緣故,很多時候我們也並不願意應用TCC機制來管理分佈式事務。


弱一致性

    弱一致性指的是分佈式系統承諾達成、但不承諾什麼時候達成一致性狀態。由於保證強一致性確實比較困難,所以,我們會盡量地把分佈式系統中的強一致性轉爲弱一致性。


    如果絕大多數情況下,業務事務都能提交成功的話,TCC事務可以簡化爲CC兩個步驟。即第一次調用時就調用Confirm接口,如果事務回滾再調用Cancel接口。這樣一來工作量就減少了三分之一。如果能保證事務不會因爲業務原因而失敗的話——例如除非網絡超時否則事務一定成功——那麼,甚至連Cancel接口都可以省去,當Confirm失敗時重試即可。這樣一來,工作量就又減少了三分之一。這樣一來,實際需要編碼的就只有Confirm接口和一些重試管理相關代碼了。

    把TCC機制轉化爲Confirm+重試機制時,實際上它就不再保證強一致性、而只能保證弱一致性了。


    與簡單重試類似的機制,是消息表機制,也就是在執行事務時,並不直接調用服務,而是把服務調用的相關信息記錄——如接口地址、參數列表等——記錄在消息表中。如果事務回滾,那就不再調用這些接口;但如果事務成功提交,則自動調用、重試這些接口。這種方式就是eBay提出的BASE事務機制,也叫基本一致性(Basically,Available,Soft state,Eventually consistent四個詞組的首字母,即基本可用+軟狀態+最終一致性 )。

    不過,消息表方式需要我們自己處理消息存儲、超時重試等問題。使用消息中間件,不僅可以保證BASE,而且可以藉助MQ自身的事務消息、持久化、重試等機制,減少我們的工作量、同時增加系統的可靠性。這也是現在系統管理分佈式事務的一種最常見方式。

640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1



    不過,弱一致性並非萬能選項。一般來說,只有不影響本地事務的服務調用可以轉化爲弱一致性事務。如果某個服務調用的返回值影響到了本地事務的處理流程,那麼它就只能按強一致性來處理了。例如下圖中,左邊的服務調用就可以用弱一致性事務來處理;右邊的服務調用由於參與到了本地事務的處理流程中,一般只能按強一致性事務來處理——除非利用React等異步回調機制來處理,不過這樣一來處理邏輯就變得更加複雜了。

640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1


最終一致性

    最終一致性是弱一致性的一種特例,是指系統保證在沒有後續更新的前提下,系統最終返回上一次更新操作的值。對於弱一致性的常見方案,前面已經簡單說過,這裏不再囉嗦。


    從字面上理解,還有一種“防禦式”的一致性方案也可以稱爲“最終一致性”,即用一個定時任務系統,定時檢查各系統中數據是否一致。如果發現了不一致,則可以通過報警、甚至自動重試等方式進行修復。不過這種方式屬於最後一道防線,而且大多數情況下除了報警也做不了什麼,一般也很少用到。



qrcode?scene=10000004&size=102&__biz=MzUzNzk0NjI1NQ==&mid=100000495&idx=1&sn=f0817536e05c534d4a414f2106a98bac&send_time=1563121023

微信掃一掃
關注該公衆號


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章