RocketMQ是如何實現事務消息的

RocketMQ是如何實現事務消息的

前言

在RocketMQ4.3.0版本後,開放了事務消息這一特性,對於分佈式事務而言,最常說的還是二階段提交協議,那麼RocketMQ的事務消息又是怎麼一回事呢,這裏主要帶着以下幾個問題來探究一下RocketMQ的事務消息:

  • 事務消息是如何實現的
  • 我們有哪些手段來監控事務消息的狀態
  • 事務消息的異常恢復機制

RocketMQ的事務消息是如何實現的

RocketMQ作爲一款消息中間件,主要作用就是幫助各個系統進行業務解耦,以及對消息流量有削峯填谷的作用,而對於事務消息,主要是通過消息的異步處理,可以保證本地事務和消息發送同時成功執行或失敗,從而保證數據的最終一致性,這裏我們先看看一條事務消息從誕生到結束的整個時間線流程:

在這裏插入圖片描述

  1. 生產者發送消息到broker,該消息是prepare消息,且事務消息的發送是同步發送的方式。
  2. broker接收到消息後,會將該消息進行轉換,所有的事務消息統一寫入Half Topic,該Topic默認是RMQ_SYS_TRANS_HALF_TOPIC ,寫入成功後會給生產者返回成功狀態。
  3. 本地生產獲取到該消息的事務Id,進行本地事務處理。
  4. 本地事務執行成功提交Commit,失敗則提交Rollback,超時提交或提交Unknow狀態則會觸發broker的事務回查。
  5. 若提交了Commit或Rollback狀態,Broker則會將該消息寫入到Op Topic,該Topic默認是RMQ_SYS_TRANS_OP_HALF_TOPIC,該Topic的作用主要記錄已經Commit或Rollback的prepare消息,Broker利用Half Topic和Op Topic計算出需要回查的事務消息。如果是commit消息,broker還會將消息從Half取出來存儲到真正的Topic裏,從而消費者可以正常進行消費,如果是Rollback則不進行其他操作
  6. 如果本地事務執行超時或返回了Unknow狀態,則broker會進行事務回查。若生產者執行本地事務超過6s則進行第一次事務回查,總共回查15次,後續回查間隔時間是60s,broker在每次回查時會將消息再在Half Topic寫一次。回查次數和時間間隔都是可配置的。
  7. 執行事務回查時,生產者可以獲取到事務Id,檢查該事務在本地執行情況,返回狀態同第一次執行本地事務一樣。

從上述流程可以看到事務消息其實只是保證了生產者發送消息成功與本地執行事務的成功的一致性,消費者在消費事務消息時,broker處理事務消息的消費與普通消息是一樣的,若消費不成功,則broker會重複投遞該消息16次,若仍然不成功則需要人工介入。

事務消息的成功投遞是需要經歷三個Topic的,分別是:

  • Half Topic:用於記錄所有的prepare消息
  • Op Half Topic:記錄已經提交了狀態的prepare消息
  • Real Topic:事務消息真正的Topic,在Commit後會纔會將消息寫入該Topic,從而進行消息的投遞

理解清楚事務消息在這三個Topic的流轉就基本理解清楚了RocketMQ的事務消息的處理。接下來我們看看在源碼中是如何使用這三個Topic的。

1.生產發送prepare消息

在這裏插入圖片描述

  1. 事務消息的生產者需要構造一個線程池與一個實現了TransactionListener的實現類,並註冊到TransactionMQProducer中,先看下TransactionListener的方法及作用:

    public interface TransactionListener {
        /**
         * 發送prepare消息成功後回調該方法用於執行本地事務
         * @param msg 回傳的消息,利用transactionId即可獲取到該消息的唯一Id
         * @param arg 調用send方法時傳遞的參數,當send時候若有額外的參數可以傳遞到send方法中,這裏能獲取到
         * @return 返回事務狀態,COMMIT:提交  ROLLBACK:回滾  UNKNOW:回調
         */
        LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);
        /**
         * @param msg 通過獲取transactionId來判斷這條消息的本地事務執行狀態
         * @return 返回事務狀態,COMMIT:提交  ROLLBACK:回滾  UNKNOW:回調
         */
        LocalTransactionState checkLocalTransaction(final MessageExt msg);
    }
    

    sendMessageInTransaction方法中,主要有:

    • 調用Validators.checkMessage(msg, this.defaultMQProducer)校驗事務消息的合法性

    • 對消息設置PROPERTY_TRANSACTION_PREPAREDPROPERTY_PRODUCER_GROUP屬性,前者用於判斷該消息是prepare消息,後者主要在回查時需要用到。

    • 調用DefaultMQProducerImpl的send方法進行發送。

  2. send方法以同步方式調用sendDefaultImpl方法

  3. sendDefaultImpl方法的作用主要用:

    • 獲取對應的Topic的路由

    • 輪詢獲取需要發送的隊列(在3.2.6版本中這裏輪詢有個Bug~,輪詢次數超過Integer.MAX時會開始報錯)

    • 調用sendKernelImpl進行消息發送

  4. sendKernelImpl方法主要設置了消息的TRANSACTION_PREPARED_TYPE標誌以及調用MQClientAPIImpl的sendMessage方法

  5. 最終會調用到通信層的RemotingClient類進行消息的發送,並接收broker的響應

  6. 收到響應後返回到sendMessageInTransaction方法中執行後序的邏輯:

    • 判斷響應狀態,如果是SNED_OK,就執行 transactionListener.executeLocalTransaction(msg, arg)方法來執行本地事務邏輯
    • 如果是其他狀態,對該消息進行回滾:返回RALLBACK狀態
    • 構造TransactionSendResult對象並返回。
  7. 在第6步中返回TransactionSendResult之前,會調用this.endTransaction(sendResult, localTransactionState, localException)方法,該方法的作用就是向broker返回本地事務狀態。

2. Broker處理prepare消息

在這裏插入圖片描述

  1. NettyServerHandler類的processMessageReceived 方法是所有broker請求的入口,該方法會調用NettyRemotingAbstract方法的processMessageReceived方法。
  2. NettyRemotingAbstract的processMessageReceived通過命令模式根據cmd的code獲取到對應的processor進行請求的處理,事務prepare消息對應的processor是SendMessageProcessor
  3. SendMessageProcessor的processRequest方法會根據判斷是否是批量消息,事務的prepare消息是單條的,調用其sendMessage方法,該方法中有一塊單獨處理事務消息的邏輯:
//判斷是否是事務消息 如果是事務消息則用事務消息的邏輯處理
String traFlag = oriProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (traFlag != null && Boolean.parseBoolean(traFlag)) {
    if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) {
        response.setCode(ResponseCode.NO_PERMISSION);
        response.setRemark(
            "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
                + "] sending transaction message is forbidden");
        return response;
    }
    putMessageResult = this.brokerController.getTransactionalMessageService().prepareMessage(msgInner);
} else {
    putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
}

4.對於prepare消息會調用TransactionMessageBridge的putHalfMessage方法,該方法調用parseHalfMessageInner對prepare消息進行轉換並在轉換後進行消息的存儲:

public PutMessageResult putHalfMessage(MessageExtBrokerInner messageInner) {
    return store.putMessage(parseHalfMessageInner(messageInner));
}
/**
 * 將消息進行轉換,最終將消息存儲到統一處理事務的Topic中:RMQ_SYS_TRANS_HALF_TOPIC
 * @return 轉換後的消息
 */
private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
    //將消息所屬真正Topic存儲到消息的properties中
    MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
    //將消息應該寫的queue存儲到消息的properties中
    MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
        String.valueOf(msgInner.getQueueId()));
    //設置事務消息標誌:Unknow,因爲現在還沒有接收到該事務消息的狀態
    msgInner.setSysFlag(
        MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
    //設置消息存儲到的Topic:統一事務消息Topic:RMQ_SYS_TRANS_HALF_TOPIC
    msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
    //所有事務消息存放在該Topic的第一個隊列裏
    msgInner.setQueueId(0);
    //將其餘該消息的屬性統一存放進來
  msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
    return msgInner;
}

5.可以看到所有的prepare消息都是存儲在一個Topic中的一個隊列裏,該Topic就是上面的Half Topic,最後會對消息進行存儲邏輯的操作,並調用handlePutMessageResult構造返回結果返回給生產者。

3.Broker結束事務消息

在這裏插入圖片描述

  1. 生產者在發送prepare消息後—>執行本地事務邏輯—>broker接收請求結束本次事務狀態:Broker在接收請求後根據命令會執行EndTransactionProcessorprocessRequest方法,該方法中下面的邏輯是真正處理事務消息狀態的:

    OperationResult result = new OperationResult();
    if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
        // 獲取Half Topic中的prepare消息
        result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
        if (result.getResponseCode() == ResponseCode.SUCCESS) {
            // 校驗消息是否正確:Half中的該消息是不是真正的本次請求處理的消息
            RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
            if (res.getCode() == ResponseCode.SUCCESS) {
                // 將prepare消息轉換爲原消息,該消息的Topic就是真正消息的Topic
                MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
                msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));
                msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());
                msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());
                msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());
                //將消息發送到真正的Topic裏,該消息可以開始下發給消費者
                RemotingCommand sendResult = sendFinalMessage(msgInner);
                if (sendResult.getCode() == ResponseCode.SUCCESS) {
                    //將消息放入Op Half Topic
                    this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
                }
                return sendResult;
            }
            return res;
        }
    } else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
        //同commitMessage方法一樣,返回真正的操作的消息:將Half Topic中的該消息還原爲原消息
        result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);
        if (result.getResponseCode() == ResponseCode.SUCCESS) {
            RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
            if (res.getCode() == ResponseCode.SUCCESS) {
                //將消息放入Op Half Topic
                this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
            }
            return res;
        }
    }
    
  2. 該方法會判斷本次事務的最終狀態,如果是Commit:

    • 獲取Half Topic中的消息
    • 將該消息轉換爲原消息
    • 將消息寫入到真正的Topic裏,這裏是事務消息的真正落盤,從而消息可以被消費者消費到
    • 如果落盤成功,則刪除prepare消息,其實是將消息寫入到Op Topic裏,該消息的內容就是這條消息在Half Topic隊列裏的offset,原因見後面的分析
  3. 如果是Rollback,則直接將消息轉換爲原消息,並寫入到Op Topic

4.事務消息是如何處理回查的

在RocketMQ中,消息都是順序寫隨機讀的,以offset來記錄消息的存儲位置與消費位置,所以對於事務消息的prepare消息來說,不可能做到物理刪除,broker啓動時每間隔60s會開始檢查一下有哪些prepare消息需要回查,從上面的分析我們知道,所有prepare消息都存儲在Half Topic中,那麼如何從該Topic中取出需要回查的消息進行回查呢?這就需要Op Half Topic以及一個內部的消費進度計算出需要回查的prepare消息進行回查:

  • Half Topic 默認Topic是RMQ_SYS_TRANS_HALF_TOPIC,建一個隊列,存儲所有的prepare消息
  • Op Half Topic默認是RMQ_SYS_TRANS_OP_HALF_TOPIC,建立的對列數與Half Topic相同,存儲所有已經確定狀態的prepare消息(rollback與commit狀態),消息內容是該條消息在Half Topic的Offset
  • Half Topic消費進度,默認消費者是CID_RMQ_SYS_TRANS,每次取prepare消息判斷回查時,從該消費進度開始依次獲取消息。
  • Op Half Topic消費進度,默認消費者是CID_RMQ_SYS_TRANS,每次獲取prepare消息都需要判斷是否在Op Topic中已存在該消息了,若存在表示該prepare消息已結束流程,不需要再進行事務回查,每次判斷都是從Op Topic中獲取一定消息數量出來進行對比的,獲取的消息就是從Op Topic中該消費進度開始獲取的,最大一次獲取32條。

下面看一下大致的時間線:

在這裏插入圖片描述

  1. broker在啓動時會啓動線程回查的服務,在TransactionMessageCheckServicerun方法中,該方法會執行到onWaitEnd方法:
@Override
protected void onWaitEnd() {
    //獲取超時時間 6s
    long timeout = brokerController.getBrokerConfig().getTransactionTimeOut();
    //獲取最大檢測次數 15次
    int checkMax = brokerController.getBrokerConfig().getTransactionCheckMax();
    //獲取當前時間
    long begin = System.currentTimeMillis();
    log.info("Begin to check prepare message, begin time:{}", begin);
    //開始檢測
    this.brokerController.getTransactionalMessageService().check(timeout, checkMax, this.brokerController.getTransactionalMessageCheckListener());
    log.info("End to check prepare message, consumed time:{}", System.currentTimeMillis() - begin);
}
  1. 該方法的最後會執行到TransactionMessageServiceImplcheck方法,該方法就是真正執行事務回查檢測的方法,該方法的主要作用就是計算出需要回查的prepare消息進行事務回查,大致邏輯是:

    • 獲取Half Topic的所有隊列,循環隊列開始檢測需要獲取的prepare消息,實際上Half Topic只有一個隊列。
    • 獲取Half TopicOp Half Topic的消費進度。
    • 調用fillOpRemoveMap方法,獲取Op Half Topic中已完成的prepare事務消息。
    • Half Topic中當前消費進度依次獲取消息,與第3步獲取的已結束的prepare消息進行對比,判斷是否進行回查:
    • 如果Op消息中包含該消息,則不進行回查,
    • 如果不包含,獲取Half Topic中的該消息,判斷寫入時間是否符合回查條件,若是新消息則不處理下次處理,並將消息重新寫入Half Topic,判斷回查次數是否小於15次,寫入時間是否小於72h,如果不滿足就丟棄消息,若滿足則更新回查次數,並將消息重新寫入Half Topic並進行事務回查,
    • 在循環完後重新更新Half TopicOp Half Topic中的消費進度,下次判斷回查邏輯時,將從最新的消費進度獲取信息。
  2. 生產客戶端的ClientRemotingProcessorprocessRequest方法會處理服務端的CHECK_TRANSACTION_STATE請求,最後會調用checkLocalTransactionState方法,該方法就是業務方可以自己實現事務消息回查邏輯的地方,並將結果最後用endTransactionOneway方法返回給Broker,該執行邏輯可以通過ClientRemotingProcessor的方法processRequest依次理解就可以了。

我們有哪些手段來監控事務消息的狀態

通過上面的文章,可以大致瞭解事務消息的實現,我們可以知道,事務消息主要有三個狀態:

  • UNKNOW狀態:表示事務消息未確定,可能是業務方執行本地事務邏輯時間耗時過長或者網絡原因等引起的,該狀態會導致broker對事務消息進行回查,默認回查總次數是15次,第一次回查間隔時間是6s,後續每次間隔60s,
  • ROLLBACK狀態,該狀態表示該事務消息被回滾,因爲本地事務邏輯執行失敗導致
  • COMMIT狀態,表示事務消息被提交,會被正確分發給消費者。

那麼監控事務消息時,主要是查看該事務消息是否是處於我們想要的狀態,而在事務消息生產者發送prepare消息成功後只能拿到一個transactionId,該id不是的RocketMQ消息存儲的物理offset地址,RocketMQ只有在準備寫入commitlog文件時纔會生成真正的msgId,而這裏可以獲取的transactionId和msgId都是客戶端生成的一個消息的唯一標識符,我們在這裏稱爲uniqId,在broker端,會把該uniqId作爲一個msgKey寫入消息,所以可以通過該uniqId來查找uniqId的一些狀態:

  1. 通過DefaultMQAdminExtviewMessage(String topic, String msgId)方法可以消息的信息,這裏topic參數是RMQ_SYS_TRANS_HALF_TOPIC ,該topic是真正的Half Topic,msgId傳發送prepare消息獲取的uniqId,這樣可以獲取prepare消息在Half Topic真正的offsetMsgId,
  2. 通過第一步獲取的offsetMsgId繼續調用viewMessage(String topic, String msgId)方法,但是topic是RMQ_SYS_TRANS_OP_HALF_TOPIC,這樣可以獲取Op Half Topic中該事務消息的狀態,如果存在說明prepare消息已處理,否則可能仍在回查中或已被丟棄
  3. 如果在第二步查到了信息可以用uniqId和事務消息真正Topic繼續調用viewMessage(String topic, String msgId)方法獲取消息真正的信息,如果存在說明消息已被投遞,否則該事務消息已被回滾。只通過Op Half Topic是不能確定消息狀態的,這裏的sysFlag被設置0,sysFlag是用於確定事務消息狀態。

通過上述三步就可以確定事務消息的狀態。

事務消息的異常恢復機制

事務消息的異常狀態主要有:

  1. 生產者提交prepare消息到broker成功,但是當前生產者實例宕機了
  2. 生產者提交prepare消息到broker失敗,可能是因爲提交的broker已宕機
  3. 生產者提交prepare消息到broker成功,執行本地事務邏輯成功,但是broker宕機了未確定事務狀態
  4. 生產提交prepare消息到broker成功,但是在進行事務回查的過程中broker宕機了,未確定事務狀態

對於1:事務消息會根據producerGroup搜尋其他的生產者實例進行回查,所以transactionId務必保存在中央存儲中,並且事務消息的pid不能跟其他消息的pid混用。

對於2:當前實例會搜尋其他的可用的broker-master進行提交,因爲只有提交prepare消息後纔會執行本地事務,所以沒有影響,注意生產者報的是超時異常時,是不會進行重發的。

對於3:因爲返回狀態是oneway方式,此時如果消費者未收到消息,需要用手段確定該事務消息的狀態,儘快將broker重啓,broker重啓後會通過回查完成事務消息。

對於4:同3,儘快重啓broker。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章