RocketMq之事務消息實現原理

RocketMQ提供了事務消息的功能,採用2PC(兩段式協議)+補償機制(事務回查)的分佈式事務功能,通過消息隊列 RocketMQ 版事務消息能達到分佈式事務的最終一致。

概覽

  • 半事務消息:

    暫不能投遞的消息,發送方已經成功地將消息發送到了消息隊列 RocketMQ 版服務端,但是服務端未收到生產者對該消息的二次確認,此時該消息被標記成“暫不能投遞”狀態,處於該種狀態下的消息即半事務消息。

  • 消息回查:

    由於網絡閃斷、生產者應用重啓等原因,導致某條事務消息的二次確認丟失,消息隊列 RocketMQ 版服務端通過掃描發現某條消息長期處於“半事務消息”時,需要主動向消息生產者詢問該消息的最終狀態(Commit 或是 Rollback),該詢問過程即消息回查。

交互流程

 

 

 

事務消息發送步驟如下:

  1. 發送方將半事務消息發送至消息隊列 RocketMQ 版服務端。
  2. 消息隊列 RocketMQ 版服務端將消息持久化成功之後,向發送方返回 Ack 確認消息已經發送成功,此時消息爲半事務消息。
  3. 發送方開始執行本地事務邏輯。
  4. 發送方根據本地事務執行結果向服務端提交二次確認(Commit 或是 Rollback),服務端收到 Commit 狀態則將半事務消息標記爲可投遞,訂閱方最終將收到該消息;服務端收到 Rollback 狀態則刪除半事務消息,訂閱方將不會接受該消息。

事務消息回查步驟如下:

  1. 在斷網或者是應用重啓的特殊情況下,上述步驟 4 提交的二次確認最終未到達服務端,經過固定時間後服務端將對該消息發起消息回查。
  2. 發送方收到消息回查後,需要檢查對應消息的本地事務執行的最終結果。
  3. 發送方根據檢查得到的本地事務的最終狀態再次提交二次確認,服務端仍按照步驟 4 對半事務消息進行操作。

總體而言RocketMQ事務消息分爲兩條主線

  • 發送流程:發送half message(半消息),執行本地事務,發送事務執行結果

  • 定時任務回查流程:MQ定時任務掃描半消息,回查本地事務,發送事務執行結果

源碼分析

Producer是如何發送事務半消息的(prepare)

在本地應用發送事務消息的核心類是TransactionMQProducer,該類通過繼承DefaultMQProducer來複用大部分發送消息相關的邏輯,這個類的代碼量非常少只有100來行,下面是這個類的sendMessageTransaction方法

@Override
public TransactionSendResult sendMessageInTransaction(final Message msg,
    final Object arg) throws MQClientException {
    //判斷transactionListener是否存在
    if (null == this.transactionListener) {
        throw new MQClientException("TransactionListener is null", null);
    }
    //發送事務消息
    return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg);
}

這裏的transactionListener就是上面所說的消息回查的類,它提供了2個方法:

  • executeLocalTransaction

    執行本地事務

  • checkLocalTransaction

    回查本地事務

接着看DefaultMQProducer.sendMessageInTransaction()方法:

public TransactionSendResult sendMessageInTransaction(final Message msg,
        final LocalTransactionExecuter localTransactionExecuter, final Object arg)
        throws MQClientException {
        //判斷檢查本地事務的listenner是否存在
        TransactionListener transactionListener = getCheckListener();
        if (null == localTransactionExecuter && null == transactionListener) {
            throw new MQClientException("tranExecutor is null", null);
        }

        //。。。省略

        SendResult sendResult = null;
        //msg設置參數TRAN_MSG,表示爲事務消息
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
        try {
            //發送消息
            sendResult = this.send(msg);
        } catch (Exception e) {
            throw new MQClientException("send message Exception", e);
        }

        LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
        Throwable localException = null;
        switch (sendResult.getSendStatus()) {
            case SEND_OK: {
                try {
                    if (sendResult.getTransactionId() != null) {
                        msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
                    }
                    String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
                    if (null != transactionId && !"".equals(transactionId)) {
                        msg.setTransactionId(transactionId);
                    }
                    if (null != localTransactionExecuter) {
                        localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
                    } else if (transactionListener != null) {
                        log.debug("Used new transaction API");
                        //發送消息成功,執行本地事務
                        localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
                    }
                    if (null == localTransactionState) {
                        localTransactionState = LocalTransactionState.UNKNOW;
                    }

                    if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
                        log.info("executeLocalTransactionBranch return {}", localTransactionState);
                        log.info(msg.toString());
                    }
                } catch (Throwable e) {
                    log.info("executeLocalTransactionBranch exception", e);
                    log.info(msg.toString());
                    localException = e;
                }
            }
            break;
            case FLUSH_DISK_TIMEOUT:
            case FLUSH_SLAVE_TIMEOUT:
            case SLAVE_NOT_AVAILABLE:
                localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
                break;
            default:
                break;
        }

        try {
            //執行endTransaction方法,如果半消息發送失敗或本地事務執行失敗告訴服務端是刪除半消息,半消息發送成功且本地事務執行成功則告訴服務端生效半消息
            this.endTransaction(sendResult, localTransactionState, localException);
        } catch (Exception e) {
            log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
        }

        //...省略
        return transactionSendResult;
    }

該方法主要做了以下事情

  • 給消息打上事務消息相關的tag,用於broker區分普通消息和事務消息

  • 發送半消息(half message)

  • 發送成功則由transactionListener執行本地事務

  • 執行endTransaction方法,告訴 broker 執行 commit/rollback。

Broker端是如何處理事務消息的

Broker端通過SendMessageProcessor.processRequest()方法接收處理 Producer 發送的消息 最後會調用到SendMessageProcessor.sendMessage(),判斷消息類型,進行消息存儲。

//SendMessageProcessor.java

private RemotingCommand sendMessage(final ChannelHandlerContext ctx,
                                        final RemotingCommand request,
                                        final SendMessageContext sendMessageContext,
                                        final SendMessageRequestHeader requestHeader) throws RemotingCommandException {
    //...省略
    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);
    }

接着看存儲半消息的代碼 prepareMessage(msgInner) :

//TransactionalMessageBridge.java

//存儲事務半消息
    public PutMessageResult putHalfMessage(MessageExtBrokerInner messageInner) {
        return store.putMessage(parseHalfMessageInner(messageInner));
    }

    private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
        //備份消息的原主題名稱與原隊列ID
        MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
        MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
            String.valueOf(msgInner.getQueueId()));
        msgInner.setSysFlag(
            MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
        //事務消息的topic和queueID是寫死固定的
        msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
        msgInner.setQueueId(0);
        msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
        return msgInner;
    }

在這一步,備份消息的原主題名稱與原隊列ID,然後取消事務消息的消息標籤,重新設置消息的主題爲:RMQ_SYS_TRANS_HALF_TOPIC,隊列ID固定爲0。與其他普通消息區分開,然後完成消息持久化。
到這裏,Broker 就初步處理完了 Producer 發送的事務半消息。

執行本地事務

接着我們回到 上面 Producer 發送半消息的地方,往下繼續看。

 switch (sendResult.getSendStatus()) {
            case SEND_OK: {
                try {
                    if (sendResult.getTransactionId() != null) {
                        msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
                    }
                    String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
                    if (null != transactionId && !"".equals(transactionId)) {
                        msg.setTransactionId(transactionId);
                    }
                    if (null != localTransactionExecuter) {
                        localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
                    } else if (transactionListener != null) {
                        log.debug("Used new transaction API");
                        //發送消息成功,執行本地事務
                        localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
                    }
                    if (null == localTransactionState) {
                        localTransactionState = LocalTransactionState.UNKNOW;
                    }

                    if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
                        log.info("executeLocalTransactionBranch return {}", localTransactionState);
                        log.info(msg.toString());
                    }
                } catch (Throwable e) {
                    log.info("executeLocalTransactionBranch exception", e);
                    log.info(msg.toString());
                    localException = e;
                }
            }
            break;
            case FLUSH_DISK_TIMEOUT:
            case FLUSH_SLAVE_TIMEOUT:
            //半消息發送失敗,回滾
            case SLAVE_NOT_AVAILABLE:
                localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
                break;
            default:
                break;
        }

事務半消息發送成功後,會調用transactionListener.executeLocalTransaction方法執行本地事務。只有半消息發送成功後,纔會執行本地事務,如果半消息發送失敗,則設置回滾。

結束事務(commit/rollback)

本地事務執行後,則調用this.endTransaction()方法,根據本地事務執行狀態,去提交事務或者回滾事務。
如果半消息發送失敗或本地事務執行失敗告訴服務端是刪除半消息,半消息發送成功且本地事務執行成功則告訴服務端生效半消息

public void endTransaction(
        final SendResult sendResult,
        final LocalTransactionState localTransactionState,
        final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException {
        final MessageId id;
        if (sendResult.getOffsetMsgId() != null) {
            id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId());
        } else {
            id = MessageDecoder.decodeMessageId(sendResult.getMsgId());
        }
        String transactionId = sendResult.getTransactionId();
        final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName());
        EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader();
        requestHeader.setTransactionId(transactionId);
        requestHeader.setCommitLogOffset(id.getOffset());
        switch (localTransactionState) {
            //提交事務
            case COMMIT_MESSAGE:
                requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
                break;
            //回滾
            case ROLLBACK_MESSAGE:
                requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
                break;
            case UNKNOW:
                requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
                break;
            default:
                break;
        }

        requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
        requestHeader.setTranStateTableOffset(sendResult.getQueueOffset());
        requestHeader.setMsgId(sendResult.getMsgId());
        String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null;
        this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark,
            this.defaultMQProducer.getSendMsgTimeout());
    }

半消息事務回查

兩段式協議發送與提交回滾消息,執行完本地事務消息的狀態爲UNKNOW時,結束事務不做任何操作。通過事務狀態定時回查得到發送端的事務狀態是rollback或commit。
通過TransactionalMessageCheckService線程定時去檢測RMQ_SYS_TRANS_HALF_TOPIC主題中的消息,回查消息的事務狀態。

  • RMQ_SYS_TRANS_HALF_TOPIC

    prepare消息的主題,事務消息首先先進入到該主題。

  • RMQ_SYS_TRANS_OP_HALF_TOPIC

    當消息服務器收到事務消息的提交或回滾請求後,會將消息存儲在該主題下。

代碼入口:

public void run() {
        log.info("Start transaction check service thread!");
        //執行間隔
        long checkInterval = brokerController.getBrokerConfig().getTransactionCheckInterval();
        while (!this.isStopped()) {
            this.waitForRunning(checkInterval);
        }
        log.info("End transaction check service thread!");
    }

    @Override
    protected void onWaitEnd() {
        //事務過期時間
        long timeout = brokerController.getBrokerConfig().getTransactionTimeOut();
        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);
    }

大致流程如下:

 

 

這裏重點說下判斷消息是否要回查的邏輯:

 

 

 

removeMap是個Map集合的鍵值對key是half隊列的消息offset,value是op隊列的消息offset,圖中看有兩對(100005,80002)、(100004,80003)

doneOpOffset是一個List集合,其中存儲的是op隊列的消息offset,圖中只有8004

check()循環查找half隊列中的消息時,100004已經在removeMap中了,跳過下面業務繼續循環下一個100005進行下一個邏輯,判斷其是否具有回查消息的條件isNeedCheck

Broker處理END_TRANSACTION

接下來我們來一起看看,當Producer或者回查定時任務提交/回滾事務的時候,Broker如何處理事務消息提交、回滾命令的。

//EndTransactionProcessor.java

public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws
        RemotingCommandException {
        final RemotingCommand response = RemotingCommand.createResponseCommand(null);
        final EndTransactionRequestHeader requestHeader =
            (EndTransactionRequestHeader)request.decodeCommandCustomHeader(EndTransactionRequestHeader.class);
        LOGGER.debug("Transaction request:{}", requestHeader);
        //從節點不處理
        if (BrokerRole.SLAVE == brokerController.getMessageStoreConfig().getBrokerRole()) {
            response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE);
            LOGGER.warn("Message store is slave mode, so end transaction is forbidden. ");
            return response;
        }

        //省略代碼,打印日誌
        
        OperationResult result = new OperationResult();
        //如果請求爲提交事務,進入事務消息提交處理流程
        if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
            //根據commitLogOffset從commitlog文件中查找消息
            result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
            if (result.getResponseCode() == ResponseCode.SUCCESS) {
                //字段檢查
                RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
                if (res.getCode() == ResponseCode.SUCCESS) {
                    //恢復事務消息的真實的主題、隊列,並設置事務ID
                    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());
                    MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED);
                    //發送最終消息,存儲,被consumer消費
                    RemotingCommand sendResult = sendFinalMessage(msgInner);
                    if (sendResult.getCode() == ResponseCode.SUCCESS) {
                        //刪除預處理消息(prepare)
                        //其實是將消息存儲在主題爲:RMQ_SYS_TRANS_OP_HALF_TOPIC的主題中,代表這些消息已經被處理(提交或回滾)。
                        this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
                    }
                    return sendResult;
                }
                return res;
            }
        } //回滾處理
        else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
            //根據commitlogOffset查找消息
            result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);
            if (result.getResponseCode() == ResponseCode.SUCCESS) {
                //字段檢查
                RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
                if (res.getCode() == ResponseCode.SUCCESS) {
                    //刪除預處理消息(prepare)
                    //將消息存儲在RMQ_SYS_TRANS_OP_HALF_TOPIC中,代表該消息已被處理
                    this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
                }
                return res;
            }
        }
        response.setCode(result.getResponseCode());
        response.setRemark(result.getResponseRemark());
        return response;
    }

這裏的邏輯很清晰,其核心實現如下:

  • 根據commitlogOffset找到消息
  • 如果是提交動作,就恢復原消息的主題與隊列,再次存入commitlog文件進而轉到消息消費隊列,供消費者消費,然後將原預處理消息存入一個新的主題RMQ_SYS_TRANS_OP_HALF_TOPIC,代表該消息已被處理
  • 回滾消息,則直接將原預處理消息存入一個新的主題RMQ_SYS_TRANS_OP_HALF_TOPIC,代表該消息已被處理

整體實現流程

 

 

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