6-RocketMQ傳送訊息

LZC發表於2021-07-07
public static void main(String[] args) throws Exception {
    // Instantiate with a producer group name.
    DefaultMQProducer producer = new
        DefaultMQProducer("GroupNameDemo");
    // Specify name server addresses.
    producer.setNamesrvAddr("localhost:9876");
    //Launch the instance.
    producer.start();
    for (int i = 0; i < 100; i++) {
        //Create a message instance, specifying topic, tag and message body.
        Message msg = new Message("TopicDemo" /* Topic */,
                                  "TagA" /* Tag */,
                                  ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
                                 );
        // Call send message to deliver message to one of brokers.
        SendResult sendResult = producer.send(msg);
        System.out.printf("%s%n", sendResult);
    }
    //Shut down once the producer instance is not longer in use.
    producer.shutdown();
}

訊息傳送的入口DefaultMQProducer#send()

// DefaultMQProducer.java
@Override
public SendResult send(
    Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    // 檢查訊息
    Validators.checkMessage(msg, this);
    msg.setTopic(withNamespace(msg.getTopic()));
    // 傳送訊息
    return this.defaultMQProducerImpl.send(msg);
}

訊息檢驗

  1. 檢查訊息內容不能為空
  2. 檢查訊息長度不能為0
  3. 預設情況,訊息大小不能超過 4M
// Validators.java
public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer)
    throws MQClientException {
    if (null == msg) {
        // 拋異常
    }
    // topic
    Validators.checkTopic(msg.getTopic());

    // body 檢查訊息內容不能為空
    if (null == msg.getBody()) {
        // 拋異常
    }
    // 檢查訊息長度不能為0
    if (0 == msg.getBody().length) {
        // 拋異常
    }
    // 預設情況,訊息大小不能超過 4M = 1024 * 1024 * 4; 
    if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) {
        // 拋異常
    }
}

訊息傳送

// DefaultMQProducerImpl.java
// DEFAULT SYNC
public SendResult send(Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    // this.defaultMQProducer.getSendMsgTimeout() 獲取訊息傳送超時時間, 預設為3s
    return send(msg, this.defaultMQProducer.getSendMsgTimeout());
}

public SendResult send(Message msg,long timeout) throws MQClientException,RemotingException, MQBrokerException, InterruptedException {
    return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
}

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());
        // 省略......
}

查詢主題路由資訊

如果生產者已經快取了topic的路由資訊,則直接返回。如果沒有快取,則向NameServer查詢該topic的路由資訊。

如果最終未能查詢到路由資訊,則直接丟擲異常。

// DefaultMQProducerImpl.java
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    // 1. 先從快取中獲取
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        // 2. 向NameServer查詢該topic的路由資訊
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
    }

    if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
        return topicPublishInfo;
    } else {
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
        return topicPublishInfo;
    }
}

TopicPublishInfo資訊如下

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

public class TopicRouteData extends RemotingSerializable {
    private String orderTopicConf;
    // topic佇列後設資料
    private List<QueueData> queueDatas;
    // topic分佈的broker後設資料
    private List<BrokerData> brokerDatas;
    private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}

具體資訊如下

{
    "haveTopicRouterInfo": true,
    "messageQueueList": [
        {
            "brokerName": "PQSZ-L0039",
            "queueId": 0,
            "topic": "TopicDemo"
        },
        {
            "brokerName": "PQSZ-L0039",
            "queueId": 1,
            "topic": "TopicDemo"
        },
        {
            "brokerName": "PQSZ-L0039",
            "queueId": 2,
            "topic": "TopicDemo"
        },
        {
            "brokerName": "PQSZ-L0039",
            "queueId": 3,
            "topic": "TopicDemo"
        }
    ],
    "orderTopic": false,
    "sendWhichQueue": {
        "andIncrement": 1497938501
    },
    "topicRouteData": {
        "brokerDatas": [
            {
                "brokerAddrs": {
                    "0": "10.178.42.122:10911"
                },
                "brokerName": "PQSZ-L0039",
                "cluster": "DefaultCluster"
            }
        ],
        "filterServerTable": {},
        "queueDatas": [
            {
                "brokerName": "PQSZ-L0039",
                "perm": 6,
                "readQueueNums": 4,
                "topicSynFlag": 0,
                "writeQueueNums": 4
            }
        ]
    }
}

選擇訊息佇列傳送訊息

根據主題路由資訊選擇訊息佇列

// DefaultMQProducerImpl.java
private SendResult sendDefaultImpl(
            Message msg,
            final CommunicationMode communicationMode,
            final SendCallback sendCallback,
            final long timeout
    ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    // 省略......
        if (topicPublishInfo != null && topicPublishInfo.ok()) {
            boolean callTimeout = false;
            MessageQueue mq = null;
            Exception exception = null;
            SendResult sendResult = null;
            // 這裡會設定一個請求次數
            // 如果為同步: timesTotal = 1 + 2,有兩次重試次數
            // 如果為非同步: timesTotal = 1, 重試操作會在回撥介面中進行
            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();
                // 選擇訊息佇列, 首次進入時 lastBrokerName = null
                MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
                if (mqSelected != null) {
                    mq = mqSelected;
                    brokersSent[times] = mq.getBrokerName();
                    // 省略......
                    // ......這裡會計算是否超時,如果超時會直接丟擲異常
                    // 傳送資訊
                    try{
                        sendResult = this.sendKernelImpl(
                        msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                        // ......
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                        // ......
                    } catch (Exception e) {
                        // 這裡其實會有很多種異常,為了方便理解,直接在這裡寫成Exception
                        // 訊息傳送失敗
                        // 啟用Broker故障延遲機制,這裡會將這個BrokerName儲存起來
                        // 預設情況,使用30s來計算Broker故障規避時長,裡面會有一些計算邏輯
                        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                    }

                    // 省略......
                }
            }
        }
}

從上面程式碼可以發現,訊息傳送成功或者失敗都會呼叫MQFaultStrategy#updateFaultItem,如果開啟了Broker故障延遲機制,裡面的程式碼才會執行。

/**
  * @param brokerName broker名稱
  * @param currentLatency 本次訊息傳送延遲時間 currentLatency
  * @param isolation 是否隔離,該引數的含義如果為true,則使用30s來計算Broker故障規避時長,
  *                  如果為false,則使用本次訊息傳送延遲時間來計算Broker故障規避時長。
  *                           
  */
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);
    }
}

選擇訊息佇列有兩種方式。

1)sendLatencyFaultEnable=false,預設不啟用Broker故障延遲機制。

2)sendLatencyFaultEnable=true,啟用Broker故障延遲機制。

不啟用Broker故障延遲機制時選擇佇列的方法入口在TopicPublishInfo#selectOneMessageQueue

public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
    // lastBrokerName == null 說明是第一次選擇訊息佇列
    if (lastBrokerName == null) {
        // 此時直接用sendWhichQueue自增再獲取值,與當前路由表中訊息佇列個數取模
        return selectOneMessageQueue();
    } else {
        // lastBrokerName != null, 說明上一次傳送訊息失敗了,這一次是重試
        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);
            // 上一次呼叫 lastBrokerName 失敗了,那麼這一次呼叫 lastBrokerName 也可能失敗
            // 所以這裡會過濾掉上一次呼叫失敗的那個 Broker
            if (!mq.getBrokerName().equals(lastBrokerName)) {
                return mq;
            }
        }
        // 如果上面沒有獲取佇列
        // 此時直接用sendWhichQueue自增再獲取值,與當前路由表中訊息佇列個數取模
        return selectOneMessageQueue();
    }
}

public MessageQueue selectOneMessageQueue() {
    // 用sendWhichQueue自增再獲取值
    int index = this.sendWhichQueue.getAndIncrement();
    // 拿 index 與佇列長度取模
    int pos = Math.abs(index) % this.messageQueueList.size();
    // 
    if (pos < 0)
        pos = 0;
    // 返回佇列
    return this.messageQueueList.get(pos);
}

啟用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;
                }
            }
            // 嘗試從規避的Broker中選擇一個可用的Broker,如果沒有找到,將返回null
            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();
    }

    return tpInfo.selectOneMessageQueue(lastBrokerName);
}

訊息佇列負載機制:訊息生產者在傳送訊息時,如果本地路由表中未快取topic的路由資訊,向NameServer傳送獲取路由資訊請求,更新本地路由資訊表,並且訊息生產者每隔30s從NameServer更新路由表。

訊息傳送異常機制:訊息傳送高可用主要透過兩個手段:重試與Broker規避。Broker規避就是在一次訊息傳送過程中發現錯誤,在某一時間段內,訊息生產者不會選擇該Broker(訊息伺服器)上的訊息佇列,提高傳送訊息的成功率。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章