RocketMQ生產消息源碼解析 從UT看Producer API 啓動過程 啓動過程的實現: 消息發送過程 sendKernelImpl() 總結 面試場景快問快答

基於最新的release-4.7.1代碼分析。

  • 客戶端是個單獨模塊,在rocketmq/client


從UT看Producer API

閱讀源碼,不推薦從入口開始看到底,畢竟你也看不到底。而應該帶着問題分析源碼:Producer是如何發消息的。

推薦從UT單元測試用例入手。因爲UT用例都是測試代碼中的一個小流程。
規範的開源框架,單元測試覆蓋率都很高,很容易找到我們所需流程所對應的用例。因此從這些用例入手,調試跟蹤調用棧,看清關鍵代碼流程。

先分析一下RocketMQ客戶端的單元測試,看看Producer API應該如何使用。

Producer的所有測試用例都在同個測試類 DefaultMQProducerTest,可以瞭解到Producer的主要功能。

  • 主要測試用例



    init和terminate是測試開始初始化和測試結束銷燬時需要執行的代碼
    testXXX方法都是各種場景發消息的測試用例

  • Producer相關的核心類和接口


  • 門面模式(Facade Pattern)
    給客戶端提供了一個可以訪問系統的接口,隱藏系統內部的複雜性。

接口MQProducer就是門面,客戶端只要使用這個接口就可以訪問Producer實現消息發送的相關功能,使用上不必再與其他複雜實現類打交道。

類DefaultMQProducer實現了接口MQProducer,方法實現大多沒有業務邏輯,只是封裝對其他實現類的方法調用,也可視爲是門面。
Producer大部分業務邏輯實現都在類DefaultMQProducerImpl。

有時實現分散在很多內部類,不方便用接口來對外提供服務,就可仿照RocketMQ,使用門面模式隱藏內部實現,對外提供服務。

  • 接口MQAdmin定義了一些元數據管理的方法,在消息發送過程會用到。


啓動過程

通過單元測試中的代碼可以看到,在init()和terminate()這兩個測試方法中,分別執行了Producer的start和shutdown方法。
說明RocketMQ Producer是個有狀態服務,在發送消息前需要先啓動Producer。這個啓動過程,實際上就是爲了發消息做的準備工作,所以,在分析發消息流程之前,我們需要先理清Producer中維護了哪些狀態,在啓動過程中,Producer都做了哪些初始化的工作。有了這個基礎才能分析其發消息的實現流程。

init()

  • 創建DefaultMQProducer實例
  • 設置一些參數值
  • 然後調用start。

跟進start方法的實現,繼續分析其初始化過程。

  • DefaultMQProducer#start()直接調用DefaultMQProducerImpl#start()




RocketMQ使用一個成員變量serviceState來記錄和管理自身的服務狀態,這實際上是(State Pattern)設計模式的變體。
狀態模式允許一個對象在其內部狀態改變時改變它的行爲,對象看起來就像是改變了它的類。
與標準的狀態模式不同的是,它沒有使用狀態子類,而是使用分支流程(switch-case)來實現不同狀態下的不同行爲,在管理比較簡單的狀態時,使用這種設計會讓代碼更加簡潔。這種模式非常廣泛地用於管理有狀態的類,推薦你在日常開發中使用。

在設計狀態的時候,有兩個要點是需要注意的

  1. 不僅要設計正常的狀態,還要設計中間狀態和異常狀態,否則,一旦系統出現異常,你的狀態就不準確了,很難處理這種異常狀態。比如在這段代碼中,RUNNING和SHUTDOWN_ALREADY是正常狀態,CREATE_JUST是一箇中間狀態,START_FAILED是一個異常狀態。
  2. 這些狀態之間的轉換路徑考慮清楚,並在進行狀態轉換的時候,檢查上一個狀態是否能轉換到下一個狀態。
    比如這裏,只有處於CREATE_JUST態才能轉爲RUNNING狀,可確保這服務一次性,只能啓動一次。避免了多次啓動服務。

啓動過程的實現:

  1. 通過一個單例模式(Singleton Pattern)的MQClientManager獲取MQClientInstance的實例mQClientFactory,沒有則自動創建新的實例
  2. 在mQClientFactory中註冊自己
  3. 啓動mQClientFactory
  4. 給所有Broker發送心跳。

其中實例mQClientFactory對應的類MQClientInstance是RocketMQ客戶端中的頂層類,大多數情況下,可以簡單地理解爲每個客戶端對應類MQClientInstance的一個實例。這個實例維護着客戶端的大部分狀態信息,以及所有的Producer、Consumer和各種服務的實例,想要學習客戶端整體結構的同學可以從分析這個類入手,逐步細化分析下去。

我們進一步分析一下MQClientInstance#start()中的代碼:


  • DefaultMQProducerImpl:Producer的內部實現類,大部分Producer的業務邏輯,也就是發消息的邏輯,都在這類。


  • MQClientInstance:封裝了客戶端一些通用的業務邏輯,無論是Producer還是Consumer,最終需要與服務端交互時,都需要調用這個類中的方法

  • MQClientAPIImpl:這個類中封裝了客戶端服務端的RPC,對調用者隱藏了真正網絡通信部分的具體實現

  • NettyRemotingClient:RocketMQ各進程之間網絡通信的底層實現類


消息發送過程

接下來我們一起分析Producer發送消息的流程。

在Producer的接口MQProducer中,定義了19個不同參數的發消息的方法,按照發送方式不同可以分成三類:

單向發送(Oneway):發送消息後立即返回,不處理響應,不關心是否發送成功;
同步發送(Sync):發送消息後等待響應;
異步發送(Async):發送消息後立即返回,在提供的回調方法中處理響應。
這三類發送實現基本上是相同的,異步發送稍微有一點兒區別,我們看一下異步發送的實現方法"DefaultMQProducerImpl#send()"(對應源碼中的1132行):

@Deprecated
public void send(final Message msg, final MessageQueueSelector selector, final Object arg, final SendCallback sendCallback, final long timeout)
    throws MQClientException, RemotingException, InterruptedException {
    final long beginStartTime = System.currentTimeMillis();
    ExecutorService executor = this.getAsyncSenderExecutor();
    try {
        executor.submit(new Runnable() {
            @Override
            public void run() {
                long costTime = System.currentTimeMillis() - beginStartTime;
                if (timeout > costTime) {
                    try {
                        try {
                            sendSelectImpl(msg, selector, arg, CommunicationMode.ASYNC, sendCallback,
                                timeout - costTime);
                        } catch (MQBrokerException e) {
                            throw new MQClientException("unknownn exception", e);
                        }
                    } catch (Exception e) {
                        sendCallback.onException(e);
                    }
                } else {
                    sendCallback.onException(new RemotingTooMuchRequestException("call timeout"));
                }
            }

        });
    } catch (RejectedExecutionException e) {
        throw new MQClientException("exector rejected ", e);
    }
}

我們可以看到,RocketMQ使用了一個ExecutorService來實現異步發送:使用asyncSenderExecutor的線程池,異步調用方法sendSelectImpl(),繼續發送消息的後續工作,當前線程把發送任務提交給asyncSenderExecutor就可以返回了。單向發送和同步發送的實現則是直接在當前線程中調用方法sendSelectImpl()。

我們來繼續看方法sendSelectImpl()的實現:

// 省略部分代碼
MessageQueue mq = null;

// 選擇將消息發送到哪個隊列(Queue)中
try {
    List<MessageQueue> messageQueueList =
        mQClientFactory.getMQAdminImpl().parsePublishMessageQueues(topicPublishInfo.getMessageQueueList());
    Message userMessage = MessageAccessor.cloneMessage(msg);
    String userTopic = NamespaceUtil.withoutNamespace(userMessage.getTopic(), mQClientFactory.getClientConfig().getNamespace());
    userMessage.setTopic(userTopic);

    mq = mQClientFactory.getClientConfig().queueWithNamespace(selector.select(messageQueueList, userMessage, arg));
} catch (Throwable e) {
    throw new MQClientException("select message queue throwed exception.", e);
}

// 省略部分代碼

// 發送消息
if (mq != null) {
    return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, null, timeout - costTime);
} else {
    throw new MQClientException("select message queue return null.", null);
}
// 省略部分代碼

方法sendSelectImpl()中主要的功能就是選定要發送的隊列,然後調用方法sendKernelImpl()發送消息。

選擇哪個隊列發送由MessageQueueSelector#select決定。
RocketMQ使用策略模式解決不同場景下需要使用不同隊列選擇算法問題。

RocketMQ提供了很多MessageQueueSelector的實現,例如隨機選擇策略,哈希選擇策略和同機房選擇策略等


也可以自己實現選擇策略。如果要保證相同key消息的嚴格順序,你需要使用哈希選擇策略,或提供一個自己實現的選擇策略。

再看方法

sendKernelImpl()

構建發送消息的

  • 請求頭部 RequestHeader


  • 上下文SendMessageContext


然後調用方法MQClientAPIImpl#sendMessage(),將消息發送給隊列所在Broker。


至此,消息被髮送給遠程調用的封裝類MQClientAPIImpl,完成後續序列化和網絡傳輸等步驟。

RocketMQ的Producer無論同步還是異步發送消息,都統一到了同一流程。
異步發送消息的實現,也是通過一個線程池,在異步線程執行的調用和同步發送相同的底層方法來實現的。

  • 方法的一個參數區分同步or異步發送
    這使得整個流程統一,很多同步異步代碼可複用,代碼結構清晰簡單,易維護。

使用同步發送,當前線程會阻塞等待服務端的響應,直到收到響應或者超時方法纔會返回,所以在業務代碼調用同步發送的時候,只要返回成功,消息就一定發送成功了。
而異步發送,發送的邏輯都是在Executor的異步線程中執行的,所以不會阻塞當前線程,當服務端返回響應或者超時之後,Producer會調用Callback方法來給業務代碼返回結果。業務代碼需要在Callback中來判斷髮送結果。

總結

本文分析了RocketMQ客戶端消息生產的實現過程,包括Producer初始化和發送消息的主流程。Producer中包含的幾個核心的服務都是有狀態的,在Producer啓動時,由MQClientInstance類中來統一啓動。

在發送消息的流程中,RocketMQ分了三種發送方式:

  1. 單向
  2. 同步
  3. 異步

這三種發送方式對應的發送流程基本相同,同步和異步發送由已封裝好的MQClientAPIImpl類分別實現。

面試場景快問快答

  • DefaultMQProducer有個屬性defaultTopicQueueNums,它是用來設置topic的ConsumeQueue的數量的嗎?有同學可能認爲consumeQueue的數量是創建topic的時候指定的,跟producer沒有關係,那這參數有什麼用呢?
    這參數是控制客戶端在生產消費的時候會訪問同一個主題的隊列數量,假設一個主題有100個隊列,對每個客戶端,它沒必要100個隊列都訪問,只需使用其中幾個隊列。

  • 在RocketMq的控制檯上可以創建topic,需要指定writeQueueNums,readQueueNums,perm,這三個參數是有什麼用呢?這裏爲什麼要區分寫跟讀隊列呢?不應該只有一個consumeQueue?
    writeQueueNums和readQueueNums是在服務端來控制每個客戶端在生產和消費的時候,分別訪問多少個隊列。這兩參數是服務端參數,優先級高於客戶端控制的參數defaultTopicQueueNums的。perm是設置Topic讀寫等權限的參數。

  • 用戶請求-->異步處理--->用戶收到響應結果。異步處理的作用是:用更少的線程來接收更多的用戶請求,然後異步處理業務邏輯。異步處理完後,如何將結果通知給原先的用戶呢?即使有回調接口,我理解也是給用戶發個短信之類的處理,那結果怎麼返回到定位到用戶,並返回之前請求的頁面上呢?需要讓之前的請求線程阻塞嗎?那也無法達到【用更少的線程來接收更多的用戶請求】的目的丫。
    如果侷限於:“APP/瀏覽器 --[http協議]-->web 服務”這樣的場景,受限於http協議,前端和web服務的交互一定是單向和同步的。一定要等待結果然後返回響應,但是,這種情況仍然可以使用異步方法。像spring web這種框架,它把處理web請求都給你封裝好了,你只要寫個handler很方便。但這handler只能是一個同步方法,它必須在返回值中給出響應結果,所以導致很多同學思維轉不過來。

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