常用的分布式事務解決方案

眾所周知,數據庫能實現本地事務,也就是在同一個數據庫中,你可以允許一組操作要么全都正確執行,要么全都不執行。這里特別強調了本地事務,也就是目前的數據庫只能支持同一個數據庫中的事務。但現在的系統往往采用微服務架構,業務系統擁有獨立的數據庫,因此就出現了跨多個數據庫的事務需求,這種事務即為“分布式事務”。那么在目前數據庫不支持跨庫事務的情況下,我們應該如何實現分布式事務呢?本文首先會為大家梳理分布式事務的基本概念和理論基礎,然后介紹幾種目前常用的分布式事務解決方案。廢話不多說,那就開始吧~

什么是事務?

事務由一組操作構成,我們希望這組操作能夠全部正確執行,如果這一組操作中的任意一個步驟發生錯誤,那么就需要回滾之前已經完成的操作。也就是同一個事務中的所有操作,要么全都正確執行,要么全都不要執行。

事務的四大特性 ACID

說到事務,就不得不提一下事務著名的四大特性。

  • 原子性 原子性要求,事務是一個不可分割的執行單元,事務中的所有操作要么全都執行,要么全都不執行。
  • 一致性 一致性要求,事務在開始前和結束后,數據庫的完整性約束沒有被破壞。
  • 隔離性 事務的執行是相互獨立的,它們不會相互干擾,一個事務不會看到另一個正在運行過程中的事務的數據。
  • 持久性 持久性要求,一個事務完成之后,事務的執行結果必須是持久化保存的。即使數據庫發生崩潰,在數據庫恢復后事務提交的結果仍然不會丟失。

注意:事務只能保證數據庫的高可靠性,即數據庫本身發生問題后,事務提交后的數據仍然能恢復;而如果不是數據庫本身的故障,如硬盤損壞了,那么事務提交的數據可能就丟失了。這屬于『高可用性』的范疇。因此,事務只能保證數據庫的『高可靠性』,而『高可用性』需要整個系統共同配合實現。

事務的隔離級別

這里擴展一下,對事務的隔離性做一個詳細的解釋。

在事務的四大特性ACID中,要求的隔離性是一種嚴格意義上的隔離,也就是多個事務是串行執行的,彼此之間不會受到任何干擾。這確實能夠完全保證數據的安全性,但在實際業務系統中,這種方式性能不高。因此,數據庫定義了四種隔離級別,隔離級別和數據庫的性能是呈反比的,隔離級別越低,數據庫性能越高,而隔離級別越高,數據庫性能越差。

事務并發執行會出現的問題

我們先來看一下在不同的隔離級別下,數據庫可能會出現的問題:

  1. 更新丟失 當有兩個并發執行的事務,更新同一行數據,那么有可能一個事務會把另一個事務的更新覆蓋掉。 當數據庫沒有加任何鎖操作的情況下會發生。
  2. 臟讀 一個事務讀到另一個尚未提交的事務中的數據。 該數據可能會被回滾從而失效。 如果第一個事務拿著失效的數據去處理那就發生錯誤了。
  3. 不可重復讀 不可重復度的含義:一個事務對同一行數據讀了兩次,卻得到了不同的結果。它具體分為如下兩種情況:
    • 虛讀:在事務1兩次讀取同一記錄的過程中,事務2對該記錄進行了修改,從而事務1第二次讀到了不一樣的記錄。
    • 幻讀:事務1在兩次查詢的過程中,事務2對該表進行了插入、刪除操作,從而事務1第二次查詢的結果發生了變化。

不可重復讀 與 臟讀 的區別? 臟讀讀到的是尚未提交的數據,而不可重復讀讀到的是已經提交的數據,只不過在兩次讀的過程中數據被另一個事務改過了。

數據庫的四種隔離級別

數據庫一共有如下四種隔離級別:

  1. Read uncommitted 讀未提交 在該級別下,一個事務對一行數據修改的過程中,不允許另一個事務對該行數據進行修改,但允許另一個事務對該行數據讀。 因此本級別下,不會出現更新丟失,但會出現臟讀、不可重復讀。
  2. Read committed 讀提交 在該級別下,未提交的寫事務不允許其他事務訪問該行,因此不會出現臟讀;但是讀取數據的事務允許其他事務的訪問該行數據,因此會出現不可重復讀的情況。
  3. Repeatable read 重復讀 在該級別下,讀事務禁止寫事務,但允許讀事務,因此不會出現同一事務兩次讀到不同的數據的情況(不可重復讀),且寫事務禁止其他一切事務。
  4. Serializable 序列化 該級別要求所有事務都必須串行執行,因此能避免一切因并發引起的問題,但效率很低。

隔離級別越高,越能保證數據的完整性和一致性,但是對并發性能的影響也越大。對于多數應用程序,可以優先考慮把數據庫系統的隔離級別設為Read Committed。它能夠避免臟讀取,而且具有較好的并發性能。盡管它會導致不可重復讀、幻讀和第二類丟失更新這些并發問題,在可能出現這類問題的個別場合,可以由應用程序采用悲觀鎖或樂觀鎖來控制。


什么是分布式事務?

到此為止,所介紹的事務都是基于單數據庫的本地事務,目前的數據庫僅支持單庫事務,并不支持跨庫事務。而隨著微服務架構的普及,一個大型業務系統往往由若干個子系統構成,這些子系統又擁有各自獨立的數據庫。往往一個業務流程需要由多個子系統共同完成,而且這些操作可能需要在一個事務中完成。在微服務系統中,這些業務場景是普遍存在的。此時,我們就需要在數據庫之上通過某種手段,實現支持跨數據庫的事務支持,這也就是大家常說的“分布式事務”。

這里舉一個分布式事務的典型例子——用戶下單過程。 當我們的系統采用了微服務架構后,一個電商系統往往被拆分成如下幾個子系統:商品系統、訂單系統、支付系統、積分系統等。整個下單的過程如下:

  1. 用戶通過商品系統瀏覽商品,他看中了某一項商品,便點擊下單
  2. 此時訂單系統會生成一條訂單
  3. 訂單創建成功后,支付系統提供支付功能
  4. 當支付完成后,由積分系統為該用戶增加積分

上述步驟2、3、4需要在一個事務中完成。對于傳統單體應用而言,實現事務非常簡單,只需將這三個步驟放在一個方法A中,再用Spring的@Transactional注解標識該方法即可。Spring通過數據庫的事務支持,保證這些步驟要么全都執行完成,要么全都不執行。但在這個微服務架構中,這三個步驟涉及三個系統,涉及三個數據庫,此時我們必須在數據庫和應用系統之間,通過某項黑科技,實現分布式事務的支持。

CAP理論

CAP理論說的是:在一個分布式系統中,最多只能滿足C、A、P中的兩個需求。

CAP的含義:

  • C:Consistency 一致性 同一數據的多個副本是否實時相同。
  • A:Availability 可用性 可用性:一定時間內 & 系統返回一個明確的結果 則稱為該系統可用。
  • P:Partition tolerance 分區容錯性 將同一服務分布在多個系統中,從而保證某一個系統宕機,仍然有其他系統提供相同的服務。

CAP理論告訴我們,在分布式系統中,C、A、P三個條件中我們最多只能選擇兩個。那么問題來了,究竟選擇哪兩個條件較為合適呢?

對于一個業務系統來說,可用性和分區容錯性是必須要滿足的兩個條件,并且這兩者是相輔相成的。業務系統之所以使用分布式系統,主要原因有兩個:

  • 提升整體性能 當業務量猛增,單個服務器已經無法滿足我們的業務需求的時候,就需要使用分布式系統,使用多個節點提供相同的功能,從而整體上提升系統的性能,這就是使用分布式系統的第一個原因。
  • 實現分區容錯性 單一節點 或 多個節點處于相同的網絡環境下,那么會存在一定的風險,萬一該機房斷電、該地區發生自然災害,那么業務系統就全面癱瘓了。為了防止這一問題,采用分布式系統,將多個子系統分布在不同的地域、不同的機房中,從而保證系統高可用性。

這說明分區容錯性是分布式系統的根本,如果分區容錯性不能滿足,那使用分布式系統將失去意義。

此外,可用性對業務系統也尤為重要。在大談用戶體驗的今天,如果業務系統時常出現“系統異常”、響應時間過長等情況,這使得用戶對系統的好感度大打折扣,在互聯網行業競爭激烈的今天,相同領域的競爭者不甚枚舉,系統的間歇性不可用會立馬導致用戶流向競爭對手。因此,我們只能通過犧牲一致性來換取系統的可用性分區容錯性。這也就是下面要介紹的BASE理論。

BASE理論

CAP理論告訴我們一個悲慘但不得不接受的事實——我們只能在C、A、P中選擇兩個條件。而對于業務系統而言,我們往往選擇犧牲一致性來換取系統的可用性和分區容錯性。不過這里要指出的是,所謂的“犧牲一致性”并不是完全放棄數據一致性,而是犧牲強一致性換取弱一致性。下面來介紹下BASE理論。

  • BA:Basic Available 基本可用
    • 整個系統在某些不可抗力的情況下,仍然能夠保證“可用性”,即一定時間內仍然能夠返回一個明確的結果。只不過“基本可用”和“高可用”的區別是:
      • “一定時間”可以適當延長 當舉行大促時,響應時間可以適當延長
      • 給部分用戶返回一個降級頁面 給部分用戶直接返回一個降級頁面,從而緩解服務器壓力。但要注意,返回降級頁面仍然是返回明確結果。
  • S:Soft State:柔性狀態 同一數據的不同副本的狀態,可以不需要實時一致。
  • E:Eventual Consisstency:最終一致性 同一數據的不同副本的狀態,可以不需要實時一致,但一定要保證經過一定時間后仍然是一致的。

酸堿平衡

ACID能夠保證事務的強一致性,即數據是實時一致的。這在本地事務中是沒有問題的,在分布式事務中,強一致性會極大影響分布式系統的性能,因此分布式系統中遵循BASE理論即可。但分布式系統的不同業務場景對一致性的要求也不同。如交易場景下,就要求強一致性,此時就需要遵循ACID理論,而在注冊成功后發送短信驗證碼等場景下,并不需要實時一致,因此遵循BASE理論即可。因此要根據具體業務場景,在ACID和BASE之間尋求平衡。

分布式事務協議

下面介紹幾種實現分布式事務的協議。

兩階段提交協議 2PC

分布式系統的一個難點是如何保證架構下多個節點在進行事務性操作的時候保持一致性。為實現這個目的,二階段提交算法的成立基于以下假設:

  • 該分布式系統中,存在一個節點作為協調者(Coordinator),其他節點作為參與者(Cohorts)。且節點之間可以進行網絡通信。
  • 所有節點都采用預寫式日志,且日志被寫入后即被保持在可靠的存儲設備上,即使節點損壞不會導致日志數據的消失。
  • 所有節點不會永久性損壞,即使損壞后仍然可以恢復。

1. 第一階段(投票階段):

  1. 協調者節點向所有參與者節點詢問是否可以執行提交操作(vote),并開始等待各參與者節點的響應。
  2. 參與者節點執行詢問發起為止的所有事務操作,并將Undo信息和Redo信息寫入日志。(注意:若成功這里其實每個參與者已經執行了事務操作)
  3. 各參與者節點響應協調者節點發起的詢問。如果參與者節點的事務操作實際執行成功,則它返回一個"同意"消息;如果參與者節點的事務操作實際執行失敗,則它返回一個"中止"消息。

2. 第二階段(提交執行階段):

當協調者節點從所有參與者節點獲得的相應消息都為"同意"時:

  1. 協調者節點向所有參與者節點發出"正式提交(commit)"的請求。
  2. 參與者節點正式完成操作,并釋放在整個事務期間內占用的資源。
  3. 參與者節點向協調者節點發送"完成"消息。
  4. 協調者節點受到所有參與者節點反饋的"完成"消息后,完成事務。

如果任一參與者節點在第一階段返回的響應消息為"中止",或者 協調者節點在第一階段的詢問超時之前無法獲取所有參與者節點的響應消息時:

  1. 協調者節點向所有參與者節點發出"回滾操作(rollback)"的請求。
  2. 參與者節點利用之前寫入的Undo信息執行回滾,并釋放在整個事務期間內占用的資源。
  3. 參與者節點向協調者節點發送"回滾完成"消息。
  4. 協調者節點受到所有參與者節點反饋的"回滾完成"消息后,取消事務。

不管最后結果如何,第二階段都會結束當前事務。

二階段提交看起來確實能夠提供原子性的操作,但是不幸的事,二階段提交還是有幾個缺點的:

  1. 執行過程中,所有參與節點都是事務阻塞型的。當參與者占有公共資源時,其他第三方節點訪問公共資源不得不處于阻塞狀態。
  2. 參與者發生故障。協調者需要給每個參與者額外指定超時機制,超時后整個事務失敗。(沒有多少容錯機制)
  3. 協調者發生故障。參與者會一直阻塞下去。需要額外的備機進行容錯。(這個可以依賴后面要講的Paxos協議實現HA)
  4. 二階段無法解決的問題:協調者再發出commit消息之后宕機,而唯一接收到這條消息的參與者同時也宕機了。那么即使協調者通過選舉協議產生了新的協調者,這條事務的狀態也是不確定的,沒人知道事務是否被已經提交。

為此,Dale Skeen和Michael Stonebraker在“A Formal Model of Crash Recovery in a Distributed System”中提出了三階段提交協議(3PC)。

三階段提交協議 3PC

與兩階段提交不同的是,三階段提交有兩個改動點。

  • 引入超時機制。同時在協調者和參與者中都引入超時機制。
  • 在第一階段和第二階段中插入一個準備階段。保證了在最后提交階段之前各參與節點的狀態是一致的。

也就是說,除了引入超時機制之外,3PC把2PC的準備階段再次一分為二,這樣三階段提交就有CanCommit、PreCommit、DoCommit三個階段。

1. CanCommit階段

3PC的CanCommit階段其實和2PC的準備階段很像。協調者向參與者發送commit請求,參與者如果可以提交就返回Yes響應,否則返回No響應。

  1. 事務詢問 協調者向參與者發送CanCommit請求。詢問是否可以執行事務提交操作。然后開始等待參與者的響應。
  2. 響應反饋 參與者接到CanCommit請求之后,正常情況下,如果其自身認為可以順利執行事務,則返回Yes響應,并進入預備狀態。否則反饋No

2. PreCommit階段

協調者根據參與者的反應情況來決定是否可以記性事務的PreCommit操作。根據響應情況,有以下兩種可能。 假如協調者從所有的參與者獲得的反饋都是Yes響應,那么就會執行事務的預執行。

  1. 發送預提交請求 協調者向參與者發送PreCommit請求,并進入Prepared階段。
  2. 事務預提交 參與者接收到PreCommit請求后,會執行事務操作,并將undo和redo信息記錄到事務日志中。
  3. 響應反饋 如果參與者成功的執行了事務操作,則返回ACK響應,同時開始等待最終指令。

假如有任何一個參與者向協調者發送了No響應,或者等待超時之后,協調者都沒有接到參與者的響應,那么就執行事務的中斷。

  1. 發送中斷請求 協調者向所有參與者發送abort請求。
  2. 中斷事務 參與者收到來自協調者的abort請求之后(或超時之后,仍未收到協調者的請求),執行事務的中斷。

3. doCommit階段 該階段進行真正的事務提交,也可以分為以下兩種情況。

該階段進行真正的事務提交,也可以分為以下兩種情況。

3.1 執行提交

  1. 發送提交請求 協調接收到參與者發送的ACK響應,那么他將從預提交狀態進入到提交狀態。并向所有參與者發送doCommit請求。
  2. 事務提交 參與者接收到doCommit請求之后,執行正式的事務提交。并在完成事務提交之后釋放所有事務資源。
  3. 響應反饋 事務提交完之后,向協調者發送Ack響應。
  4. 完成事務 協調者接收到所有參與者的ack響應之后,完成事務。

3.2 中斷事務 協調者沒有接收到參與者發送的ACK響應(可能是接受者發送的不是ACK響應,也可能響應超時),那么就會執行中斷事務。

  1. 發送中斷請求 協調者向所有參與者發送abort請求
  2. 事務回滾 參與者接收到abort請求之后,利用其在階段二記錄的undo信息來執行事務的回滾操作,并在完成回滾之后釋放所有的事務資源。
  3. 反饋結果 參與者完成事務回滾之后,向協調者發送ACK消息
  4. 中斷事務 協調者接收到參與者反饋的ACK消息之后,執行事務的中斷。

分布式事務的解決方案

分布式事務的解決方案有如下幾種:

  • 全局消息
  • 基于可靠消息服務的分布式事務
  • TCC
  • 最大努力通知

方案1:全局事務(DTP模型)

全局事務基于DTP模型實現。DTP是由X/Open組織提出的一種分布式事務模型——X/Open Distributed Transaction Processing Reference Model。它規定了要實現分布式事務,需要三種角色:

  • AP:Application 應用系統 它就是我們開發的業務系統,在我們開發的過程中,可以使用資源管理器提供的事務接口來實現分布式事務。
  • TM:Transaction Manager 事務管理器
    • 分布式事務的實現由事務管理器來完成,它會提供分布式事務的操作接口供我們的業務系統調用。這些接口稱為TX接口。
    • 事務管理器還管理著所有的資源管理器,通過它們提供的XA接口來同一調度這些資源管理器,以實現分布式事務。
    • DTP只是一套實現分布式事務的規范,并沒有定義具體如何實現分布式事務,TM可以采用2PC、3PC、Paxos等協議實現分布式事務。
  • RM:Resource Manager 資源管理器
    • 能夠提供數據服務的對象都可以是資源管理器,比如:數據庫、消息中間件、緩存等。大部分場景下,數據庫即為分布式事務中的資源管理器。
    • 資源管理器能夠提供單數據庫的事務能力,它們通過XA接口,將本數據庫的提交、回滾等能力提供給事務管理器調用,以幫助事務管理器實現分布式的事務管理。
    • XA是DTP模型定義的接口,用于向事務管理器提供該資源管理器(該數據庫)的提交、回滾等能力。
    • DTP只是一套實現分布式事務的規范,RM具體的實現是由數據庫廠商來完成的。
  1. 有沒有基于DTP模型的分布式事務中間件?
  1. DTP模型有啥優缺點?

方案2:基于可靠消息服務的分布式事務

這種實現分布式事務的方式需要通過消息中間件來實現。假設有A和B兩個系統,分別可以處理任務A和任務B。此時系統A中存在一個業務流程,需要將任務A和任務B在同一個事務中處理。下面來介紹基于消息中間件來實現這種分布式事務。


image.png
  • 在系統A處理任務A前,首先向消息中間件發送一條消息
  • 消息中間件收到后將該條消息持久化,但并不投遞。此時下游系統B仍然不知道該條消息的存在。
  • 消息中間件持久化成功后,便向系統A返回一個確認應答;
  • 系統A收到確認應答后,則可以開始處理任務A;
  • 任務A處理完成后,向消息中間件發送Commit請求。該請求發送完成后,對系統A而言,該事務的處理過程就結束了,此時它可以處理別的任務了。 但commit消息可能會在傳輸途中丟失,從而消息中間件并不會向系統B投遞這條消息,從而系統就會出現不一致性。這個問題由消息中間件的事務回查機制完成,下文會介紹。
  • 消息中間件收到Commit指令后,便向系統B投遞該消息,從而觸發任務B的執行;
  • 當任務B執行完成后,系統B向消息中間件返回一個確認應答,告訴消息中間件該消息已經成功消費,此時,這個分布式事務完成。

上述過程可以得出如下幾個結論:

  1. 消息中間件扮演者分布式事務協調者的角色。
  2. 系統A完成任務A后,到任務B執行完成之間,會存在一定的時間差。在這個時間差內,整個系統處于數據不一致的狀態,但這短暫的不一致性是可以接受的,因為經過短暫的時間后,系統又可以保持數據一致性,滿足BASE理論。

上述過程中,如果任務A處理失敗,那么需要進入回滾流程,如下圖所示:

image.png
  • 若系統A在處理任務A時失敗,那么就會向消息中間件發送Rollback請求。和發送Commit請求一樣,系統A發完之后便可以認為回滾已經完成,它便可以去做其他的事情。
  • 消息中間件收到回滾請求后,直接將該消息丟棄,而不投遞給系統B,從而不會觸發系統B的任務B。

此時系統又處于一致性狀態,因為任務A和任務B都沒有執行。

上面所介紹的Commit和Rollback都屬于理想情況,但在實際系統中,Commit和Rollback指令都有可能在傳輸途中丟失。那么當出現這種情況的時候,消息中間件是如何保證數據一致性呢?——答案就是超時詢問機制。

image.png

系統A除了實現正常的業務流程外,還需提供一個事務詢問的接口,供消息中間件調用。當消息中間件收到一條事務型消息后便開始計時,如果到了超時時間也沒收到系統A發來的Commit或Rollback指令的話,就會主動調用系統A提供的事務詢問接口詢問該系統目前的狀態。該接口會返回三種結果:

  • 提交 若獲得的狀態是“提交”,則將該消息投遞給系統B。
  • 回滾 若獲得的狀態是“回滾”,則直接將條消息丟棄。
  • 處理中 若獲得的狀態是“處理中”,則繼續等待。

消息中間件的超時詢問機制能夠防止上游系統因在傳輸過程中丟失Commit/Rollback指令而導致的系統不一致情況,而且能降低上游系統的阻塞時間,上游系統只要發出Commit/Rollback指令后便可以處理其他任務,無需等待確認應答。而Commit/Rollback指令丟失的情況通過超時詢問機制來彌補,這樣大大降低上游系統的阻塞時間,提升系統的并發度。

下面來說一說消息投遞過程的可靠性保證。 當上游系統執行完任務并向消息中間件提交了Commit指令后,便可以處理其他任務了,此時它可以認為事務已經完成,接下來消息中間件一定會保證消息被下游系統成功消費掉!那么這是怎么做到的呢?這由消息中間件的投遞流程來保證。

消息中間件向下游系統投遞完消息后便進入阻塞等待狀態,下游系統便立即進行任務的處理,任務處理完成后便向消息中間件返回應答。消息中間件收到確認應答后便認為該事務處理完畢!

如果消息在投遞過程中丟失,或消息的確認應答在返回途中丟失,那么消息中間件在等待確認應答超時之后就會重新投遞,直到下游消費者返回消費成功響應為止。當然,一般消息中間件可以設置消息重試的次數和時間間隔,比如:當第一次投遞失敗后,每隔五分鐘重試一次,一共重試3次。如果重試3次之后仍然投遞失敗,那么這條消息就需要人工干預。

image.png
image.png

有的同學可能要問:消息投遞失敗后為什么不回滾消息,而是不斷嘗試重新投遞?

這就涉及到整套分布式事務系統的實現成本問題。 我們知道,當系統A將向消息中間件發送Commit指令后,它便去做別的事情了。如果此時消息投遞失敗,需要回滾的話,就需要讓系統A事先提供回滾接口,這無疑增加了額外的開發成本,業務系統的復雜度也將提高。對于一個業務系統的設計目標是,在保證性能的前提下,最大限度地降低系統復雜度,從而能夠降低系統的運維成本。

不知大家是否發現,上游系統A向消息中間件提交Commit/Rollback消息采用的是異步方式,也就是當上游系統提交完消息后便可以去做別的事情,接下來提交、回滾就完全交給消息中間件來完成,并且完全信任消息中間件,認為它一定能正確地完成事務的提交或回滾。然而,消息中間件向下游系統投遞消息的過程是同步的。也就是消息中間件將消息投遞給下游系統后,它會阻塞等待,等下游系統成功處理完任務返回確認應答后才取消阻塞等待。為什么這兩者在設計上是不一致的呢?

首先,上游系統和消息中間件之間采用異步通信是為了提高系統并發度。業務系統直接和用戶打交道,用戶體驗尤為重要,因此這種異步通信方式能夠極大程度地降低用戶等待時間。此外,異步通信相對于同步通信而言,沒有了長時間的阻塞等待,因此系統的并發性也大大增加。但異步通信可能會引起Commit/Rollback指令丟失的問題,這就由消息中間件的超時詢問機制來彌補。

那么,消息中間件和下游系統之間為什么要采用同步通信呢?

異步能提升系統性能,但隨之會增加系統復雜度;而同步雖然降低系統并發度,但實現成本較低。因此,在對并發度要求不是很高的情況下,或者服務器資源較為充裕的情況下,我們可以選擇同步來降低系統的復雜度。 我們知道,消息中間件是一個獨立于業務系統的第三方中間件,它不和任何業務系統產生直接的耦合,它也不和用戶產生直接的關聯,它一般部署在獨立的服務器集群上,具有良好的可擴展性,所以不必太過于擔心它的性能,如果處理速度無法滿足我們的要求,可以增加機器來解決。而且,即使消息中間件處理速度有一定的延遲那也是可以接受的,因為前面所介紹的BASE理論就告訴我們了,我們追求的是最終一致性,而非實時一致性,因此消息中間件產生的時延導致事務短暫的不一致是可以接受的。

方案3:最大努力通知(定期校對)

最大努力通知也被稱為定期校對,其實在方案二中已經包含,這里再單獨介紹,主要是為了知識體系的完整性。這種方案也需要消息中間件的參與,其過程如下:

image.png
  • 上游系統在完成任務后,向消息中間件同步地發送一條消息,確保消息中間件成功持久化這條消息,然后上游系統可以去做別的事情了;
  • 消息中間件收到消息后負責將該消息同步投遞給相應的下游系統,并觸發下游系統的任務執行;
  • 當下游系統處理成功后,向消息中間件反饋確認應答,消息中間件便可以將該條消息刪除,從而該事務完成。

上面是一個理想化的過程,但在實際場景中,往往會出現如下幾種意外情況:

  1. 消息中間件向下游系統投遞消息失敗
  2. 上游系統向消息中間件發送消息失敗

對于第一種情況,消息中間件具有重試機制,我們可以在消息中間件中設置消息的重試次數和重試時間間隔,對于網絡不穩定導致的消息投遞失敗的情況,往往重試幾次后消息便可以成功投遞,如果超過了重試的上限仍然投遞失敗,那么消息中間件不再投遞該消息,而是記錄在失敗消息表中,消息中間件需要提供失敗消息的查詢接口,下游系統會定期查詢失敗消息,并將其消費,這就是所謂的“定期校對”。

如果重復投遞和定期校對都不能解決問題,往往是因為下游系統出現了嚴重的錯誤,此時就需要人工干預。

對于第二種情況,需要在上游系統中建立消息重發機制。可以在上游系統建立一張本地消息表,并將 任務處理過程向本地消息表中插入消息 這兩個步驟放在一個本地事務中完成。如果向本地消息表插入消息失敗,那么就會觸發回滾,之前的任務處理結果就會被取消。如果這量步都執行成功,那么該本地事務就完成了。接下來會有一個專門的消息發送者不斷地發送本地消息表中的消息,如果發送失敗它會返回重試。當然,也要給消息發送者設置重試的上限,一般而言,達到重試上限仍然發送失敗,那就意味著消息中間件出現嚴重的問題,此時也只有人工干預才能解決問題。

對于不支持事務型消息的消息中間件,如果要實現分布式事務的話,就可以采用這種方式。它能夠通過重試機制+定期校對實現分布式事務,但相比于第二種方案,它達到數據一致性的周期較長,而且還需要在上游系統中實現消息重試發布機制,以確保消息成功發布給消息中間件,這無疑增加了業務系統的開發成本,使得業務系統不夠純粹,并且這些額外的業務邏輯無疑會占用業務系統的硬件資源,從而影響性能。

因此,盡量選擇支持事務型消息的消息中間件來實現分布式事務,如RocketMQ。

方案4:TCC(兩階段型、補償型)

TCC即為Try Confirm Cancel,它屬于補償型分布式事務。顧名思義,TCC實現分布式事務一共有三個步驟:

  • Try:嘗試待執行的業務
    • 這個過程并未執行業務,只是完成所有業務的一致性檢查,并預留好執行所需的全部資源
  • Confirm:執行業務
    • 這個過程真正開始執行業務,由于Try階段已經完成了一致性檢查,因此本過程直接執行,而不做任何檢查。并且在執行的過程中,會使用到Try階段預留的業務資源。
  • Cancel:取消執行的業務
    • 若業務執行失敗,則進入Cancel階段,它會釋放所有占用的業務資源,并回滾Confirm階段執行的操作。

下面以一個轉賬的例子來解釋下TCC實現分布式事務的過程。

假設用戶A用他的賬戶余額給用戶B發一個100元的紅包,并且余額系統和紅包系統是兩個獨立的系統。

  • Try
    • 創建一條轉賬流水,并將流水的狀態設為交易中
    • 將用戶A的賬戶中扣除100元(預留業務資源)
    • Try成功之后,便進入Confirm階段
    • Try過程發生任何異常,均進入Cancel階段
  • Confirm
    • 向B用戶的紅包賬戶中增加100元
    • 將流水的狀態設為交易已完成
    • Confirm過程發生任何異常,均進入Cancel階段
    • Confirm過程執行成功,則該事務結束
  • Cancel
    • 將用戶A的賬戶增加100元
    • 將流水的狀態設為交易失敗

在傳統事務機制中,業務邏輯的執行和事務的處理,是在不同的階段由不同的部件來完成的:業務邏輯部分訪問資源實現數據存儲,其處理是由業務系統負責;事務處理部分通過協調資源管理器以實現事務管理,其處理由事務管理器來負責。二者沒有太多交互的地方,所以,傳統事務管理器的事務處理邏輯,僅需要著眼于事務完成(commit/rollback)階段,而不必關注業務執行階段。

TCC全局事務必須基于RM本地事務來實現全局事務

TCC服務是由Try/Confirm/Cancel業務構成的, 其Try/Confirm/Cancel業務在執行時,會訪問資源管理器(Resource Manager,下文簡稱RM)來存取數據。這些存取操作,必須要參與RM本地事務,以使其更改的數據要么都commit,要么都rollback。

這一點不難理解,考慮一下如下場景:

image.png

假設圖中的服務B沒有基于RM本地事務(以RDBS為例,可通過設置auto-commit為true來模擬),那么一旦[B:Try]操作中途執行失敗,TCC事務框架后續決定回滾全局事務時,該[B:Cancel]則需要判斷[B:Try]中哪些操作已經寫到DB、哪些操作還沒有寫到DB:假設[B:Try]業務有5個寫庫操作,[B:Cancel]業務則需要逐個判斷這5個操作是否生效,并將生效的操作執行反向操作。

不幸的是,由于[B:Cancel]業務也有n(0<=n<=5)個反向的寫庫操作,此時一旦[B:Cancel]也中途出錯,則后續的[B:Cancel]執行任務更加繁重。因為,相比第一次[B:Cancel]操作,后續的[B:Cancel]操作還需要判斷先前的[B:Cancel]操作的n(0<=n<=5)個寫庫中哪幾個已經執行、哪幾個還沒有執行,這就涉及到了冪等性問題。而對冪等性的保障,又很可能還需要涉及額外的寫庫操作,該寫庫操作又會因為沒有RM本地事務的支持而存在類似問題。。。可想而知,如果不基于RM本地事務,TCC事務框架是無法有效的管理TCC全局事務的。

反之,基于RM本地事務的TCC事務,這種情況則會很容易處理:[B:Try]操作中途執行失敗,TCC事務框架將其參與RM本地事務直接rollback即可。后續TCC事務框架決定回滾全局事務時,在知道“[B:Try]操作涉及的RM本地事務已經rollback”的情況下,根本無需執行[B:Cancel]操作。

換句話說,基于RM本地事務實現TCC事務框架時,一個TCC型服務的cancel業務要么執行,要么不執行,不需要考慮部分執行的情況。

TCC事務框架應該提供Confirm/Cancel服務的冪等性保障

一般認為,服務的冪等性,是指針對同一個服務的多次(n>1)請求和對它的單次(n=1)請求,二者具有相同的副作用。

在TCC事務模型中,Confirm/Cancel業務可能會被重復調用,其原因很多。比如,全局事務在提交/回滾時會調用各TCC服務的Confirm/Cancel業務邏輯。執行這些Confirm/Cancel業務時,可能會出現如網絡中斷的故障而使得全局事務不能完成。因此,故障恢復機制后續仍然會重新提交/回滾這些未完成的全局事務,這樣就會再次調用參與該全局事務的各TCC服務的Confirm/Cancel業務邏輯。

既然Confirm/Cancel業務可能會被多次調用,就需要保障其冪等性。 那么,應該由TCC事務框架來提供冪等性保障?還是應該由業務系統自行來保障冪等性呢? 個人認為,應該是由TCC事務框架來提供冪等性保障。如果僅僅只是極個別服務存在這個問題的話,那么由業務系統來負責也是可以的;然而,這是一類公共問題,毫無疑問,所有TCC服務的Confirm/Cancel業務存在冪等性問題。TCC服務的公共問題應該由TCC事務框架來解決;而且,考慮一下由業務系統來負責冪等性需要考慮的問題,就會發現,這無疑增大了業務系統的復雜度。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容