微服務倡導將複雜的單體應用拆分為若干個功能簡單、鬆耦合的服務,而於此同時就會引入多個服務之間的分散式事務的問題。
眾所周知,資料庫能實現本地事務,也就是說在同一個資料庫中,可以保證事務的原子性,就是全部成功或者失敗,上篇文章也寫過簡單的多資料來源事務的解決方案(類似2PC)
但現在的系統往往採用微服務架構,業務系統擁有獨立的資料庫,因此就出現了跨多個資料庫的事務需求,這種事務即為“分散式事務”。
針對這樣的問題一般常用的實現方案有:
- 2PC/3PC (兩階段提交協議/三階段提交協議)
- TCC
- 基於可靠訊息服務的分散式事務
基於可靠訊息服務的分散式事務
基於這個方案,我自己實現了一個基於rabbitmq的分散式事務中介軟體,shine-mq
一開始本來是想用來封裝mq的操作方便使用,後續迭代增加了分散式事務的功能。下面就來介紹下這個中介軟體:
- 在服務A處理任務A前,首先向Coordinator傳送一條prepare(攜帶回查id)記錄,表示要開始這個分散式任務
- Coordinator持久化prepare記錄後響應服務A
- 服務A收到確認應答後,服務A處理任務A,成功後傳送一條ready記錄,Coordinator將刪除之前對應的prepare記錄,並持久化ready記錄和完整的訊息
- 服務A在收到ready記錄和訊息持久化的應答後,就可以提交訊息到訊息中介軟體了,針對rabbitmq可以設定
setPublisherConfirms(true)
以及實現setConfirmCallback
的回撥來實現訊息中介軟體持久化應答服務A。這之後對於服務A來說就可以刪除之前的ready記錄和去處理其他任務了。 - 訊息中介軟體(rabbitmq可以通過映象佇列來實現高可用)在確定將訊息落盤之後就可以向服務B投遞訊息
- 服務B消費了該訊息,併成功處理了任務B,服務B再向訊息中介軟體返回一個確認應答,告訴訊息中介軟體該訊息已經成功消費,此時,這個分散式事務完成。
上述是整個流程,服務A完成任務A後,到任務B執行完成之間,會存在一定的時間差。在這個時間差內,整個系統處於資料不一致的狀態,但這短暫的不一致性是可以接受的,因為經過短暫的時間後,系統又可以保持資料一致性,滿足BASE理論。
BASE理論:
- BA:Basic Available 基本可用
- S:Soft State:柔性狀態 同一資料的不同副本的狀態,可以不需要實時一致。
- E:Eventual Consisstency:最終一致性 同一資料的不同副本的狀態,可以不需要實時一致,但一定要保證經過一定時間後仍然是一致的。
上面的是一個比較理想的流程,但是真正的環境會有很多突發情況,比如任務A處理失敗,那麼需要進入回滾流程
因為任務A的異常對於服務A是可以直接捕獲的,回滾異常後刪除prepare記錄,服務A刪除之後便可以認為回滾已經完成,便可以去做其他的事情。
而該訊息沒有投遞到訊息中介軟體,則服務B沒有影響。此時系統又處於一致性狀態,因為任務A和任務B都沒有執行。
Coordinator提供了介面可以自己來實現,我預設實現的方式是用redis。對於這類中介軟體在生產環境中網路傳輸是有可能丟失的。
上圖表現的是傳送ready記錄的時候,失敗了。這時候對於服務A是會收到異常或者收不到應答,這時候可以直接將之前的任務A進行回滾,任務A在回滾的時候會觸發刪除ready的操作。同樣如果異常是發生在傳送prepare的情況下,這時候服務A還沒執行任務也不會有影響。
分析完服務A,Coordinator和訊息中介軟體之間的一些情況後,現在分析下訊息中介軟體和服務B之間的一些特殊情況。
當訊息成功釋出到訊息中介軟體之後,服務A就可以做自己的事情去了,訊息中介軟體會保證訊息能成功投遞到服務B。這個就是訊息中介軟體在訊息投遞情況下的可靠性保證,具體流程是訊息中介軟體向下遊系統投遞完訊息後便進入阻塞等待狀態,下游系統便立即進行任務的處理,任務處理完成後便向訊息中介軟體返回應答。訊息中介軟體收到確認應答後便認為該事務處理完畢!如果訊息在投遞過程中丟失,或訊息的確認應答在返回途中丟失,那麼訊息中介軟體在等待確認應答超時之後就會重新投遞,直到下游消費者返回消費成功響應為止。
這之間可以設定訊息重試的次數和時間間隔,如果一直失敗這時候就會用到死信佇列。具體看下圖:
當訊息一直無法被正常消費,超過設定的重試閾值就會投遞到死信佇列,死信佇列的exchange和routeKey預設是@DistributedTrans
中設定的值。
通過消費死信佇列的訊息來處理這種異常情況(可以設定簡訊或郵箱提醒,人工介入),這裡暫時不實現服務A的回滾,因為讓服務A事先提供回滾介面,這無疑增加了額外的開發成本,業務系統的複雜度也將提高。對於一個業務系統的設計目標是,在保證效能的前提下,最大限度地降低系統複雜度,從而能夠降低系統的運維成本。
最後整理一下整個中介軟體的設計思路
上面已經分析了下游服務和訊息中介軟體,我們可以通過訊息中介軟體投遞的可靠性來保證。 那麼我們要實現分散式事務,剩下的就是要保證上游服務執行的任務和向訊息中介軟體投遞訊息這2個操作的原子性。
這時候一般就會有兩種方案,同步和非同步通訊。通過之前的時序圖,很顯然上游系統和訊息中介軟體之間採用的是非同步通訊,這主要是為了提高系統併發度,另外業務系統直接和使用者打交道,使用者體驗尤為重要,因此這種非同步通訊方式能夠極大程度地降低使用者等待時間。
而非同步通訊的方式,rabbitmq也其實有提供事務機制,使用txSelect(), txCommit()以及txRollback()來實現,通過測試抓包發現,Tx.Commit與Tx.Commit-Ok之間的時間間隔會比較長,簡單的測試能到300ms,這是很耗時的。所以我沒有用這種方式,而是引入一個Coordinator(協調者)來實現。
另外還有一個比較關鍵的daemon(守護執行緒),是處理在Coordinator一些錯誤超時的記錄(類似Rocketmq的超時詢問機制)。所以服務A除了實現正常的業務流程外,還需提供一個事務詢問的介面,供Coordinator呼叫,來保障服務A在執行任務出現當機的情況。當有prepare超時就會觸發訪問這個回查介面,該介面會返回三種結果:
- 提交 將該訊息投遞
- 回滾 直接將條訊息丟棄
- 處理中 繼續等待,重置時間。
而超時的ready的訊息,就直接撈起傳送到訊息中介軟體,因為只要是ready訊息持久化到協調者,那就說明服務A的任務已經完成。
這樣就能保證上游服務和訊息中介軟體的原子性了,再通過訊息中介軟體可靠的投遞結合下游服務,就完成了分散式事務。
Github 不要吝嗇你的star ^.^