RocketMQ-Producer生產者解析


  • Producer 概念說明*
  • 初始化流程&流程圖&相關類關系說明*
  • 消息發送過程*
  • 批量消息發送*
  • 發送順序消息、延遲消息、事務消息*
  • 消息發送方式 同步、異步、單向區別和過程*
  • 消息發送如何進行負載*
  • 消息發送如何實現高可用*
  • 批量消息發送如何實現一致性*
  • 消息發送失敗如何重試*
  • Producer 和 nameServ 通信機制*
  • Producer 和 broker 通信機制*
  • 消息發送異常機制*
  • Producer 配置講解*

1 Producer 概念說明

本文講解的是rocketmq中的生產者部分為了方便理解下圖中帶有顏色部分。


rocketmq cluster.png

producer: 消息生產者,主要作用用于發送消息。
producerGroup:
用來表示一個發送消息應用,一個Producer Group下包含多個Producer實例,可以是多臺機器,也可以是一臺機器的多個進程,或者一個進程的多個Producer對象。一個Producer Group可以發送多個Topic消息,
Producer Group作用如下:標識一類Producer 可以通過運維工具查詢這個發送消息應用下有多個Producer實例,發送分布式事務消息時,如果Producer中途意外宕機,Broker會主動回調Producer Group內的任意一臺機器來確認 事務狀態
Topic:
標識一類消息的邏輯名字,消息的邏輯管理單位。無論消息生產還是消費,都需要指定Topic,建議一個應用一個topic,同時線上應該關閉程序自動創建topic的功能。
Tag:
RocketMQ支持給在發送的時候給topic打tag,同一個topic的消息雖然邏輯管理是一樣的。但是消費topic1的時候,如果你訂閱的時候指定的是tagA,那么tagB的消息將不會投遞,建議同一個應用處理不同業務或場景使用。
Message Queue:
簡稱Queue或Q。消息物理管理單位。一個Topic將有若干個Q。若Topic同時創建在不通的Broker,則不同的broker上都有若干Q,消息將物理地存儲落在不同Broker結點上,具有水平擴展的能力。
無論生產者還是消費者,實際的生產和消費都是針對Q級別。例如Producer發送消息的時候,會預先選擇(默認輪詢)好該Topic下面的某一條Q地發送;Consumer消費的時候也會負載均衡地分配若干個Q,只拉取對應Q的消息。每一條message queue均對應一個文件,這個文件存儲了實際消息的索引信息。并且即使文件被刪除,也能通過實際純粹的消息文件(commit log)恢復回來。
Offset:
RocketMQ中,有很多offset的概念。但通常我們只關心暴露到客戶端的offset。一般我們不特指的話,就是指邏輯Message Queue下面的offset。
注: 邏輯offset的概念在RocketMQ中字面意思實際上和真正的意思有一定差別,這點在設計上顯得有點混亂。祥見下面的解釋。
可以認為一條邏輯的message queue是無限長的數組。一條消息進來下標就會漲1,而這個數組的下標就是offset。
max offset:
字面上可以理解為這是標識message queue中的max offset表示消息的最大offset。但是從源碼上看,這個offset實際上是最新消息的offset+1,即:下一條消息的offset。
min offset
標識現存在的最小offset。而由于消息存儲一段時間后,消費會被物理地從磁盤刪除,message queue的min offset也就對應增長。這意味著比min offset要小的那些消息已經不在broker上了,無法被消費。
consumer offset:
字面上,可以理解為標記Consumer Group在一條邏輯Message Queue上,消息消費到哪里即消費進度。但從源碼上看,這個數值是消費過的最新消費的消息offset+1,即實際上表示的是下次拉取的offset位置
消費者拉取消息的時候需要指定offset,broker不主動推送消息, offset的消息返回給客戶端。consumer剛啟動的時候會獲取持久化的consumer offset,用以決定從哪里開始消費,consumer以此發起第一次請求。
每次消息消費成功后,這個offset在會先更新到內存,而后定時持久化。在集群消費模式下,會同步持久化到broker,而在廣播模式下,則會持久化到本地文件。

2 Producer 初始化流程&流程圖&相關類關系說明


在發送消息之前我們先初始化相關類,這里主要用到的類是DefaultMQProducer

public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook,
    boolean enableMsgTrace, final String customizedTraceTopic) {
    this.namespace = namespace;
    this.producerGroup = producerGroup;
    defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
    //if client open the message trace feature
    if (enableMsgTrace) {
     ...       
    }
}

namespace:實例名稱。
producerGroup:生產消息組。
rpcHook:注冊一個發送鉤子,在發送消息before和after需要處理的邏輯。
enableMsgTrace: 是否開啟鏈路追蹤。
customizedTraceTopic: 發送鏈路追蹤Topic(默認值:RMQ_SYS_TRACE_TOPIC)
當然以上參數并非都是必選的,我們要根據實際情況選擇不同參數。

3 Producer 啟動流程&Shutdown流程&相關類關系說明

DefaultMQProducer.start() 啟動一個消息生產者實例
DefaultMQProducer.shutdown() 關閉一個消息生產者實例
那我們先來看看啟動一個生產者實例都做了哪些事情

@Override
public void start() throws MQClientException {
     //設置當前prdoucer group
    this.setProducerGroup(withNamespace(this.producerGroup));
    //調用 defaultMQProducerImpl start 方法
    this.defaultMQProducerImpl.start();
    if (null != traceDispatcher) { // 啟動鏈路追蹤
       ...
    }
}

this.defaultMQProducerImpl.start()

public void start() throws MQClientException {
    this.start(true);
}
public void start(final boolean startFactory) throws MQClientException {
    switch (this.serviceState) {
        case CREATE_JUST:
            ...
            break;
        case RUNNING:
        case START_FAILED:
        case SHUTDOWN_ALREADY:
            throw new MQClientException("The producer service state not OK, maybe started once, "
                + this.serviceState
                + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                null);
        default:
            break;
    }
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
}

this.serviceState 狀態默認是 ServiceState.CREATE_JUST, 那我們來看一下CREATE_JUST都做了什么事情

 ....
 this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);

boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
public boolean registerProducer(final String group, final DefaultMQProducerImpl producer) {
    ...     
 MQProducerInner prev = this.producerTable.putIfAbsent(group, producer);
    ...
}

獲取一個MQClientInstance 如果沒有則創建,然后把當前的producer放到MQClientInstance的內存producerTable 中.
private final ConcurrentMap<String_* group *_, MQProducerInner> producerTable = new ConcurrentHashMap<String, MQProducerInner>();
producerTable對象里面存儲producerGroupName和DefaultMQProducer的映射。key-value:<producerGroupName,DefaultMQProducer>。

把當前producer 放入到 MQClientInstance table之后:

if (startFactory) { //
    mQClientFactory.start(); //mQClientFactory上邊獲取的MQClientInstance 實例
}

開始啟動MQClientInstance

public void start() throws MQClientException {
    synchronized (this) {
        switch (this.serviceState) {
            case CREATE_JUST:
                .....
                break;
            case RUNNING:
                break;
            case SHUTDOWN_ALREADY:
                break;
            case START_FAILED:
                throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
            default:
                break;
        }
    }
}

感覺和之前 producer的start有點熟悉的感覺,細心的同學們可能發現了有synchronized 來做修飾,CREATE_JUST的狀態做的事情

 ...
// 啟動和netty遠程通信
this.mQClientAPIImpl.start();
// 啟動定時任務
this.startScheduledTask();
// 消費者使用線程
this.pullMessageService.start();
// 負載均衡線程
this.rebalanceService.start();
// 又調用 producer start  this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
 ...

上邊說的定時任務都有哪些
(MQClientInstance說明一下,因為生產和消費者都會持有MQClientInstance所以在啟動任務的時候會啟動生產和消費相關的任務線程)

每兩分鐘執行一次尋址服務(NameServer地址)

if (null == this.clientConfig.getNamesrvAddr()) {
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            try {
MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
            } catch (Exception e) {
                log.error("ScheduledTask fetchNameServerAddr exception", e);
            }
        }
    }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
}

每30秒更新一次所有的topic的路由信息(topicRouteTable)

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        try {
MQClientInstance.this.updateTopicRouteInfoFromNameServer();
        } catch (Exception e) {
            log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);
        }
    }
}, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);

每30秒移除離線的broker
每30秒發送一次心跳給所有的master broker

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        try {
            MQClientInstance.this.cleanOfflineBroker();
    MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
        } catch (Exception e) {
            log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
        }
    }
}, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);

更新offset每5秒提交一次消費的offset,broker端為ConsumerOffsetManager負責記錄,此offset是邏輯偏移量,比如說,consumerA@consumerAGroup 在broker_a的queue 0的消費隊列共有10000條消息,目前消費到888,那么offset就是888.
因為producer和consumer內部都持有MQClientInstance實例,故MQClientInstance既有生產者邏輯,又有消費者邏輯

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        try {
            MQClientInstance.this.persistAllConsumerOffset();
        } catch (Exception e) {
            log.error("ScheduledTask persistAllConsumerOffset exception", e);
        }
    }
}, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

每1分鐘調整一次線程池,這也是針對消費者來說的,具體為如果消息堆積超過10W條,則調大線程池,最多64個線程;如果消息堆積少于8W條,則調小線程池,最少20的線程。

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        try {
            MQClientInstance.this.adjustThreadPool();
        } catch (Exception e) {
            log.error("ScheduledTask adjustThreadPool exception", e);
        }
    }
}, 1, 1, TimeUnit.MINUTES);

以上就是producer start的流程,我們做個簡單的總結:
1.獲取當前的MQClientInstance實例,沒有則新建
2.把當前的producer 注冊到 MQClientInstance table 中
3.啟動MQClientInstance,啟動通信、定時()、消費線程(針對消費者)。

start 的事情做完了,那我們看看shutdwon的事情 ,shutdwon做的事情就是將之前start啟動的線程停止掉和注冊的實例刪除掉
Producer

public void shutdown() {
    this.shutdown(true);
}
public void shutdown(final boolean shutdownFactory) {
    switch (this.serviceState) {
        case CREATE_JUST:
            break;
        case RUNNING:
            .....
            break;
        default:
            break;
    }
}

執行shutdown的時候,service 本身狀態是RUNNING

this.mQClientFactory.unregisterProducer(this.defaultMQProducer.getProducerGroup()); //注銷 producer
this.defaultAsyncSenderExecutor.shutdown(); // 停止發送消息線程池
if (shutdownFactory) {
    this.mQClientFactory.shutdown(); // 停止之前啟動通信以及定時任務等
}
...

shutdown做的事情就是釋放之前start啟動的線程。

4 消息發送方式

同步
發送者向MQ執行發送消息API時,同步等待,直到消息服務器返回發送結(主要運用在比較重要一點消息傳遞/通知等業務)

異步
發送者向MQ執行發送消息API時,指定消息發送成功后的回掉函數,然后調用消息發送API后,立即返回,消息發送者線程不阻塞,直到運行結束,消息發送成功或失敗的回調任務在一個新的線程中執行(通常用于對發送消息響應時間要求更高/更快的場景)

單向
消息發送者向MQ執行發送消息API時,直接返回,不等待消息服務器的結果,也不注冊回調函數,簡單地說,就是只管發,不在乎消息是否成功存儲在消息服務器上(適用于某些耗時非常短,但對可靠性要求并不高的場景,例如日志收集)

發送前 (DefaultMQProducerImpl) 以上三種流程最后都會調用此方法

private SendResult sendDefaultImpl(
    Message msg,
    final CommunicationMode communicationMode,
    final SendCallback sendCallback,
    final long timeout
) 

1.路由topic 信息 TopicPublishInfo

    private boolean orderTopic = false;  是否是順序消息
    private boolean haveTopicRouterInfo = false;
    private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>(); 該主題隊列的消息隊列
    private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); 每選擇一次消息隊列,該值會自增1,如果Integer.MAX_VALUE,則重置為0,用于選擇消息隊列
    private TopicRouteData topicRouteData;

2.根據TopicPublishInfo 選擇 MessageQueue
根據路由信息選擇消息隊列,返回的消息隊列按照broker、序號排序。舉例說明,如果topicA在broker-a, broker-b上分別創建了4個隊列,那么返回的消息隊列:
[
{“broker-Name”: ”broker-a”, ”queueId”:0},
{“brokerName”: ”broker-a”, ”queueId”:1},
{“brokerName”:”broker-a”,”queueId”:2},
{“brokerName”:”broker-a”, ”queueId”:3},
{“brokerName”: ”broker-b”, ”queueId”:0},
{“brokerName”: ”broker-b”, ”queueId”:1},
{“brokerName”: ”broker-b”, ”queueId”:2},
{“brokerName”:”broker-b”, ”queueId”:3}
]
首先消息發送端采用重試機制,由retryTimesWhenSendFailed指定同步方式重試次數,異步重試機制在收到消息發送結構后執行回調之前進行重試。由retryTimes When Send-AsyncFailed指定,接下來就是循環執行,選擇消息隊列、發送消息,發送成功則返回,收到異常則重試。選擇消息隊列有兩種方式。
1)sendLatencyFaultEnable=false,默認不啟用Broker故障延遲機制。
2)sendLatencyFaultEnable=true,啟用Broker故障延遲機制。
1.默認機制 sendLatencyFaultEnable=false,調用
TopicPublishInfo#selectOneMessageQueue

public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
    if (lastBrokerName == null) {
        return selectOneMessageQueue();
    } else {
        int index = this.sendWhichQueue.getAndIncrement();
        for (int i = 0; i < this.messageQueueList.size(); i++) {
            int pos = Math.abs(index++) % this.messageQueueList.size();
            if (pos < 0)
                pos = 0;
            MessageQueue mq = this.messageQueueList.get(pos);
            if (!mq.getBrokerName().equals(lastBrokerName)) {
                return mq;
            }
        }
        return selectOneMessageQueue();
    }
}

首先在一次消息發送過程中,可能會多次執行選擇消息隊列這個方法,lastBrokerName就是上一次選擇的執行發送消息失敗的Broker。第一次執行消息隊列選擇時,lastBrokerName為null,此時直接用sendWhichQueue自增再獲取值,與當前路由表中消息隊列個數取模,返回該位置的MessageQueue(selectOneMessageQueue()方法),如果消息發送再失敗的話,下次進行消息隊列選擇時規避上次MesageQueue所在的Broker,否則還是很有可能再次失敗。

2.Broker故障延遲機制

MQFaultStrategy#selectOneMessageQueue

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
    if (this.sendLatencyFaultEnable) {
        try {
            int index = tpInfo.getSendWhichQueue().getAndIncrement();
            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);
                if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
                    if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                        return mq;
                }
            }

            final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
            int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
            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);
            }
        } catch (Exception e) {
            log.error("Error occurred when selecting message queue", e);
        }

        return tpInfo.selectOneMessageQueue();
    }
}

首先對上述代碼進行解讀。
1)根據對消息隊列進行輪詢獲取一個消息隊列。
2)驗證該消息隊列是否可用,latencyFaultTolerance.isAvailable(mq.getBrokerName())是關鍵。3)如果返回的MessageQueue可用,移除latencyFaultTolerance關于該topic條目,表明該Broker故障已經恢復。

發送 消息發送API核心入口:DefaultMQProducerImpl#sendKernelImpl

private SendResult sendKernelImpl(final Message msg,
                                  final MessageQueue mq,
                                  final CommunicationMode communicationMode,
                                  final SendCallback sendCallback,
                                  final TopicPublishInfo topicPublishInfo,
                                  final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
}

消息發送參數詳解。
1)Message msg:待發送消息。
2)MessageQueue mq:消息將發送到該消息隊列上。
3)CommunicationMode communicationMode:消息發送模式,SYNC、ASYNC、ONEWAY。
4)SendCallback sendCallback:異步消息回調函數。
5)TopicPublishInfo topicPublishInfo:主題路由信息
6)long timeout:消息發送超時時間。

DefaultMQProducerImpl#sendKernelImpl

String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
if (null == brokerAddr) {
    tryToFindTopicPublishInfo(mq.getTopic());
    brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
}

Step1:根據MessageQueue獲取Broker的網絡地址。如果MQClientInstance的brokerAddrTable未緩存該Broker的信息,則從NameServer主動更新一下topic的路由信息。如果路由更新后還是找不到Broker信息,則拋出MQClientException,提示Broker不存在

int sysFlag = 0;
boolean msgBodyCompressed = false;
if (this.tryToCompressMessage(msg)) {
    sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
    msgBodyCompressed = true;
}
final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
    sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
}

Step2 : 標記 sysFlag 是否壓縮消息(大于4K),是否是事務性消息

if (hasCheckForbiddenHook()) {
    CheckForbiddenContext checkForbiddenContext = new CheckForbiddenContext();
    ....
}

if (this.hasSendMessageHook()) {
    context = new SendMessageContext();
    context.setProducer(this);
    ....
    this.executeSendMessageHookBefore(context);
}

Step3: 如果注冊了消息發送鉤子函數,則執行消息發送之前的增強邏輯。通過Defaul tMQProducerImpl#registerSendMessageHook注冊鉤子處理類,并且可以注冊多個。

SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();

Step4 :構建消息發送請求包。主要包含如下重要信息:生產者組、主題名稱、默認創建主題Key、該主題在單個Broker默認隊列數、隊列ID(隊列序號)、消息系統標記(MessageSysFlag)、消息發送時間、消息標記(RocketMQ對消息中的flag不做任何處理,供應用程序使用)、消息擴展屬性、消息重試次數、是否是批量消息等

MQClientAPIImpl#sendMessage

public SendResult sendMessage(
    final String addr,
    final String brokerName,
    final Message msg,
    final SendMessageRequestHeader requestHeader,
    final long timeoutMillis,
    final CommunicationMode communicationMode,
    final SendCallback sendCallback,
    final TopicPublishInfo topicPublishInfo,
    final MQClientInstance instance,
    final int retryTimesWhenSendFailed,
    final SendMessageContext context,
    final DefaultMQProducerImpl producer
) throws RemotingException, MQBrokerException, InterruptedException {
}

Step5:根據消息發送方式,同步、異步、單向方式進行網絡傳輸發送到broker

DefaultMQProducerImpl#sendKernelImpl

if (this.hasSendMessageHook()) {
    context.setSendResult(sendResult);
    this.executeSendMessageHookAfter(context);
}

Step6: 如果注冊了消息發送鉤子函數,執行after邏輯。注意,就算消息發送過程中發生RemotingException、MQBrokerException、InterruptedException時該方法也會執行。

在此整個發送消息的大概邏輯就結束了,那接下來我們一起總結一下整個發送消息的邏輯的。
1.獲取發送topic信息,先從本地獲取,如果沒有從nameserver獲取并更新到本地緩存,topic信息包括broker和發送mq隊列列表的信息。
2.從topic信息里邊拿到mq隊列集合并選擇一個進行發送,選擇的方式有兩種,第一種是輪詢所有的隊列進行負載均衡選擇一個發送,另一種方式是如果broker開啟了Broker故障延遲機制 ,則有效的規避一段時間不在進行當前broker選擇隊列。

故障延遲機制主要解決的問題應該是某個Broker單機的failover,或者是某個broker瞬時壓力過大,導致接口超時,從而需要路由到別的broker進行消息發送。
3.發送前進行消息的標記是否壓縮和是否是事務性消息構建發送消息體,進行網絡傳輸。

5 批量消息發送

批量消息發送是將同一主題的多條消息一起打包發送到消息服務端,減少網絡調用次數,提高網絡傳輸效率。當然,并不是在同一批次中發送的消息數量越多性能就越好,其判斷依據是單條消息的長度,如果單條消息內容比較長,則打包多條消息發送會影響其他線程發送消息的響應時間,并且單批次消息發送總長度不能超過DefaultMQProducer#maxMessageSize(4 M)。批量消息發送要解決的是如何將這些消息編碼以便服務端能夠正確解碼出每條消息的消息內容。

DefaultMQProducer#send

public SendResult send(
    Collection<Message> msgs) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    return this.defaultMQProducerImpl.send(batch(msgs));
}

DefaultMQProducer#batch

private MessageBatch batch(Collection<Message> msgs) throws MQClientException {
    MessageBatch msgBatch;
    try {
        msgBatch = MessageBatch.generateFromList(msgs);
        for (Message message : msgBatch) {
            Validators.checkMessage(message, this);
            MessageClientIDSetter.setUniqID(message);
            message.setTopic(withNamespace(message.getTopic()));
        }
        msgBatch.setBody(msgBatch.encode());
    } catch (Exception e) {
        throw new MQClientException("Failed to initiate the MessageBatch", e);
    }
    msgBatch.setTopic(withNamespace(msgBatch.getTopic()));
    return msgBatch;
}

首先在消息發送端,調用batch方法,將一批消息封裝成MessageBatch對象。Message-Batch繼承自Message對象,MessageBatch內部持有List<Message> messages。這樣的話,批量消息發送與單條消息發送的處理流程完全一樣。MessageBatch只需要將該集合中的每條消息的消息體body聚合成一個byte[]數值,在消息服務端能夠從該byte[]數值中正確解析出消息即可

MessageBatch#encode

public byte[] encode() {
    return MessageDecoder.encodeMessages(messages);
}

MessageDecoder#encodeMessage

public static byte[] encodeMessages(List<Message> messages) {
    //TO DO refactor, accumulate in one buffer, avoid copies
    List<byte[]> encodedMessages = new ArrayList<byte[]>(messages.size());
    int allSize = 0;
    for (Message message : messages) {
        byte[] tmp = encodeMessage(message);
        encodedMessages.add(tmp);
        allSize += tmp.length;
    }
    byte[] allBytes = new byte[allSize];
    int pos = 0;
    for (byte[] bytes : encodedMessages) {
        System.arraycopy(bytes, 0, allBytes, pos, bytes.length);
        pos += bytes.length;
    }
    return allBytes;
}

在消息發送端將會按照上述結構進行解碼,然后整個發送流程與單個消息發送沒什么差異,就不一一介紹了

6 發送順序消息、延遲消息、事務消息

順序消息
Rocketmq能夠保證消息嚴格順序,但是Rocketmq需要producer保證順序消息按順序發送到同一個queue中,比如購買流程(1)下單(2)支付(3)支付成功,
這三個消息需要根據特定規則將這個三個消息按順序發送到一個queue
如何實現把順序消息發送到同一個queue:
一般消息是通過輪詢所有隊列發送的,順序消息可以根據業務比如說訂單號orderId相同的消息發送到同一個隊列, 或者同一用戶userId發送到同一隊列等等
messageQueueList [orderId%messageQueueList.size()]
messageQueueList [userId%messageQueueList.size()]

延遲消息:
RocketMQ 支持發送延遲消息,但不支持任意時間的延遲消息的設置,僅支持內置預設值的延遲時間間隔的延遲消息。
預設值的延遲時間間隔為:1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h
在消息創建的時候,調用 setDelayTimeLevel(int level) 方法設置延遲時間。broker在接收到延遲消息的時候會把對應延遲級別的消息先存儲到對應的延遲隊列中,等延遲消息時間到達時,會把消息重新存儲到對應的topic的queue里面

事務消息 // TODO

7 消息發送方式 同步、異步、單向區別

1.同步發送 ,需要同時等待有返回值,主要運用在比較重要一點消息傳遞/通知等業務。
SendResult sendResult = producer.send(message);
2.異步發送,異步線程發送出去消息,速度快,sendCallback 拿到返回信息,通常用于對發送消息響應時間要求更高/更快的場景。

producer.send(message, new SendCallback() {
           @Override
           public void onSuccess(SendResult sendResult) {
           }
           @Override
           public void onException(Throwable throwable) {
           }
       });

3.oneway 方式,只管發送,不在意是否成功,日志處理一般這樣,/只發送消息,不等待服務器響應,只發送請求不等待應答。此方式發送消息的過程耗時非常短,一般在微秒級別。

8 消息發送如何進行負載

通過上文 4 消息發送方式 有一步操作是選擇消息對立進行發送,通過輪詢所有隊列的機制實現負載。

9 消息發送如何實現高可用

通過上文 4 消息發送方式 選擇消息隊列的時候有開啟Broker故障延遲機制 策略來規避一直發送失敗的broker中的隊列提升高可用。

10 批量消息發送如何實現一致性

將批量消息統一進行編碼 發送到body,消費端根據編碼規則進行解碼。

11 消息發送失敗如何重試

SYNC 重試3 其他1

int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
String[] brokersSent = new String[timesTotal];
for (; times < timesTotal; times++) {
   .....
}

sendOneway不可能對遠程狀態碼重試,因為它不會收到任何值,所以會重試本地錯誤重試,也就是 1次
本地的錯誤重試包括RemotingException、MQClientException

send同步/異步都會做broker錯誤碼MQBrokerException
重試,oneway不會。

private void sendMessageAsync(
    final String addr,
    final String brokerName,
    final Message msg,
    final long timeoutMillis,
    final RemotingCommand request,
    final SendCallback sendCallback,
    final TopicPublishInfo topicPublishInfo,
    final MQClientInstance instance,
    final int retryTimesWhenSendFailed,
    final AtomicInteger times,
    final SendMessageContext context,
    final DefaultMQProducerImpl producer
) throws InterruptedException, RemotingException {
   .....
    catch (Exception e) {
    producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), true);
    *onExceptionImpl*(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance,
        retryTimesWhenSendFailed, times, e, context, false, producer);
}

}

這里調用了processSendResponse,這個會拋出broker錯,在catch里會重試錯誤,onExceptionImpl里做了遞歸重試處理,超過一定次數就不再重試,默認為3次。

12 Producer 和 nameServ&broker 通信機制

與nameserver關系

  • 連接
    • 單個生產者者和一臺nameserver保持長連接,定時查詢topic配置信息,如果該nameserver掛掉,生產者會自動連接下一個nameserver,直到有可用連接為止,并能自動重連。
  • 輪詢時間
    • 默認情況下,生產者每隔30秒從nameserver獲取所有topic的最新隊列情況,這意味著某個broker如果宕機,生產者最多要30秒才能感知,在此期間,發往該broker的消息發送失敗。該時間由DefaultMQProducer的pollNameServerInteval參數決定,可手動配置。
  • 心跳
    • 與nameserver沒有心跳

與broker關系

  • 連接
    • 單個生產者和該生產者關聯的所有broker保持長連接。
  • 心跳
    • 默認情況下,生產者每隔30秒向所有broker發送心跳,該時間由DefaultMQProducer的heartbeatBrokerInterval參數決定,可手動配置。broker每隔10秒鐘(此時間無法更改),掃描所有還存活的連接,若某個連接2分鐘內(當前時間與最后更新時間差值超過2分鐘,此時間無法更改)沒有發送心跳數據,則關閉連接。
  • 連接斷開
    • 移除broker上的生產者信息

13 消息發送異常機制

針對異常機制做重試處理

14 Producer 配置講解 //TODO

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容