分散式事務利器——RocketMQ事務訊息的啟示

普通程式設計師發表於2019-07-01

一、大事務 = 小事務 + 非同步

我們以一個轉帳的場景為例來說明這個問題,Bob向Smith轉賬100塊。這個列子在瓜子也有很多實際場景對映,如:車源狀態變化,訂單狀態變化,金融放款,物流運輸……

單機環境下,執行事務的情況,大概是下面這個樣子 

分散式事務利器——RocketMQ事務訊息的啟示

當使用者增長到一定程度,Bob和Smith的賬戶及餘額資訊已經不在同一臺伺服器上了,那麼上面的流程就變成了這樣

分散式事務利器——RocketMQ事務訊息的啟示

這時候你會發現,同樣是一個轉賬的業務,在叢集環境下,耗時居然成倍的增長,這顯然是不能夠接受的。而且跨網路呼叫的事務需要解決網路不穩定的因素,直接放到業務程式碼裡控制,成本很高。那如何來規避這個問題?

大事務 = 小事務 + 非同步

將大事務拆分成多個小事務非同步執行。這樣基本上能夠將跨機事務的執行效率優化到與單機一致。轉賬的事務就可以分解成如下兩個小事務 

分散式事務利器——RocketMQ事務訊息的啟示

圖中執行本地事務(Bob賬戶扣款)和傳送非同步訊息應該保證同時成功或者同時失敗,也就是扣款成功了,傳送訊息一定要成功,如果扣款失敗了,就不能再傳送訊息。

去哪兒qmq方案(《寫資料庫同時發mq訊息事務一致性的一種解決方案》)巧妙的把問題轉換為單點的資料庫事務,確保事務的完整性。RocketMQ採用了分散式事務的方式來解決這個問題。

二、什麼是事務訊息(Transactional message)

RocketMQ官方是這樣定義的。可以將其視為兩階段提交訊息實現,以確保分散式系統中的最終一致性。事務性訊息確保可以原子方式執行本地事務的執行和訊息的傳送

1、事務狀態

事務訊息有3種狀態

(1)TransactionStatus.CommitTransaction,提交事務,表示允許消費者消費(使用)這條訊息

(2)TransactionStatus.RollbackTransaction,回滾事務,表示訊息將被刪除,不允許使用

(3)TransactionStatus.Unknown,中間狀態,表示需要MQ向訊息傳送方進行檢查以確定狀態

2、如何傳送事務訊息

RocketMQ(4.5.1版本)已經把事務訊息的傳送方式封裝得非常優雅,只需要兩個大的環節就能夠完成,建立事務訊息生產者和實現TransactionListener介面。看一下官方的例子程式碼

(1)建立事務訊息生產者

使用TransactionMqProducer類建立訊息生產客戶端,並指定唯一的ProducerGroup

設定自定義執行緒池來處理檢查請求

執行本地事務之後,需要根據執行結果回覆MQ,回覆上一小節中描述的狀態 

分散式事務利器——RocketMQ事務訊息的啟示

(2)實現TransactionListener介面

“executeLocalTransaction”方法用於在傳送半條訊息成功時執行本地事務。它返回上一節中提到的三個事務狀態之一。

“check local transaction”方法用於檢查本地事務狀態並響應MQ檢查請求。它還返回前一節中提到的三個事務狀態之一。 

分散式事務利器——RocketMQ事務訊息的啟示

3、事務訊息的執行流程

程式碼寫起來非常簡單,以至於光看程式碼,並不能知道事務訊息具體的執行過程。

RocketMQ 事務訊息的設計流程借鑑了兩階段提交理論,整體互動流程如下圖所示 

分散式事務利器——RocketMQ事務訊息的啟示

事務發起方(即訊息傳送者)首先傳送 prepare 訊息到 MQ。

事務發起方(即訊息傳送者)在傳送 prepare 訊息成功後執行本地事務。

根據本地事務執行結果傳送 commit 或者是 rollback 給 MQ。

如果訊息是 rollback,MQ 將刪除該 prepare 訊息不進行下發。

如果訊息是 commit,MQ 將會把這個訊息傳送給 consumer 端。

如果執行本地事務過程中,執行端掛掉,或者超時,導致 MQ 收不到任何的訊息(不知道是該 commit 還是該 rollback),RocketMQ 會定期掃描訊息叢集中的事務訊息,這時候發現了某個 prepare 訊息還不知道該怎麼處理,它會向訊息傳送者確認,所以訊息傳送者需要實現一個 check 介面,RocketMQ 會根據訊息傳送者設定的策略來決定是 rollback 還是繼續 commit。這樣就保證了訊息傳送與本地事務同時成功或同時失敗。

Consumer 端的消費成功機制由 MQ 保證。

4、事務訊息的儲存模型

在具體實現上,RocketMQ 通過使用 Half Topic 以及 Operation Topic 兩個內部佇列來儲存事務訊息推進狀態,如下圖所示 

分散式事務利器——RocketMQ事務訊息的啟示

其中,Half Topic 對應佇列中存放著 prepare 訊息,Operation Topic 對應的佇列則存放了 prepare message 對應的 commit/rollback 訊息,訊息體中則是 prepare message 對應的 offset,服務端通過比對兩個佇列的差值來找到尚未提交的超時事務,進行回查。

使用者側來說,使用者需要分別實現本地事務執行以及本地事務回查方法,因此只需關注本地事務的執行狀態即可;而在 service 層,則對事務訊息的兩階段提交進行了抽象,同時針對超時事務實現了回查邏輯,通過不斷掃描當前事務推進狀態,來不斷反向請求 Producer 端獲取超時事務的執行狀態,在避免事務掛起的同時,也避免了 Producer 端的單點故障。

而在儲存層,RocketMQ 通過 Bridge 封裝了與底層佇列儲存的相關操作,用以操作兩個對應的內部佇列,使用者也可以依賴其他儲存介質實現自己的 service,RocketMQ 會通過 ServiceProvider 載入進來。

三、Notify的異曲同工

Notify和MetaQ是阿里的兩個訊息中介軟體。MetaQ是一個高效能的儲存佇列;Notify是淘寶自主研發的一套訊息服務引擎。貼兩個圖就什麼都明白了 

分散式事務利器——RocketMQ事務訊息的啟示


分散式事務利器——RocketMQ事務訊息的啟示

整體方案跟RocketMQ是完全相同的,只是兩者的Storage不同。

四、瓜子該怎麼做事務一致性這塊工作

針對這個典型場景,有很多解決方案

1、Kafka換成RocketMQ

不行。有太多的業務跑在了Kafka上,替換訊息中介軟體的成本基本不能接受。

2、類似去哪兒qmq的方案(《寫資料庫同時發mq訊息事務一致性的一種解決方案》)

這個方案研發簡單,但是侵入具體業務的資料庫,而且增加了部署運維的成本。

3、有人提出binlog+TCC的方案

沒有仔細研究,但是業務會經常調整,想想負責配置資料庫日誌的同學肯定會抓狂(DBA沒有那麼瞭解業務)。

4、為Kafka配一個類似Notify的訊息引擎

這個方案有一定的可行性

(1)把Kafka定位為MetaQ,研製一個Notify,為prepare message提供單獨的儲存

(2)現在各業務系統所採用的Kafka客戶端已經是瓜子定製化開發的,可以模仿RocketMQ的客戶端進行改造。已有程式碼的邏輯完全不受影響;需要事務一致性的功能,只需要換個介面,實現check邏輯即可,而原有消費方毫無感覺。

(3)似乎有可能結合spring的@Transactional標籤,在完全不改業務程式碼(只升級自研Kafka客戶端)的情況下,也能緩解一些不一致問題(《事務註解(@Transactional)引起的資料覆蓋故障》) 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31556438/viewspace-2649246/,如需轉載,請註明出處,否則將追究法律責任。

相關文章