【RocketMQ|源码分析】Producer是如何选择路由队列的?

概述

消息发送到RocketMQ服务器涉及到下面几个概念

image-20230321003216239

  • 生产者

生产者是消息生成的实体

  • 主题(Topic)

Topic是消息传输和存储的顶层容器,通常用于标识同意类型的业务逻辑消息,它是由多个队列组成,消息的存储和水平扩展是通过Topic内队列来实现的。

  • 队列(MessageQueue)

队列是消息存储和传输的实际单元容器,它相当于Kakfa中的分区(partition)。MessageQueue代码如下所示,它主要包含topic,brokername,queueId三个属性。

public class MessageQueue implements Comparable<MessageQueue>, Serializable {
    // topic
    private String topic;
    // broker名称
    private String brokerName;
    // queueId
    private int queueId;
}

Producer中3中发送消息的方式

我们通过Producer发送消息可以指定需要发送的队列,或者让Producer帮我们选择要发送的队列,从MessageQueue选择的角度来看,RocketMQ client的Producer提供了3种不同类型的消息推送方法

  • 自动选择MessageQueue

在发送消息时只传入Message,Producer会自动选择MessageQueue

SendResult send(final Message msg);
  • 将消息推送到指定的MessageQueue

发送消息时手动指定MessageQueue

SendResult send(final Message msg, final MessageQueue mq);
  • 通过MessageQueueSelector选择MessageQueue

根据用户定义的MessageQueueSelector选择要推送的MessageQueue

SendResult send(final Message msg, final MessageQueueSelector selector, final Object arg);

MessageQueueSelector是一个接口,我们可以根据需求自定义一个MessageQueue选择算法,将消息推送到指定的Messagequeue中。

public interface MessageQueueSelector {
    MessageQueue select(final List<MessageQueue> mqs/*当前Topic对应的MessageQueue*/, final Message msg, final Object arg);
}

官方也给我们提供了3个默认的MessageQueueSelector实现类

  1. 通过Hash选择MessageQueue

它通过参数arg的hash与mqs.size()取模,来选择要存储的队列

public class SelectMessageQueueByHash implements MessageQueueSelector {
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        int value = arg.hashCode() % mqs.size();
        if (value < 0) {
            value = Math.abs(value);
        }
        return mqs.get(value);
    }
}
  1. 随机选择MessageQueue

随机产生一个0 ≤ value ≤ mqs.size()的整数,来选择MessageQueue

public class SelectMessageQueueByRandom implements MessageQueueSelector {
    private Random random = new Random(System.currentTimeMillis());

    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        int value = random.nextInt(mqs.size());
        return mqs.get(value);
    }
}
  1. 根据机房信息选择MessageQueue

可以看到SelectMessageQueueByMachineRoom返回了一个null值,它需要我们根据场景实现,不能直接使用。

public class SelectMessageQueueByMachineRoom implements MessageQueueSelector {
    private Set<String> consumeridcs;

    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        return null;
    }

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

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

如果RocketMQ默认的MessageQueueSelector实现不能满足业务的需求,我们还可以根据自己的需要实现MessageQueueSelector。

Producer路由选择源码分析

DefaultMQProducer的send方法底层都是通过调用DefaultMQProducerImpl的方法实现的,DefaultMQProducerImpl是Producer方法的实现类。下面3个时序图展示了消息推送的过程,由于本文是分析Producer是如何选择MessageQueue的,因此时序图中的调用方法做了精简,仅保留路由选择相关的方法。

自动选择MessageQueue源码分析

如下图所示,如果用户调用send方法仅传入Message时,整个消息发送过程步骤如下

  1. DefaultMQProducer调用DefaultMQProducerImpl的send(Message)方法推送消息
  2. DefaultMQProducerImpl调用tryToFindTopicPublishInfo获得topic相关信息
  3. DefaultMQProducerImpl 调用selectOneMessageQueue获得MessageQueue
  4. DefaultMQProducerImpl调用sendKernelImpl推送消息,并返回推送结果(SendResult)

消息发送关键代码如下所示

private SendResult sendDefaultImpl(
    Message msg,
    final CommunicationMode communicationMode /*同步,异步,oneWay模式*/,
    final SendCallback sendCallback,
    final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
  	// 省略部分代码
    // 根据topic获取topic路由信息
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    String lastBrokerName = null == mq ? null : mq.getBrokerName();
  	// 省略部分代码
     // 选择MessageQueue
     MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
  	 // 省略部分代码
     // 推送消息消息
     sendResult = this.sendKernelImpl(msg,mq,communicationMode,sendCallback,topicPublishInfo,timeout-costTime);
     // 省略部分代码
     return sendResult;
     // 省略部分代码
}

我们来看下是如何根据topic获取Topic路由信息的(TopicPublishInfo),DefaultMQProducerImpl#tryToFindTopicPublishInfo源码如下

// topic和topic路由信息类,key是topic,
private final ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable =
        new ConcurrentHashMap<>();

private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    // 如果topic没有MessageQueue信息
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        // 从namesrv更新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;
    }
}

topic路由信息是缓存在DefaultMQProducerImpl中的Map中,获取topic路由信息首先会根据topic查询缓存Map中的路由信息,如果缓存中的路由信息不存在,则会去namesrv中查询topic的路由信息,并更新到本地缓存中。

查询到路由信息后,会调用selectOneMessageQueue方法选择MessageQueue。

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
  	// 是否打开发送延时故障启动机制 默认是关闭的
    if (this.sendLatencyFaultEnable) {
        try {
            // 获取MessageQueue选择索引,并+1
            int index = tpInfo.getSendWhichQueue().incrementAndGet();
            for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                // index与messageQueueSize取余,如果可用,则返回,否则选择下一个MessageQueue
                int pos = index++ % tpInfo.getMessageQueueList().size();
                MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                if (latencyFaultTolerance.isAvailable(mq.getBrokerName()))
                    return mq;
            }
            // 随机选择一个broker
            final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
            // 轮询选择一个写队列
            int writeQueueNums = tpInfo.getWriteQueueIdByBroker(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();
    }
		// 轮询选择MessageQueue
    return tpInfo.selectOneMessageQueue(lastBrokerName);
}

选择MessageQueue流程如下

image-20230323191154292

指定MessageQueue

发送消息时指定MessageQueue,消息推送过程就不需要再选择MessageQueue。

image-20230323163039718

通过MessageQueueSelector选择MessageQueue

发送消息时传入MessageQueueSelector,消息推送过程如下

  1. DefaultMQProducerImpl调用tryToFindTopicPublishInfo获得topic相关信息
  2. List<MessageQueue>传入selector,选择MessageQueue
  3. DefaultMQProducerImpl调用sendKernelImpl推送消息,并返回推送结果(SendResult)

image-20230323163016779

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值