rocketmq-消息路由

背景

先从MessageQueue入手,即先从细节入手。

细节的话,就是从MessageQueue核心类入手。

为什么要从这个类入手?因为它包含了路由的核心字段。

说白了,路由的本质,就是写消息到哪个broker节点。

rocketmq比较特殊一点,除了要路由到哪一个broker节点,还需要知道路由到哪一个队列。因为一个broker节点包含了多个队列------本质是因为一个topic包含了多个队列,而实际生产环境,一般都是集群部署,比如2主2从,即包含了2个broker节点。那么平均下来,就是一个broker节点包含了2个队列。

MessageQueue

消息队列,包含了核心字段:

1、哪个节点

2、哪个队列

java 复制代码
/**
 * 作用:消息队列,主要作用是路由,不是逻辑存储,也不存储消息
 *
 * 只和发送者有关系,和消费者没有关系
 *
 * @author gzh
 */
public class MessageQueue implements Comparable<MessageQueue>, Serializable {
   
    private String topic; //topic名字
    private String brokerName; //存储节点名字。生产者可以根据名字找到存储节点。
    private int queueId; //消息队列id。brokerName是哪个节点,queueId是哪个队列。

说白了,其实就是这个类,决定了消息写到哪个节点的哪个队列。

因为一个topic包含了多个队列,默认是包含4个队列。

架构图

队列长什么样子?

这个是实际的截图

队列id的值是什么?其实就是0、1、2、3。每个队列,对应一个物理文件。


架构图

一个topic,包含4个队列。每个队列,包含多个消息。

注意,前面的截图0123是在同一个broker节点,因为那是单机。实际生产环境,如果包含多个broker节点,比如包含2个broker节点,那么队列是会分布到不同的broker节点的,即每个broker节点包含2个队列,因为要负载均衡。


集群部署

每个topic,都包含了多个队列。

一般情况下,每个broker,都包含了多个队列------目的是把topic分片到不同的broker节点。


控制台看到的MessageQueue的样子


实际的发送结果,下面是发送成功的情况:

为什么要包含多个队列?

因为要负载均衡啊,如果只有一个队列,那么该topic所有的消息,都只会写在同一个broker节点,那会很要命,性能很低。所以本质其实是为了分片,提高topic的性能。

你说是往同一个broker节点写数据快呢,还是分片到不同的broker节点快呢?答案显而易见。

队列是最小存储单元

一个队列也是对应一个具体的物理文件。

负载均衡

一般的负载均衡,只需要负载均衡到哪个节点即可。比如nginx负载均衡到哪个tomcat节点。

而rocketmq,稍微特殊一点,不仅要负载均衡到哪个broker,还要负载均衡到哪个队列。

因为一个broker一般都会包含多个队列。

哪个broker?

算法:默认是按顺序。


源码分析

入口是在发送消息

java 复制代码
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());
    if (topicPublishInfo != null && topicPublishInfo.ok()) {
        boolean callTimeout = false;
        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 mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
            if (mqSelected != null) {
                mq = mqSelected;
                brokersSent[times] = mq.getBrokerName();
                try {
                    beginTimestampPrev = System.currentTimeMillis();
                    if (times > 0) {
                        //Reset topic with namespace during resend.
                        msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()));
                    }
                    long costTime = beginTimestampPrev - beginTimestampFirst;
                    if (timeout < costTime) {
                        callTimeout = true;
                        break;
                    }

                    //发送消息
                    sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                    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);
                    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;
                    if (this.defaultMQProducer.getRetryResponseCodes().contains(e.getResponseCode())) {
                        continue;
                    } else {
                        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 (callTimeout) {
            throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
        }

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

    validateNameServerSetting();

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

调用selectOneMessageQueue方法

java 复制代码
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
    return this.mqFaultStrategy.selectOneMessageQueue(tpInfo, lastBrokerName);
}

真正的路由方法

java 复制代码
/**
 * 选择一个消息队列:按顺序选择。和选择队列的算法一样,也是按顺序。
 *
 * @param tpInfo
 * @param lastBrokerName
 * @return org.apache.rocketmq.common.message.MessageQueue
 * @author gzh
 */
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
    if (this.sendLatencyFaultEnable) {
        try {
            // 先累加
            int index = tpInfo.getSendWhichQueue().incrementAndGet();
            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()))
                    return mq;
            }

            // 选择一个相对好的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().incrementAndGet() % 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);
}

先累加的本质,其实就是按顺序。


方法调用栈

java 复制代码
-   MQFaultStrategy.selectOneMessageQueue(TopicPublishInfo, String) (org.apache.rocketmq.client.latency)
  -   DefaultMQProducerImpl.selectOneMessageQueue(TopicPublishInfo, String) (org.apache.rocketmq.client.impl.producer)
    -   DefaultMQProducerImpl.sendDefaultImpl(Message, CommunicationMode, SendCallback, long) (org.apache.rocketmq.client.impl.producer)

哪个队列?

算法:也是按顺序。

其实刚才上面的源码,已经回答了这个问题,因为在选择哪一个broker节点的时候,就已经选择了哪一个队列------因为消息队列包含了哪一个节点字段和哪一个队列字段。

说白了,一开始是有一个消息队列集合:

java 复制代码
private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>(); //消息队列集合。一个topic包含多个消息队列,每个消息队列可能分布在不同的broker存储节点。

然后呢,根据按顺序的算法,从集合里获取一个消息队列MessageQueue。然后这个消息队列MessageQueue,就已经包含了:

1、哪个broker节点

2、哪个队列

也就是说,创建消息队列集合的时候,每个元素就已经包含了哪个broker和哪个队列。

所谓路由,无非就是按某种算法,从中取一个即可。

这就是负载均衡的本质。

总结

单机

假设是单机,那么一个topic包含4个队列。

由于是单机,所以一个broker包含了4个队列。因为这个broker是唯一的broker嘛。

这个时候的路由,就不需要路由broker了,因为只有一个broker节点。所以这个时候,只需要路由队列即可。

可以发现,broker节点是节点粒度的路由,而队列是更细粒度的路由。本质是节点粒度分片之后,再在节点内部进行更细粒度的分片------说白了,就是分片的粒度更细了,从而更高的提高性能。

集群

假设是2个broker节点,那么一个topic的4个队列,平均分配到2个broker节点------那么每个broker节点就包含了2个队列。

所以,这个时候,既需要路由broker节点,又需要路由队列。

小结

路由或者负载均衡的本质,无非就是路由到哪个节点而已。

比如数据库分库的路由,redis的路由,基本上都差不多。

无非是负载均衡的算法,稍微有点区别。有的是哈希取模,有的是按顺序,有的是随机。

而rocketmq相当于在节点分片之后,继续在节点内部进行了更细粒度的分片,即rocketmq分片的最小单位是队列,而不是节点。

这个就有点类似数据库分库之后,还进行了分表。分库是不同的节点。而分表是一个数据库节点内部包含了多个表,就相当于这里的队列。

参考

xie.infoq.cn/article/05c...

www.cnblogs.com/mingyueyy/p...

相关推荐
不想睡觉的橘子君2 天前
【MQ】RabbitMQ、RocketMQ、kafka特性对比
kafka·rabbitmq·rocketmq
厌世小晨宇yu.3 天前
RocketMQ学习笔记
笔记·学习·rocketmq
洛卡卡了4 天前
如何选择最适合的消息队列?详解 Kafka、RocketMQ、RabbitMQ 的使用场景
kafka·rabbitmq·rocketmq
菜鸟起航ing5 天前
Spring Cloud Alibaba
spring cloud·java-ee·rocketmq
乄bluefox5 天前
学习RocketMQ(记录了个人艰难学习RocketMQ的笔记)
java·spring boot·中间件·rocketmq
虽千万人 吾往矣7 天前
golang rocketmq开发
开发语言·golang·rocketmq
HippoSystem8 天前
[RocketMQ 5.3.1] Win11 + Docker Desktop 本地部署全流程 + 踩坑记录
rocketmq
幸运小锦李先生12 天前
基于RabbitMQ,Redis,Redisson,RocketMQ四种技术实现订单延时关闭功能及其相关优缺点介绍(以12306为主题)
redis·rabbitmq·rocketmq·redisson·1024程序员节
₁ ₀ ₂ ₄14 天前
一篇文章了解RocketMQ基础知识。
分布式·中间件·rocketmq·1024程序员节
炭烤玛卡巴卡15 天前
【MacOS】RocketMQ 搭建Java客户端
macos·rocketmq·java-rocketmq