《深入理解RocketMQ》- MQ消息的投遞機制

原文鏈接:https://mp.weixin.qq.com/s/9Cnj_hwN37BXaOERKFKUoA

0. 前言

RocketMQ的消息投遞分爲兩種:一種是生產者往MQ Broker中投遞;另外一種則是MQ broker 往消費者 投遞(這種投遞的說法是從消息傳遞的角度闡述的,實際上底層是消費者從MQ broker 中Pull拉取的)。本文將從模型的角度來闡述這兩種機制。


1. RocketMQ的消息模型

RocketMQ 的消息模型整體並不複雜,如下圖所示:

 

 

一個Topic(消息主題)可能對應多個實際的消息隊列(MessgeQueue)
在底層實現上,爲了提高MQ的可用性和靈活性,一個Topic在實際存儲的過程中,採用了多隊列的方式,具體形式如上圖所示。每個消息隊列在使用中應當保證先入先出(FIFO,First In First Out)的方式進行消費。
那麼,基於這種模型,就會引申出兩個問題:

  • 生產者 在發送相同Topic的消息時,消息體應當被放置到哪一個消息隊列(MessageQueue)中?

  • 消費者 在消費消息時,應當從哪些消息隊列中拉取消息?

 


2. 生產者(Producer)投遞消息的策略

2.1 默認投遞方式:基於Queue隊列輪詢算法投遞

默認情況下,採用了最簡單的輪詢算法,這種算法有個很好的特性就是,保證每一個Queue隊列的消息投遞數量儘可能均勻,算法如下圖所示:

/**
*  根據 TopicPublishInfo Topic發佈信息對象中維護的index,每次選擇隊列時,都會遞增
*  然後根據 index % queueSize 進行取餘,達到輪詢的效果
*
*/
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
        return tpInfo.selectOneMessageQueue(lastBrokerName);
}

/**
*  TopicPublishInfo Topic發佈信息對象中
*/
public class TopicPublishInfo {
    //基於線程上下文的計數遞增,用於輪詢目的
    private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();
   

    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();
        }
    }

    public MessageQueue selectOneMessageQueue() {
        int index = this.sendWhichQueue.getAndIncrement();
        int pos = Math.abs(index) % this.messageQueueList.size();
        if (pos < 0)
            pos = 0;
        return this.messageQueueList.get(pos);
    }
}

2.2 默認投遞方式的增強:基於Queue隊列輪詢算法和消息投遞延遲最小的策略投遞

默認的投遞方式比較簡單,但是也暴露了一個問題,就是有些Queue隊列可能由於自身數量積壓等原因,可能在投遞的過程比較長,對於這樣的Queue隊列會影響後續投遞的效果。
基於這種現象,RocketMQ在每發送一個MQ消息後,都會統計一下消息投遞的時間延遲,根據這個時間延遲,可以知道往哪些Queue隊列投遞的速度快。
在這種場景下,會優先使用消息投遞延遲最小的策略,如果沒有生效,再使用Queue隊列輪詢的方式。

public class MQFaultStrategy {
    /**
     * 根據 TopicPublishInfo 內部維護的index,在每次操作時,都會遞增,
     * 然後根據 index % queueList.size(),使用了輪詢的基礎算法
     *
     */
    public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
        if (this.sendLatencyFaultEnable) {
            try {
                // 從queueid 爲 0 開始,依次驗證broker 是否有效,如果有效
                int index = tpInfo.getSendWhichQueue().getAndIncrement();
                for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                    //基於index和隊列數量取餘,確定位置
                    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
                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);
    }
}

2.3 順序消息的投遞方式

上述兩種投遞方式屬於對消息投遞的時序性沒有要求的場景,這種投遞的速度和效率比較高。而在有些場景下,需要保證同類型消息投遞和消費的順序性。
例如,假設現在有TOPIC TOPIC_SALE_ORDER,該 Topic下有4個Queue隊列,該Topic用於傳遞訂單的狀態變遷,假設訂單有狀態:未支付已支付發貨中(處理中)發貨成功發貨失敗
在時序上,生產者從時序上可以生成如下幾個消息:
訂單T0000001:未支付 --> 訂單T0000001:已支付 --> 訂單T0000001:發貨中(處理中) --> 訂單T0000001:發貨失敗
消息發送到MQ中之後,可能由於輪詢投遞的原因,消息在MQ的存儲可能如下:

 

這種情況下,我們希望消費者消費消息的順序和我們發送是一致的,然而,有上述MQ的投遞和消費機制,我們無法保證順序是正確的,對於順序異常的消息,消費者 即使有一定的狀態容錯,也不能完全處理好這麼多種隨機出現組合情況。

基於上述的情況,RockeMQ採用了這種實現方案:對於相同訂單號的消息,通過一定的策略,將其放置在一個 queue隊列中,然後消費者再採用一定的策略(一個線程獨立處理一個queue,保證處理消息的順序性),能夠保證消費的順序性

 

 

至於消費者是如何保證消費的順序行的,後續再詳細展開,我們先看生產者是如何能將相同訂單號的消息發送到同一個queue隊列的:
生產者在消息投遞的過程中,使用了 MessageQueueSelector 作爲隊列選擇的策略接口,其定義如下:

package org.apache.rocketmq.client.producer;

import java.util.List;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;

public interface MessageQueueSelector {
        /**
         * 根據消息體和參數,從一批消息隊列中挑選出一個合適的消息隊列
         * @param mqs  待選擇的MQ隊列選擇列表
         * @param msg  待發送的消息體
         * @param arg  附加參數
         * @return  選擇後的隊列
         */
        MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
}

相應地,目前RocketMQ提供瞭如下幾種實現:

 

 

 

默認實現:

投遞策略 策略實現類 說明
隨機分配策略 SelectMessageQueueByRandom 使用了簡單的隨機數選擇算法
基於Hash分配策略 SelectMessageQueueByHash 根據附加參數的Hash值,按照消息隊列列表的大小取餘數,得到消息隊列的index
基於機器機房位置分配策略 SelectMessageQueueByMachineRoom 開源的版本沒有具體的實現,基本的目的應該是機器的就近原則分配

現在大概看下策略的代碼實現:

public class SelectMessageQueueByHash implements MessageQueueSelector {

    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        int value = arg.hashCode();
        if (value < 0) {
            value = Math.abs(value);
        }

        value = value % mqs.size();
        return mqs.get(value);
    }
}

實際的操作代碼樣例如下,通過訂單號作爲hash運算對象,就能保證相同訂單號的消息能夠落在相同的queue隊列上

rocketMQTemplate.asyncSendOrderly(saleOrderTopic + ":" + tag, msg,saleOrderId /*傳入訂單號作爲hash運算對象*/, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("SALE ORDER NOTIFICATION SUCCESS:{}",sendResult.getMsgId());
            }
            @Override
            public void onException(Throwable throwable) {
                 //exception happens
            }
        });

3. 如何爲消費者分配queue隊列?

RocketMQ對於消費者消費消息有兩種形式:

  • BROADCASTING:廣播式消費,這種模式下,一個消息會被通知到每一個消費者

  • CLUSTERING: 集羣式消費,這種模式下,一個消息最多隻會被投遞到一個消費者上進行消費
    模式如下:

     

廣播式的消息模式比較簡單,下面我們介紹下集羣式。對於使用了消費模式爲MessageModel.CLUSTERING進行消費時,需要保證一個消息在整個集羣中只需要被消費一次。實際上,在RoketMQ底層,消息指定分配給消費者的實現,是通過queue隊列分配給消費者的方式完成的:也就是說,消息分配的單位是消息所在的queue隊列。即:

queue隊列指定給特定的消費者後,queue隊列內的所有消息將會被指定到消費者進行消費。

RocketMQ定義了策略接口AllocateMessageQueueStrategy,對於給定的消費者分組,和消息隊列列表消費者列表當前消費者應當被分配到哪些queue隊列,定義如下:

/**
 * 爲消費者分配queue的策略算法接口
 */
public interface AllocateMessageQueueStrategy {

    /**
     * Allocating by consumer id
     *
     * @param consumerGroup 當前 consumer羣組
     * @param currentCID 當前consumer id
     * @param mqAll 當前topic的所有queue實例引用
     * @param cidAll 當前 consumer羣組下所有的consumer id set集合
     * @return 根據策略給當前consumer分配的queue列表
     */
    List<MessageQueue> allocate(
        final String consumerGroup,
        final String currentCID,
        final List<MessageQueue> mqAll,
        final List<String> cidAll
    );

    /**
     * 算法名稱
     *
     * @return The strategy name
     */
    String getName();
}

相應地,RocketMQ提供瞭如下幾種實現:

 

 

算法名稱 含義
AllocateMessageQueueAveragely 平均分配算法
AllocateMessageQueueAveragelyByCircle 基於環形平均分配算法
AllocateMachineRoomNearby 基於機房臨近原則算法
AllocateMessageQueueByMachineRoom 基於機房分配算法
AllocateMessageQueueConsistentHash 基於一致性hash算法
AllocateMessageQueueByConfig 基於配置分配算法

爲了講述清楚上述算法的基本原理,我們先假設一個例子,下面所有的算法將基於這個例子講解。

假設當前同一個topic下有queue隊列 10個,消費者共有4個,如下圖所示:

 

 

下面依次介紹其原理:

3.1. AllocateMessageQueueAveragely- 平均分配算法

這裏所謂的平均分配算法,並不是指的嚴格意義上的完全平均,如上面的例子中,10個queue,而消費者只有4個,無法是整除關係,除了整除之外的多出來的queue,將依次根據消費者的順序均攤。
按照上述例子來看,10/4=2,即表示每個消費者平均均攤2個queue;而10%4=2,即除了均攤之外,多出來2個queue還沒有分配,那麼,根據消費者的順序consumer-1consumer-2consumer-3consumer-4,則多出來的2個queue將分別給consumer-1consumer-2。最終,分攤關係如下:
consumer-1:3個;consumer-2:3個;consumer-3:2個;consumer-4:2個,如下圖所示:

 


其代碼實現非常簡單:

 

public class AllocateMessageQueueAveragely implements AllocateMessageQueueStrategy {
    private final InternalLogger log = ClientLogger.getLog();

    @Override
    public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
        List<String> cidAll) {
        if (currentCID == null || currentCID.length() < 1) {
            throw new IllegalArgumentException("currentCID is empty");
        }
        if (mqAll == null || mqAll.isEmpty()) {
            throw new IllegalArgumentException("mqAll is null or mqAll empty");
        }
        if (cidAll == null || cidAll.isEmpty()) {
            throw new IllegalArgumentException("cidAll is null or cidAll empty");
        }

        List<MessageQueue> result = new ArrayList<MessageQueue>();
        if (!cidAll.contains(currentCID)) {
            log.info("[BUG] ConsumerGroup: {} The consumerId: {} not in cidAll: {}",
                consumerGroup,
                currentCID,
                cidAll);
            return result;
        }

        int index = cidAll.indexOf(currentCID);
        int mod = mqAll.size() % cidAll.size();
        int averageSize =
            mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()
                + 1 : mqAll.size() / cidAll.size());
        int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
        int range = Math.min(averageSize, mqAll.size() - startIndex);
        for (int i = 0; i < range; i++) {
            result.add(mqAll.get((startIndex + i) % mqAll.size()));
        }
        return result;
    }

    @Override
    public String getName() {
        return "AVG";
    }
}

3.2 AllocateMessageQueueAveragelyByCircle -基於環形平均算法

環形平均算法,是指根據消費者的順序,依次在由queue隊列組成的環形圖中逐個分配。具體流程如下所示:

 


這種算法最終分配的結果是:
consumer-1: #0,#4,#8
consumer-2: #1, #5, # 9
consumer-3: #2,#6
consumer-4: #3,#7
其代碼實現如下所示:

 

/**
 * Cycle average Hashing queue algorithm
 */
public class AllocateMessageQueueAveragelyByCircle implements AllocateMessageQueueStrategy {
    private final InternalLogger log = ClientLogger.getLog();

    @Override
    public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
        List<String> cidAll) {
        if (currentCID == null || currentCID.length() < 1) {
            throw new IllegalArgumentException("currentCID is empty");
        }
        if (mqAll == null || mqAll.isEmpty()) {
            throw new IllegalArgumentException("mqAll is null or mqAll empty");
        }
        if (cidAll == null || cidAll.isEmpty()) {
            throw new IllegalArgumentException("cidAll is null or cidAll empty");
        }

        List<MessageQueue> result = new ArrayList<MessageQueue>();
        if (!cidAll.contains(currentCID)) {
            log.info("[BUG] ConsumerGroup: {} The consumerId: {} not in cidAll: {}",
                consumerGroup,
                currentCID,
                cidAll);
            return result;
        }

        int index = cidAll.indexOf(currentCID);
        for (int i = index; i < mqAll.size(); i++) {
            if (i % cidAll.size() == index) {
                result.add(mqAll.get(i));
            }
        }
        return result;
    }

    @Override
    public String getName() {
        return "AVG_BY_CIRCLE";
    }
}

3.3 AllocateMachineRoomNearby-基於機房臨近原則算法

該算法使用了裝飾者設計模式,對分配策略進行了增強。一般在生產環境,如果是微服務架構下,RocketMQ集羣的部署可能是在不同的機房中部署,其基本結構可能如下圖所示:

 


對於跨機房的場景,會存在網絡、穩定性和隔離心的原因,該算法會根據queue的部署機房位置和消費者consumer的位置,過濾出當前消費者consumer相同機房的queue隊列,然後再結合上述的算法,如基於平均分配算法在queue隊列子集的基礎上再挑選。相關代碼實現如下:

 

@Override
    public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
        List<String> cidAll) {
        //省略部分代碼
        List<MessageQueue> result = new ArrayList<MessageQueue>();

        //將MQ按照 機房進行分組
        Map<String/*machine room */, List<MessageQueue>> mr2Mq = new TreeMap<String, List<MessageQueue>>();
        for (MessageQueue mq : mqAll) {
            String brokerMachineRoom = machineRoomResolver.brokerDeployIn(mq);
            if (StringUtils.isNoneEmpty(brokerMachineRoom)) {
                if (mr2Mq.get(brokerMachineRoom) == null) {
                    mr2Mq.put(brokerMachineRoom, new ArrayList<MessageQueue>());
                }
                mr2Mq.get(brokerMachineRoom).add(mq);
            } else {
                throw new IllegalArgumentException("Machine room is null for mq " + mq);
            }
        }

        //將消費者 按照機房進行分組
        Map<String/*machine room */, List<String/*clientId*/>> mr2c = new TreeMap<String, List<String>>();
        for (String cid : cidAll) {
            String consumerMachineRoom = machineRoomResolver.consumerDeployIn(cid);
            if (StringUtils.isNoneEmpty(consumerMachineRoom)) {
                if (mr2c.get(consumerMachineRoom) == null) {
                    mr2c.put(consumerMachineRoom, new ArrayList<String>());
                }
                mr2c.get(consumerMachineRoom).add(cid);
            } else {
                throw new IllegalArgumentException("Machine room is null for consumer id " + cid);
            }
        }

        List<MessageQueue> allocateResults = new ArrayList<MessageQueue>();

        //1.過濾出當前機房內的MQ隊列子集,在此基礎上使用分配算法挑選
        String currentMachineRoom = machineRoomResolver.consumerDeployIn(currentCID);
        List<MessageQueue> mqInThisMachineRoom = mr2Mq.remove(currentMachineRoom);
        List<String> consumerInThisMachineRoom = mr2c.get(currentMachineRoom);
        if (mqInThisMachineRoom != null && !mqInThisMachineRoom.isEmpty()) {
            allocateResults.addAll(allocateMessageQueueStrategy.allocate(consumerGroup, currentCID, mqInThisMachineRoom, consumerInThisMachineRoom));
        }

        //2.不在同一機房,按照一般策略進行操作
        for (String machineRoom : mr2Mq.keySet()) {
            if (!mr2c.containsKey(machineRoom)) { // no alive consumer in the corresponding machine room, so all consumers share these queues
                allocateResults.addAll(allocateMessageQueueStrategy.allocate(consumerGroup, currentCID, mr2Mq.get(machineRoom), cidAll));
            }
        }

        return allocateResults;
    }

3.4 AllocateMessageQueueByMachineRoom- 基於機房分配算法

該算法適用於屬於同一個機房內部的消息,去分配queue。這種方式非常明確,基於上面的機房臨近分配算法的場景,這種更徹底,直接指定基於機房消費的策略。這種方式具有強約定性,比如broker名稱按照機房的名稱進行拼接,在算法中通過約定解析進行分配。
其代碼實現如下:

/**
 * Computer room Hashing queue algorithm, such as Alipay logic room
 */
public class AllocateMessageQueueByMachineRoom implements AllocateMessageQueueStrategy {
    private Set<String> consumeridcs;

    @Override
    public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
        List<String> cidAll) {
        List<MessageQueue> result = new ArrayList<MessageQueue>();
        int currentIndex = cidAll.indexOf(currentCID);
        if (currentIndex < 0) {
            return result;
        }
        List<MessageQueue> premqAll = new ArrayList<MessageQueue>();
        for (MessageQueue mq : mqAll) {
            String[] temp = mq.getBrokerName().split("@");
            if (temp.length == 2 && consumeridcs.contains(temp[0])) {
                premqAll.add(mq);
            }
        }

        int mod = premqAll.size() / cidAll.size();
        int rem = premqAll.size() % cidAll.size();
        int startIndex = mod * currentIndex;
        int endIndex = startIndex + mod;
        for (int i = startIndex; i < endIndex; i++) {
            result.add(mqAll.get(i));
        }
        if (rem > currentIndex) {
            result.add(premqAll.get(currentIndex + mod * cidAll.size()));
        }
        return result;
    }

    @Override
    public String getName() {
        return "MACHINE_ROOM";
    }

    public Set<String> getConsumeridcs() {
        return consumeridcs;
    }

    public void setConsumeridcs(Set<String> consumeridcs) {
        this.consumeridcs = consumeridcs;
    }

3.5 AllocateMessageQueueConsistentHash基於一致性hash算法

使用這種算法,會將consumer消費者作爲Node節點構造成一個hash環,然後queue隊列通過這個hash環來決定被分配給哪個consumer消費者
其基本模式如下:

 

 

什麼是一致性hash 算法 ?
一致性hash算法用於在分佈式系統中,保證數據的一致性而提出的一種基於hash環實現的算法,限於文章篇幅,不在這裏展開描述,有興趣的同學可以參考下 別人的博文:一致性哈希算法原理

算法實現上也不復雜,如下圖所示:

public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
        List<String> cidAll) {
        //省略部分代碼
        List<MessageQueue> result = new ArrayList<MessageQueue>();
        if (!cidAll.contains(currentCID)) {
            log.info("[BUG] ConsumerGroup: {} The consumerId: {} not in cidAll: {}",
                consumerGroup,
                currentCID,
                cidAll);
            return result;
        }

        Collection<ClientNode> cidNodes = new ArrayList<ClientNode>();
        for (String cid : cidAll) {
            cidNodes.add(new ClientNode(cid));
        }
//使用consumer id 構造hash環
        final ConsistentHashRouter<ClientNode> router; //for building hash ring
        if (customHashFunction != null) {
            router = new ConsistentHashRouter<ClientNode>(cidNodes, virtualNodeCnt, customHashFunction);
        } else {
            router = new ConsistentHashRouter<ClientNode>(cidNodes, virtualNodeCnt);
        }
        //依次爲 隊列分配 consumer
        List<MessageQueue> results = new ArrayList<MessageQueue>();
        for (MessageQueue mq : mqAll) {
            ClientNode clientNode = router.routeNode(mq.toString());
            if (clientNode != null && currentCID.equals(clientNode.getKey())) {
                results.add(mq);
            }
        }

        return results;

    }

3.6 AllocateMessageQueueByConfig--基於配置分配算法

這種算法單純基於配置的,非常簡單,實際使用中可能用途不大。代碼如下:

public class AllocateMessageQueueByConfig implements AllocateMessageQueueStrategy {
    private List<MessageQueue> messageQueueList;

    @Override
    public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
        List<String> cidAll) {
        return this.messageQueueList;
    }

    @Override
    public String getName() {
        return "CONFIG";
    }

    public List<MessageQueue> getMessageQueueList() {
        return messageQueueList;
    }

    public void setMessageQueueList(List<MessageQueue> messageQueueList) {
        this.messageQueueList = messageQueueList;
    }
}

3.7 消費者如何指定分配算法?

默認情況下,消費者使用的是AllocateMessageQueueAveragely算法,也可以自己指定:

public class DefaultMQPushConsumer{    
    /**
     * Default constructor.
     */
    public DefaultMQPushConsumer() {
        this(MixAll.DEFAULT_CONSUMER_GROUP, null, new AllocateMessageQueueAveragely());
    }

    /**
     * Constructor specifying consumer group, RPC hook and message queue allocating algorithm.
     *
     * @param consumerGroup Consume queue.
     * @param rpcHook RPC hook to execute before each remoting command.
     * @param allocateMessageQueueStrategy message queue allocating algorithm.
     */
    public DefaultMQPushConsumer(final String consumerGroup, RPCHook rpcHook,
        AllocateMessageQueueStrategy allocateMessageQueueStrategy) {
        this.consumerGroup = consumerGroup;
        this.allocateMessageQueueStrategy = allocateMessageQueueStrategy;
        defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook);
    }
}

4. 結束語

以上是從設計上簡單介紹了RocketMQ的投遞機制,如果想了解詳細的設計原理,可關注下方的我的公衆賬號,會同步更新,謝謝支持 ~
作者水平有限,歡迎留言指正吐槽~

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