RocketMQ源碼分析:Broker概述+同步消息發送原理與高可用設計及思考 1、Broker概述 2、Broker存儲設計概要

1、Broker概述

Broker 在 RocketMQ 架構中的角色,就是存儲消息,核心任務就是持久化消息,生產者發送消息給 Broker,消費者從 Broker 消費消息,其物理部署架構圖如下:

備註:以上摘錄自官方 RocketMQ 設計文檔。

上述基本描述了消息中間件的架構設計,不僅限於 RocketMQ,不同消息中間件的最大區別之一在消息的存儲上。

2、Broker存儲設計概要

接下來從配置文件的角度來窺探 Broker 存儲設計的關注點,對應代碼(MessageStoreConfig)。

  • storePathRootDir
    設置Broker的存儲根目錄,默認爲 $Broker_Home/store。
  • storePathCommitLog
    設置commitlog的存儲目錄,默認爲$Broker_Home/store/commitlog。
  • mapedFileSizeCommitLog
    commitlog 文件的大小,默認爲1G。
  • mapedFileSizeConsumeQueue
    consumeQueueSize,ConsumeQueue 存放的是定長的信息(20個字節,偏移量、size、tagscode),默認30w * ConsumeQueue.CQ_STORE_UNIT_SIZE。
  • enableConsumeQueueExt
    是否開啓 consumeQueueExt,默認爲 false,就是如果消費端消息消費速度跟不上,是否創建一個擴展的 ConsumeQueue文件,如果不開啓,應該會阻塞從 commitlog 文件中獲取消息,並且 ConsumeQueue,應該是按topic獨立的。
  • mappedFileSizeConsumeQueueExt
    擴展consume文件的大小,默認爲48M。
  • flushIntervalCommitLog
    刷寫 CommitLog 的間隔時間,RocketMQ 後臺會啓動一個線程,將消息刷寫到磁盤,這個也就是該線程每次運行後等待的時間,默認爲500毫秒。flush 操作,調用文件通道的force()方法。
  • commitIntervalCommitLog
    提交消息到 CommitLog 對應的文件通道的間隔時間,原理與上面類似;將消息寫入到文件通道(調用FileChannel.write方法)得到最新的寫指針,默認爲200毫秒。
  • useReentrantLockWhenPutMessage
    在put message( 將消息按格式封裝成msg放入相關隊列時實用的鎖機制:自旋或ReentrantLock)。
  • flushIntervalConsumeQueue
    刷寫到ConsumeQueue的間隔,默認爲1s。
  • flushCommitLogLeastPages
    每次 flush commitlog 時最小發生變化的頁數。
  • commitCommitLogLeastPages
    每一次 commitlog 提交任務至少需要的頁數。
  • flushLeastPagesWhenWarmMapedFile
    用字節0填充整個文件,每多少頁刷盤一次,默認4096,異步刷盤模式生效。
  • flushConsumeQueueLeastPages
    一次刷盤至少需要的髒頁數量,默認爲2,針對 consuequeue 文件。
  • putMsgIndexHightWater
    當前版本未使用。

接下來從如下方面去深入其實現:

1)生產者發送消息

2)消息協議(格式)

3)消息存儲、檢索

4)消費隊列維護

5)消息消費、重試等機制

2.1 消息發送

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl sendDefaultImpl方法源碼分析
rprivate SendResult sendDefaultImpl(//
        Message msg, //    
        final CommunicationMode communicationMode, //
        final SendCallback sendCallback, //
        final long timeout//
    ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
 
}

2.1.1 消息發送參數詳解:

1、Message msg

2、communicationMode communicationMode

發送方式,SYNC(同步)、ASYNC(異步)、ONEWAY(單向,不關注返回)

3、SendCallback sendCallback

異步消息發送回調函數。

4、long timeout

消息發送超時時間。

2.2.2 消息發送流程

默認消息發送實現:

private SendResult sendDefaultImpl(//
        Message msg, //
        final CommunicationMode communicationMode, //
        final SendCallback sendCallback, //
        final long timeout//
    ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        this.makeSureStateOK();
        Validators.checkMessage(msg, this.defaultMQProducer);
        final long invokeID = random.nextLong();
        long beginTimestampFirst = System.currentTimeMillis();
        long beginTimestampPrev = beginTimestampFirst;
        long endTimestamp = beginTimestampFirst;
        TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic()); // @1
        if (topicPublishInfo != null && topicPublishInfo.ok()) {
            MessageQueue mq = null;
            Exception exception = null;
            SendResult sendResult = null;
            int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
            int times = 0;
            String[] brokersSent = new String[timesTotal];
            for (; times < timesTotal; times++) {
                String lastBrokerName = null == mq ? null : mq.getBrokerName();
                MessageQueue tmpmq = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName); // @2
                if (tmpmq != null) {
                    mq = tmpmq;
                    brokersSent[times] = mq.getBrokerName();
                    try {
                        beginTimestampPrev = System.currentTimeMillis();
                        sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout);  // @3
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                        switch (communicationMode) {
                            case ASYNC:
                                return null;
                            case ONEWAY:
                                return null;
                            case SYNC:
                                if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
                                    if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
                                        continue;
                                    }
                                }
                                return sendResult;
                            default:
                                break;
                        }
                    } catch (RemotingException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true); // @4
                        log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());
                        exception = e;
                        continue;
                    } catch (MQClientException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true); 
                        log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());
                        exception = e;
                        continue;
                    } catch (MQBrokerException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                        log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());
                        exception = e;
                        switch (e.getResponseCode()) {
                            case ResponseCode.TOPIC_NOT_EXIST:
                            case ResponseCode.SERVICE_NOT_AVAILABLE:
                            case ResponseCode.SYSTEM_ERROR:
                            case ResponseCode.NO_PERMISSION:
                            case ResponseCode.NO_BUYER_ID:
                            case ResponseCode.NOT_IN_CURRENT_UNIT:
                                continue;
                            default:
                                if (sendResult != null) {
                                    return sendResult;
                                }
                                throw e;
                        }
                    } catch (InterruptedException e) {
                        endTimestamp = System.currentTimeMillis();
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                        log.warn(String.format("sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                        log.warn(msg.toString());
                        log.warn("sendKernelImpl exception", e);
                        log.warn(msg.toString());
                        throw e;
                    }
                } else {
                    break;
                }
            }
            if (sendResult != null) {
                return sendResult;
            }
            String info = String.format("Send [%d] times, still failed, cost [%d]ms, Topic: %s, BrokersSent: %s",
                times,
                System.currentTimeMillis() - beginTimestampFirst,
                msg.getTopic(),
                Arrays.toString(brokersSent));
            info += FAQUrl.suggestTodo(FAQUrl.SEND_MSG_FAILED);
            MQClientException mqClientException = new MQClientException(info, exception);
            if (exception instanceof MQBrokerException) {
                mqClientException.setResponseCode(((MQBrokerException) exception).getResponseCode());
            } else if (exception instanceof RemotingConnectException) {
                mqClientException.setResponseCode(ClientErrorCode.CONNECT_BROKER_EXCEPTION);
            } else if (exception instanceof RemotingTimeoutException) {
                mqClientException.setResponseCode(ClientErrorCode.ACCESS_BROKER_TIMEOUT);
            } else if (exception instanceof MQClientException) {
                mqClientException.setResponseCode(ClientErrorCode.BROKER_NOT_EXIST_EXCEPTION);
            }
            throw mqClientException;
        }
        List<String> nsList = this.getmQClientFactory().getMQClientAPIImpl().getNameServerAddressList();
        if (null == nsList || nsList.isEmpty()) {
            throw new MQClientException(
                "No name server address, please set it." + FAQUrl.suggestTodo(FAQUrl.NAME_SERVER_ADDR_NOT_EXIST_URL), null).setResponseCode(ClientErrorCode.NO_NAME_SERVER_EXCEPTION);
        }
        throw new MQClientException("No route info of this topic, " + msg.getTopic() + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO),
            null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION);
    }

主要的核心步驟如下:

代碼@1:獲取topic的路由信息。

代碼@2:根據topic負載均衡算法選擇一個MessageQueue。

代碼@3:向 MessageQueue 發送消息。

代碼@4:更新失敗策略,主要用於規避發生故障的 broker,下文會詳細介紹。

代碼@5:如果是同步調用方式(SYNC),則執行失敗重試策略,默認重試兩次。

2.2.2.1 獲取topic的路由信息

首先我們來思考一下,topic 的路由信息包含哪些內容。

消息的發佈與訂閱基於topic,路由發佈信息以 topic 維度進行描述。

Broker 負載消息存儲,一個 topic 可以分佈在多臺 Broker 上(負載均衡),每個 Broker 包含多個 Queue。隊列元數據基於Broker來描述(QueueData:所在 BrokerName、讀隊列個數、寫隊列個數、權限、同步或異步)。

接下來從源碼分析 tryToFindTopicPublishInfo方法,詳細瞭解獲取 Topic 的路由信息。

DefaultMQProducerImpl#tryToFindTopicPublishInfo

private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
        TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);        // @1
        if (null == topicPublishInfo || !topicPublishInfo.ok()) {
            this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);          // @2
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
        }
        if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {            //@3
            return topicPublishInfo;
        } else {
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);      //@4
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
            return topicPublishInfo;
        }
    }

代碼@1:從本地緩存(ConcurrentMap< String/* topic */, TopicPublishInfo>)中嘗試獲取,第一次肯定爲空,走代碼@2的流程。

代碼@2:嘗試從 NameServer 獲取配置信息並更新本地緩存配置。

代碼@3:如果找到可用的路由信息並返回。

代碼@4:如果未找到路由信息,則再次嘗試使用默認的 topic 去找路由配置信息。

接下來我們重點關注updateTopicRouteInfoFromNameServer方法。

MQClientInstance#updateTopicRouteInfoFromNameServer

public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault, DefaultMQProducer defaultMQProducer) {
        try {
            if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {     // @1
                try {
                    TopicRouteData topicRouteData;
                    if (isDefault && defaultMQProducer != null) {      //@2
                        topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
                            1000 * 3);
                        if (topicRouteData != null) {
                            for (QueueData data : topicRouteData.getQueueDatas()) {
                                int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
                                data.setReadQueueNums(queueNums);
                                data.setWriteQueueNums(queueNums);
                            }
                        }
                    } else {
                        topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);    //@3
                    }
                    if (topicRouteData != null) {
                        TopicRouteData old = this.topicRouteTable.get(topic);     //@4
                        boolean changed = topicRouteDataIsChange(old, topicRouteData);    //@5
                        if (!changed) {
                            changed = this.isNeedUpdateTopicRouteInfo(topic);                        //@6
                        } else {
                            log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
                        }
                        if (changed) {    //@7
                            TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
                            for (BrokerData bd : topicRouteData.getBrokerDatas()) {
                                this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
                            }
                            // Update Pub info     //@8
                            {
                                TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
                                publishInfo.setHaveTopicRouterInfo(true);
                                Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQProducerInner> entry = it.next();
                                    MQProducerInner impl = entry.getValue();
                                    if (impl != null) {
                                        impl.updateTopicPublishInfo(topic, publishInfo);
                                    }
                                }
                            }
                            // Update sub info    //@9
                            {
                                Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
                                Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQConsumerInner> entry = it.next();
                                    MQConsumerInner impl = entry.getValue();
                                    if (impl != null) {
                                        impl.updateTopicSubscribeInfo(topic, subscribeInfo);
                                    }
                                }
                            }
                            log.info("topicRouteTable.put. Topic = {}, TopicRouteData[{}]", topic, cloneTopicRouteData);
                            this.topicRouteTable.put(topic, cloneTopicRouteData);
                            return true;
                        }
                    } else {
                        log.warn("updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}", topic);
                    }
                } catch (Exception e) {
                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) && !topic.equals(MixAll.DEFAULT_TOPIC)) {
                        log.warn("updateTopicRouteInfoFromNameServer Exception", e);
                    }
                } finally {
                    this.lockNamesrv.unlock();
                }
            } else {
                log.warn("updateTopicRouteInfoFromNameServer tryLock timeout {}ms", LOCK_TIMEOUT_MILLIS);
            }
        } catch (InterruptedException e) {
            log.warn("updateTopicRouteInfoFromNameServer Exception", e);
        }
        return false;
    }

代碼@1:爲了避免重複從 NameServer 獲取配置信息,在這裏使用了ReentrantLock,並且設有超時時間。固定爲3000s。

代碼@2,@3的區別,一個是獲取默認 topic 的配置信息,一個是獲取指定 topic 的配置信息,該方法在這裏就不跟蹤進去了,具體的實現就是通過與 NameServer 的長連接 Channel 發送 GET_ROUTEINTO_BY_TOPIC (105)命令,獲取配置信息。注意,次過程的超時時間爲3s,由此可見,NameServer的實現要求高效。

代碼@4、@5、@6:從這裏開始,拿到最新的 topic 路由信息後,需要與本地緩存中的 topic 發佈信息進行比較,如果有變化,則需要同步更新發送者、消費者關於該 topic 的緩存。

代碼@7:更新發送者的緩存。

代碼@8:更新訂閱者的緩存(消費隊列信息)。

至此 tryToFindTopicPublishInfo 運行完畢,從 NameServe r獲取 TopicPublishData,繼續消息發送的第二個步驟,選取一個消息隊列。

2.2.2.2 獲取MessageQueue

核心源碼:DefaultMQProducerImpl.sendDefaultImpl,對應 selectOneMessageQueue 方法。

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
        if (this.sendLatencyFaultEnable) {   // @1
            try {
                int index = tpInfo.getSendWhichQueue().getAndIncrement();   //@2 start
                for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                    int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                    if (pos < 0)
                        pos = 0;
                    MessageQueue mq = tpInfo.getMessageQueueList().get(pos);    //@2 end
                    if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {     //@3
                        if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                            return mq;
                    }
                }
                final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();   //@4
                int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);     //@5 start
                if (writeQueueNums > 0) {
                    final MessageQueue mq = tpInfo.selectOneMessageQueue();
                    if (notBestBroker != null) {
                        mq.setBrokerName(notBestBroker);
                        mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                    }
                    return mq;
                } else {
                    latencyFaultTolerance.remove(notBestBroker);                               //@5 end
                }
            } catch (Exception e) {
                log.error("Error occurred when selecting message queue", e);
            }
            return tpInfo.selectOneMessageQueue();
        }
        return tpInfo.selectOneMessageQueue(lastBrokerName);     //@6 
    }

代碼@1: sendLatencyFaultEnable,是否開啓消息失敗延遲規避機制,該值在消息發送者那裏可以設置,如果該值爲false,直接從 topic 的所有隊列中選擇下一個,而不考慮該消息隊列是否可用(比如Broker掛掉)。


代碼@2-start--end,這裏使用了本地線程變量 ThreadLocal 保存上一次發送的消息隊列下標,消息發送使用輪詢機制獲取下一個發送消息隊列。

代碼@2對 topic 所有的消息隊列進行一次驗證,爲什麼要循環呢?因爲加入了發送異常延遲,要確保選中的消息隊列(MessageQueue)所在的Broker是正常的。

代碼@3:判斷當前的消息隊列是否可用。

要理解代碼@2,@3 處的邏輯,我們就需要理解 RocketMQ 發送消息延遲機制,具體實現類:MQFaultStrategy。


private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
 private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
        if (this.sendLatencyFaultEnable) {
            long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
            this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
        }
    }
    private long computeNotAvailableDuration(final long currentLatency) {
        for (int i = latencyMax.length - 1; i >= 0; i--) {
            if (currentLatency >= latencyMax[i])
                return this.notAvailableDuration[i];
        }
        return 0;
    }

latencyMax:最大延遲時間數值,在消息發送之前,先記錄當前時間(start),然後消息發送成功或失敗時記錄當前時間(end),(end-start)代表一次消息延遲時間,發送錯誤時,updateFaultItem 中 isolation 爲 true,與 latencyMax 中值進行比較時得值爲 30s,也就時該 broke r在接下來得 600000L,也就時5分鐘內不提供服務,等待該 Broker 的恢復。


計算出來的延遲值+加上本次消息的延遲值,設置 爲FaultItem 的 startTimestamp,表示當前時間必須大於該 startTimestamp 時,該 broker 才重新參與 MessageQueue 的負載。

從@2--@3,一旦一個 MessageQueue 符合條件,即刻返回,但該 Topic 所在的所 有Broker全部標記不可用時,進入到下一步邏輯處理。(在此處,我們要知道,標記爲不可用,並不代表真的不可用,Broker 是可以在故障期間被運營管理人員進行恢復的,比如重啓)。

代碼@4,5:根據 Broker 的 startTimestart 進行一個排序,值越小,排前面,然後再選擇一個,返回(此時不能保證一定可用,會拋出異常,如果消息發送方式是同步調用,則有重試機制)。

接下來將進入到消息發送的第三步,發現消息。

2.2.2.3 根據MessageQueue向特定的Broker發送消息

消息發送方法爲 sendKernelImpl。本文將不深入研究該方法,此刻理解爲通過Product與Broker的長連接將消息發送給Broker,然後Broker將消息存儲,並返回生產者。值得注意的是,如果消息發送模式爲(SYNC)同步調用時,在生產者實現這邊默認提供重試機制,通過(retryTimesWhenSendFailed)參數設置,默認爲2,表示重試2次,也就時最多運行3次。

備註:異步消息發送的重試是在回調時。

本文主要分析了 RocketMQ 以同步方式發送消息的過程,異步模式與單向模式實現原理基本一樣,異步只是增加了發送成功或失敗的回掉方法。

思考題:

1、消息發送時時異常處理思路

1)NameServer 宕機

2)Broker 宕機

1、消息發送者在同一時刻持有 NameServer 集羣中的一個連接,用來及時獲取 broker 等信息(topic路由信息),每一個 Topic的隊列分散在不同的 Broker上,默認 topic在Broker 中對應4個發送隊列,4個消息隊列。

消息發送圖解:

1、NameServer 掛機

在發送消息階段,如果生產者本地緩存中沒有緩存 topic 的路由信息,則需要從 NameServer 獲取,只有當所有 NameServer 都不可用時,此時會拋 MQClientException。如果所有的 NameServer 全部掛掉,並且生產者有緩存 Topic 的路由信息,此時依然可以發送消息。所以,NameServer 的宕機,通常不會對整個消息發送帶來什麼嚴重的問題。

2、Broker掛機

基礎知識:消息生產者每隔 30s 從 NameServer 處獲取最新的 Broker 存活信息(topic路由信息),Broker 每30s 向所有的 NameServer 報告自己的情況,故 Broker 的 down 機,Procuder 的最大可感知時間爲 60s,在這 60s,消息發送會有什麼影響呢?

此時分兩種情況分別進行分析。

1)啓用sendLatencyFaultEnable

由於使用了故障延遲機制,詳細原理見上文詳解,會對獲取的 MQ 進行可用性驗證,比如獲取一個MessageQueue 發送失敗,這時會對該 Broker 進行標記,標記該 Broker 在未來的某段時間內不會被選擇到,默認爲(5分鐘,不可改變),所有此時只有當該 topic 全部的 broker 掛掉,才無法發送消息,符合高可用設計。

2)不啓用sendLatencyFaultEnable = false

此時會出現消息發送失敗的情況,因爲默認情況下,procuder 每次發送消息,會採取輪詢機制取下一個 MessageQueue,由於可能該 Message 所在的Broker掛掉,會拋出異常。因爲一個 Broker 默認爲一個 topic 分配4個 messageQueue,由於默認只重試2次,故消息有可能發送成功,有可能發送失敗。

作者:唯有堅持不懈
原文鏈接:https://blog.csdn.net/prestigeding/article/details/75799003

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