說在前面
Apache RocketMQ-4.3.0正式Release了事務訊息的特性,順著最近的這個熱點。第一篇文章,就來聊一下在軟體工程學上的長久的難題——分散式事務(Distributed Transaction)。
這個技術也在各個諸如阿里,騰訊等大廠的內部,被廣泛地實現,利用及優化。但是由於理論上就有難點,所以分散式事務就隱晦得成了大廠對於小廠的技術壁壘。相信來看這篇文章的同學,一定都聽過很多關於分散式事務的術語,比較二階段提交,TCC,最終一致性等,所以這裡也不多普及概念。
基於RocketMQ的分散式事務
我們直接上正題,利用RocketMQ設計自己的分散式事務元件。
舉個虛擬場景引出問題
使用者從農行
轉賬100元去招行
,農行的系統和招行的系統分別部署在自己的機房,系統之間通過訊息
進行通訊,防止過度耦合。
整個模型可以不恰當得描述為:農行扣了100元后,傳送“已經扣款”的訊息給招行,招行收到訊息,知道農行扣款成功了,然後在招行賬戶上加100元。
問題是,農行這邊,方案1. 先扣100元再發訊息,方案2. 先發訊息再扣100元
整理下整個事務不一致的場景:
方案1,
農行扣100後成功,但是訊息傳送失敗,招行沒有加100
方案2,
訊息傳送成功,但是農行扣100元失敗,招行收到訊息加了100
各位同學應該已經發現問題所在了,扣款和傳送訊息這兩個事情,沒有辦法通過調換順序實現「同時成功」,或者「同時失敗」。如果前者成功,後者失敗,就會造成不一致。
RocketMQ,以下簡稱RMQ,為了實現事務訊息引入了一種新的訊息型別:TransactionMsg
一個完整的事務訊息分成兩個部分:
HalfMsg(Prepare)
+ Commit/RollbackMsg
Producer傳送了HalfMsg後,由於HalfMsg不是一個完整的事務訊息,Consumer無法立刻就消費到該訊息,Producer可以對HalfMsg進行Commit或者Rollback來終結事務(EndTransacaction)。只有當Commit了HalfMsg後,Consumer才能消費到這條訊息。RMQ會定期去向Producer詢問,是否可以Commit或者Rollback那些由於錯誤沒有被終結的HalfMsg來結束它們的生命週期,以達成事務最終的一致。
依然是剛剛的轉賬場景,我們用RMQ事務訊息來優化下流程:
-
農行向RMQ同步傳送HalfMsg,訊息中攜帶農行即將要扣100元的資訊
-
農行HalfMsg成功傳送後,執行資料庫本地事務,在自己的系統中扣100元
-
農行檢視本地事務執行情況
-
本地事務返回成功,農行向RMQ提交(Commit)HalfMsg
-
招行系統訂閱了RMQ,順利收到農行已經扣款100元的資訊
-
招行系統執行本地事務,在招行的系統中加100元
圖1:RMQ事務訊息原理
同樣得,我們逐個來分析下這個流程是不是會出現不一致:
-
農行傳送HalfMsg是同步傳送(Sync),如果HalfMsg傳送不成功,壓根就不會執行本地事務
-
傳送HalfMsg成功,但是農行扣款****本地事務失敗,也沒事,如果本地事務沒有成功,立刻就傳送Rollback去回滾HalfMsg。就當之前啥事都沒有發生過
-
農行本地事務成功了,但是Commit卻失敗了,但是由於HalfMsg已經在RMQ中,RMQ就能通過定時程式讓農行重新檢測本地事務是否成功,重新Commit。Rollback失敗了也是同理
-
招行消費了訊息後,加錢本地事務失敗了,但是招行收到的訊息持久化在MQ,甚至可以持久化在招行資料庫,可以進行事務重試
剛剛討論的案例是非常理想化的,整個分散式事務中,只涉及到了金額的變化,但是,真正的線上系統,作為訊息傳送方的本地事務可能就非常複雜,可能涉及到了幾十張不同的表,那RMQ用定時器來Check HalfMsg,難道去查下涉及該事務的每一張表的資料是否提交成功?顯然這種方案非常業務侵入非常大,並且很難元件化。所以需要在本地事務中設計一張Transaction表,將業務表和Transaction繫結在同一個本地事務中,如果農行的扣款本地事務成功時,Transaction中應當已經記錄該TransactionId的狀態為「已完成」。當最後需要檢查時,只需要檢查對應的TransactionId的狀態是否是「已完成」就好,而不用關心具體的業務資料。
再談一個小細節,
細心的同學可能發現,剛剛No.3的討論其實是有點不嚴謹的,RMQ在呼叫Commit或者Rollback時,用的是Oneway
的方式,熟悉RMQ原始碼的話,知道這種網路呼叫是只單向傳送Request,不會去獲取Response。訊息傳送效能上是有非常大的提升的,但是如果真的傳送失敗,Producer是不會知曉的,最後只能通過定時檢查HalfMsg才能終結事務。
public void endTransactionOneway(
final String addr,
final EndTransactionRequestHeader requestHeader,
final String remark,
final long timeoutMillis
) throws RemotingException, MQBrokerException, InterruptedException {
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, requestHeader);
request.setRemark(remark);
// 使用Oneway傳送end transaction型別的
this.remotingClient.invokeOneway(addr, request, timeoutMillis);
}
複製程式碼
脫離RocketMQ的分散式事務
不是所有的MQ都能支援事務訊息,如何使用一般的MQ來搭建分散式事務元件,甚至抽象成一個事務SOA服務?
其實仔細分析下RMQ的事務訊息,我們可以把它拆解成兩個部分:
事務管理器
+ 訊息
所謂的事務管理器,就是對於事務的預備(Prepare)、提交(Commit)和回滾(Rollback)的管理,另外還包含預備事務的定時檢查器。
訊息,指的就是一般的同步訊息,傳送後能明確得到傳送結果,用於事務系統與業務系統解耦。幾乎所有的分散式MQ都是支援這種訊息的。
我們來設計下自己的DistributedTransaction SOA,以下簡稱DT-SOA
圖2:分散式事務服務化
流程還是沒有變,但分散式事務不再強依賴RMQ,而是用一般的MQ代替:
-
系統A傳送事務,首先呼叫DT-SOA的Prepare方法準備開啟事務,由於是同步呼叫,獲取SendResult,如果傳送成功,拿到全域性分散式事務的ID——TID
-
系統A用獲取到的TID執行本地事務,本地事務中包含Transaction狀態表,成功後將TID對應的狀態置為“已完成”
-
系統A呼叫DT-SOA提交事務,DT-SOA用MQ傳送同步訊息給系統B
-
系統B監聽對應Topic,接收到訊息後,執行對應的本地事務