RocketMQ 和 Kafka是如何實現(xiàn)事務(wù)的?
每個時代,都不會虧待會學(xué)習(xí)的人。
本文公眾號來源:yes的練級攻略 作者:是Yes呀 本文已收錄至我的GitHub
今天我們來談一談消息隊列的事務(wù)消息,一說起事務(wù)相信大家都不陌生,腦海里蹦出來的就是 ACID。
通常我們理解的事務(wù)就是為了一些更新操作要么都成功,要么都失敗,不會有中間狀態(tài)的產(chǎn)生,而 ACID 是一個嚴格的事務(wù)實現(xiàn)的定義,不過在單體系統(tǒng)時候一般都不會嚴格的遵循 ACID 的約束來實現(xiàn)事務(wù),更別說分布式系統(tǒng)了。
分布式系統(tǒng)往往只能妥協(xié)到最終一致性,保證數(shù)據(jù)最終的完整性和一致性,主要原因就是實力不允許...因為可用性為王。
而且要保證完全版的事務(wù)實現(xiàn)代價很大,你想想要維護這么多系統(tǒng)的數(shù)據(jù),不允許有中間狀態(tài)數(shù)據(jù)可以被讀取,所有的操作必須不可分割,這意味著一個事務(wù)的執(zhí)行是阻塞的,資源是被長時間鎖定的。
在高并發(fā)情況下資源被長時間的占用,就是致命的傷害,舉一個有味道的例子,如廁高峰期,好了懂得都懂。

對了, ACID 是什么還不太清楚的同學(xué),趕緊去查一查,這里我就不展開說了。
分布式事務(wù)
那說到分布式事務(wù),常見的有 2PC、TCC 和事務(wù)消息,這篇文章重點就是事務(wù)消息,不過 2PC 和 TCC 我稍微提一下。
2PC
2PC 就是二階段提交,分別有協(xié)調(diào)者和參與者兩個角色,二階段分別是準(zhǔn)備階段和提交階段。
準(zhǔn)備階段就是協(xié)調(diào)者向各參與者發(fā)送準(zhǔn)備命令,這個階段參與者除了事務(wù)的提交啥都做了,而提交階段就是協(xié)調(diào)者看看各個參與者準(zhǔn)備階段都 o 不 ok,如果有 ok 那么就向各個參與者發(fā)送提交命令,如果有一個不 ok 那么就發(fā)送回滾命令。
這里的重點就是 2PC 只適用于數(shù)據(jù)庫層面的事務(wù),什么意思呢?就是你想在數(shù)據(jù)庫里面寫一條數(shù)據(jù)同時又要上傳一張圖片,這兩個操作 2PC 無法保證兩個操作滿足事務(wù)的約束。
而且 2PC 是一種強一致性的分布式事務(wù),它是同步阻塞的,即在接收到提交或回滾命令之前,所有參與者都是互相等待,特別是執(zhí)行完準(zhǔn)備階段的時候,此時的資源都是鎖定的狀態(tài),假如有一個參與者卡了很久,其他參與者都得等它,產(chǎn)生長時間資源鎖定狀態(tài)下的阻塞。
總體而言效率低,并且存在單點故障問題,協(xié)調(diào)者是就是那個單點,并且在極端條件下存在數(shù)據(jù)不一致的風(fēng)險,例如某個參與者未收到提交命令,此時宕機了,恢復(fù)之后數(shù)據(jù)是回滾的,而其他參與者其實都已經(jīng)執(zhí)行了提交事務(wù)的命令了。
TCC
TCC 能保證業(yè)務(wù)層面的事務(wù),也就是說它不僅僅是數(shù)據(jù)庫層面,上面的上傳圖片這種操作它也能做。
TCC 分為三個階段 try - confirm - cancel,簡單的說就是每個業(yè)務(wù)都需要有這三個方法,先都執(zhí)行 try 方法,這一階段不會做真正的業(yè)務(wù)操作,只是先占個坑,什么意思呢?比如打算加 10 個積分,那先在預(yù)添加字段加上這 10 積分,這個時候用戶賬上的積分其實是沒有增加的。
然后如果都 try 成功了那么就執(zhí)行 confirm 方法,大家都來做真正的業(yè)務(wù)操作,如果有一個 try 失敗了那么大家都執(zhí)行 cancel 操作,來撤回剛才的修改。
可以看到 TCC 其實對業(yè)務(wù)的耦合性很大,因為業(yè)務(wù)上需要做一定的改造才能完成這三個方法,這其實就是 TCC 的缺點,并且 confirm 和 cancel 操作要注意冪等,因為到執(zhí)行這兩步的時候沒有退路,是務(wù)必要完成的,因此需要有重試機制,所以需要保證方法冪等。
事務(wù)消息
事務(wù)消息就是今天文章的主角了,它主要是適用于異步更新的場景,并且對數(shù)據(jù)實時性要求不高的地方。
它的目的是為了解決消息生產(chǎn)者與消息消費者的數(shù)據(jù)一致性問題。
比如你點外賣,我們先選了炸雞加入購物車,又選了瓶可樂,然后下單,付完款這個流程就結(jié)束了。

而購物車里面的數(shù)據(jù)就很適合用消息通知異步刪除,因為一般而言我們下完單不會再去點開這個店家的菜單,而且就算點開了購物車里還有這些菜品也沒有關(guān)系,影響不大。
我們希望的就是下單成功之后購物車的菜品最終會被刪除,所以要點就是下單和發(fā)消息這兩個步驟要么都成功要么都失敗。
RocketMQ 事務(wù)消息
我們先來看一下 RocketMQ 是如何實現(xiàn)事務(wù)消息的。
RocketMQ 的事務(wù)消息也可以被認為是一個兩階段提交,簡單的說就是在事務(wù)開始的時候會先發(fā)送一個半消息給 Broker。
半消息的意思就是這個消息此時對 Consumer 是不可見的,而且也不是存在真正要發(fā)送的隊列中,而是一個特殊隊列。
發(fā)送完半消息之后再執(zhí)行本地事務(wù),再根據(jù)本地事務(wù)的執(zhí)行結(jié)果來決定是向 Broker 發(fā)送提交消息,還是發(fā)送回滾消息。
此時有人說這一步發(fā)送提交或者回滾消息失敗了怎么辦?
影響不大,Broker 會定時的向 Producer 來反查這個事務(wù)是否成功,具體的就是 Producer 需要暴露一個接口,通過這個接口 Broker 可以得知事務(wù)到底有沒有執(zhí)行成功,沒成功就返回未知,因為有可能事務(wù)還在執(zhí)行,會進行多次查詢。
如果成功那么就將半消息恢復(fù)到正常要發(fā)送的隊列中,這樣消費者就可以消費這條消息了。
我們再來簡單的看下如何使用,我根據(jù)官網(wǎng)示例代碼簡化了下。

可以看到使用起來還是很簡便直觀的,無非就是多加個反查事務(wù)結(jié)果的方法,然后把本地事務(wù)執(zhí)行的過程寫在 TransationListener 里面。
至此 RocketMQ 事務(wù)消息大致的流程已經(jīng)清晰了,我們畫一張整體的流程圖來過一遍,其實到第四步這個消息要么就是正常的消息,要么就是拋棄什么都不存在,此時這個事務(wù)消息已經(jīng)結(jié)束它的生命周期了。

RocketMQ 事務(wù)消息源碼分析
然后我們再從源碼的角度來看看到底是怎么做的,首先我們看下sendMessageInTransaction 方法,方法有點長,不過沒有關(guān)系結(jié)構(gòu)還是很清晰的。

流程也就是我們上面分析的,將消息塞入一些屬性,標(biāo)明此時這個消息還是半消息,然后發(fā)送至 Broker,然后執(zhí)行本地事務(wù),然后將本地事務(wù)的執(zhí)行狀態(tài)發(fā)送給 Broker ,我們現(xiàn)在再來看下 Broker 到底是怎么處理這個消息的。
在 Broker 的 SendMessageProcessor#sendMessage 中會處理這個半消息請求,因為今天主要分析的是事務(wù)消息,所以其他流程不做分析,我大致的說一下原理。
簡單的說就是 sendMessage 中查到接受來的消息的屬性里面MessageConst.PROPERTY_TRANSACTION_PREPARED 是 true ,那么可以得知這個消息是事務(wù)消息,然后再判斷一下這條消息是否超過最大消費次數(shù),是否要延遲,Broker 是否接受事務(wù)消息等操作后,將這條消息真正的 topic 和隊列存入屬性中,然后重置消息的 topic 為RMQ_SYS_TRANS_HALF_TOPIC,并且隊列是 0 的隊列中,使得消費者無法讀取這個消息。
以上就是整體處理半消息的流程,我們來看一下源碼。

就是來了波貍貓換太子,其實延時消息也是這么實現(xiàn)的,最終將換了皮的消息入盤。
Broker 處理提交或者回滾消息的處理方法是 EndTransactionProcessor#processRequest,我們來看一看它做了什么操作。

可以看到,如果是提交事務(wù)就是把皮再換回來寫入真正的topic所屬的隊列中,供消費者消費,如果是回滾則是將半消息記錄到一個 half_op 主題下,到時候后臺服務(wù)掃描半消息的時候就依據(jù)其來判斷這個消息已經(jīng)處理過了。
那個后臺服務(wù)就是 TransactionalMessageCheckService 服務(wù),它會定時的掃描半消息隊列,去請求反查接口看看事務(wù)成功了沒,具體執(zhí)行的就是TransactionalMessageServiceImpl#check 方法。
我大致說一下流程,這一步驟其實涉及到的代碼很多,我就不貼代碼了,有興趣的同學(xué)自行了解。不過我相信用語言也是能說清楚的。
首先取半消息 topic 即RMQ_SYS_TRANS_HALF_TOPIC下的所有隊列,如果還記得上面內(nèi)容的話,就知道半消息寫入的隊列是 id 是 0 的這個隊列,然后取出這個隊列對應(yīng)的 half_op 主題下的隊列,即 RMQ_SYS_TRANS_OP_HALF_TOPIC 主題下的隊列。
這個 half_op 主要是為了記錄這個事務(wù)消息已經(jīng)被處理過,也就是說已經(jīng)得知此事務(wù)消息是提交的還是回滾的消息會被記錄在 half_op 中。
然后調(diào)用 fillOpRemoveMap 方法,從 half_op 取一批已經(jīng)處理過的消息來去重,將那些沒有記錄在 half_op 里面的半消息調(diào)用 putBackHalfMsgQueue 又寫入了 commitlog 中,然后發(fā)送事務(wù)反查請求,這個反查請求也是 oneWay,即不會等待響應(yīng)。當(dāng)然此時的半消息隊列的消費 offset 也會推進。
然后producer中的 ClientRemotingProcessor#processRequest 會處理這個請求,會把任務(wù)扔到 TransactionMQProducer 的線程池中進行,最終會調(diào)用上面我們發(fā)消息時候定義的 checkLocalTransactionState 方法,然后將事務(wù)狀態(tài)發(fā)送給 Broker,也是用 oneWay 的方式。
看到這里相信大家會有一些疑問,比如為什么要有個 half_op ,為什么半消息處理了還要再寫入 commitlog 中別急聽我一一道來。
首先 RocketMQ 的設(shè)計就是順序追加寫入,所以說不會更改已經(jīng)入盤的消息,那事務(wù)消息又需要更新反查的次數(shù),超過一定反查失敗就判定事務(wù)回滾。
因此每一次要反查的時候就將以前的半消息再入盤一次,并且往前推進消費進度。而 half_op 又會記錄每一次反查的結(jié)果,不論是提交還是回滾都會記錄,因此下一次還循環(huán)到處理此半消息的時候,可以從 half_op 得知此事務(wù)已經(jīng)結(jié)束了,因此就被過濾掉不需要處理了。
如果得到的反查的結(jié)果是 UNKNOW,那 half_op 中也不會記錄此結(jié)果,因此還能再次反查,并且更新反查次數(shù)。
到現(xiàn)在整個流程已經(jīng)清晰了,我再畫個圖總結(jié)一下 Broker 的事務(wù)處理流程。

Kafka 事務(wù)消息
Kafka 的事務(wù)消息和 RocketMQ 的事務(wù)消息又不一樣了,RocketMQ 解決的是本地事務(wù)的執(zhí)行和發(fā)消息這兩個動作滿足事務(wù)的約束。
而 Kafka 事務(wù)消息則是用在一次事務(wù)中需要發(fā)送多個消息的情況,保證多個消息之間的事務(wù)約束,即多條消息要么都發(fā)送成功,要么都發(fā)送失敗,就像下面代碼所演示的。

Kafka 的事務(wù)基本上是配合其冪等機制來實現(xiàn) Exactly Once 語義的,所以說 Kafka 的事務(wù)消息不是我們想的那種事務(wù)消息,RocketMQ 的才是。
講到這我就想扯一下了,說到這個 Exactly Once 其實不太清楚的同學(xué)很容易會誤解。
我們知道消息可靠性有三種,分別是最多一次、恰好一次、最少一次,之前在消息隊列連環(huán)問的文章我已經(jīng)提到了基本上我們都是用最少一次然后配合消費者端的冪等來實現(xiàn)恰好一次。
消息恰好被消費一次當(dāng)然我們所有人追求的,但是之前文章我已經(jīng)從各方面已經(jīng)分析過了,基本上難以達到。
而 Kafka 竟說它能實現(xiàn) Exactly Once?這么牛啤嗎?這其實是 Kafka 的一個噱頭,你要說他錯,他還真沒錯,你要說他對但是他實現(xiàn)的 Exactly Once 不是你心中想的那個 Exactly Once。
它的恰好一次只能存在一種場景,就是從 Kafka 作為消息源,然后做了一番操作之后,再寫入 Kafka 中。

那他是如何實現(xiàn)恰好一次的?就是通過冪等,和我們在業(yè)務(wù)上實現(xiàn)的一樣通過一個唯一 Id, 然后記錄下來,如果已經(jīng)記錄過了就不寫入,這樣來保證恰好一次。
所以說 Kafka 實現(xiàn)的是在特定場景下的恰好一次,不是我們所想的利用 Kafka 來發(fā)送消息,那么這條消息只會恰巧被消費一次。
這其實和 Redis 說他實現(xiàn)事務(wù)了一樣,也不是我們心想的事務(wù)。
所以開源軟件說啥啥特性開發(fā)出來了,我們一味的相信,因此其往往都是殘血的或者在特殊的場景下才能滿足,不要被誤導(dǎo)了,不能相信表面上的描述,還得詳細的看看文檔或者源碼。
不過從另一個角度看也無可厚非,作為一個開源軟件肯定是想更多的人用,我也沒說謊呀,我文檔上寫的很清楚的,這標(biāo)題也沒騙人吧?
確實,比如你點進震驚xxxx標(biāo)題的文章,人家也沒騙你啥,他自己確實震驚的呢。

再回來談 Kafka 的事務(wù)消息,所以說這個事務(wù)消息不是我們想要的那個事務(wù)消息,其實不是今天的主題了,不過我還是簡單的說一下。
Kafka 的事務(wù)有事務(wù)協(xié)調(diào)者角色,事務(wù)協(xié)調(diào)者其實就是 Broker 的一部分。
在開始事務(wù)的時候,生產(chǎn)者會向事務(wù)協(xié)調(diào)者發(fā)起請求表示事務(wù)開啟,事務(wù)協(xié)調(diào)者會將這個消息記錄到特殊的日志-事務(wù)日志中,然后生產(chǎn)者再發(fā)送真正想要發(fā)送的消息,這里 Kafka 和 RocketMQ 處理不一樣,Kafka 會像對待正常消息一樣處理這些事務(wù)消息,由消費端來過濾這個消息。
然后發(fā)送完畢之后生產(chǎn)者會向事務(wù)協(xié)調(diào)者發(fā)送提交或者回滾請求,由事務(wù)協(xié)調(diào)者來進行兩階段提交,如果是提交那么會先執(zhí)行預(yù)提交,即把事務(wù)的狀態(tài)置為預(yù)提交然后寫入事務(wù)日志,然后再向所有事務(wù)有關(guān)的分區(qū)寫入一條類似事務(wù)結(jié)束的消息,這樣消費端消費到這個消息的時候就知道事務(wù)好了,可以把消息放出來了。
最后協(xié)調(diào)者會向事務(wù)日志中再記一條事務(wù)結(jié)束信息,至此 Kafka 事務(wù)就完成了,我拿 confluent.io 上的圖來總結(jié)一下這個流程。

最后
至此我們已經(jīng)知道了 RocketMQ 和 Kakfa 的事務(wù)消息全流程,可以看到 RocketMQ 的事務(wù)消息才是我們想要的,當(dāng)然你要是用的流式計算那么 Kakfa 的事務(wù)消息也是你想要的。
需要貼代碼的文章其實很難受,這貼的多不好,貼的少又怕不清晰,真的難,如果覺得文章不錯記得點個在看喲。

原創(chuàng)電子書原創(chuàng)思維導(dǎo)圖
掃碼或微信搜 Java3y?回復(fù)「888」領(lǐng)取1000+頁原創(chuàng)電子書和思維導(dǎo)圖。
![]() |
|





