文章目录

  • 一、基本用法
  • 二、消息发送原理
  • 2.1 生产者启动
  • 2.2 查找主题路由信息
  • 2.2.1 几个关键的元数据:
  • 2.2.2 查找过程
  • 2.3 选择消息队列(MessageQueue)
  • 2.4 发送消息
  • 三、关键源码
  • 3.1 启动源码
  • 3.2 查找主题路由元素
  • 3.3 选择消息队列


一、基本用法

基本用法主要有同步发送,异步发送和指定队列发送,具体见下。这里主要介绍

// 1. 创建生产者对象,指定生产者group。生产者group的目的是当MQ需要回查事务状态时,会选择group中的任意一个producer进行会擦汗
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");

//2. 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876;localhost:9878");

//3. 是否启用broker容错规避机制
// 默认不启用,如果启用,则发送消息失败时,会暂时规避发送失败的Broker
producer.setSendLatencyFaultEnable(true);

// 4.设置同步发送失败重试次数,默认是2次,这里重试的次数是在超时时间内,也就是说
// 无论重试多少次,总耗时都不会超过超时时间
producer.setRetryTimesWhenSendFailed(10);

// 5. 启动生产者,全局整个进程只有一个MQClientInstance实例,可以有多个生产消费者,每个生产消费者注册到MQClientInstance实例上
producer.start();

// 6. 创建一个消息对象,设置主题,tag和消息体
Message msg = new Message("topic1" /* Topic */,
    "TagA" /* Tag */,
    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);


//7.1 同步发送
SendResult sendResult = producer.send(msg);

//7.2 异步发送
  producer.send(msg, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        System.out.printf("%s%n", sendResult);


    }
    // 当Broker返回错误时才会调用,超时不会
    @Override
    public void onException(Throwable e) {
        System.out.printf("%s%n", e);
    }
});

// 7.3 局部顺序消息,指定队列发送
producer.send(msg, new MessageQueueSelector() {
        @Override
        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
            MessageQueue q = mqs.get(0);
            System.out.println(q);
            return mqs.get(0);
        }
        // 这个i参数就是传给MessageQueueSelector接口的select方法的第三个参数
    }, i);

二、消息发送原理

消息发送主要包含以下几步:

  1. 启动,实例化MQClientInstance或将生产者注册到MQClientInstance。
  2. 查找主题路由信息。
  3. 从路由信息中,选择要发送的消息队列。
  4. 发送消息。

2.1 生产者启动

MQClientInstance一般全局只有一个,创建生产者时会将生产者加入到MQClientInstance中进行管理,以便后续调用网络请求,心跳检测等。

启动的关键代码在DefaultMQProducerImpl类中:

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

关键对象是MQClientInstance,它记录了主题路由信息,同时连接着通信客户端,进行网请求。

2.2 查找主题路由信息

2.2.1 几个关键的元数据:

每个消息发送者的每个主题都依赖一个元数据:TopicPublishInfo:

  1. TopicPublishInfo:
    属性:
  • orderTopic是否顺序消息
  • List messageQueueList 该主题队列的消息队列,MessageQueue有三个属性:topic,brokerName和queueId
  • sendWhichQueue,用于选择消息队列,每选择一次消息队列就自增一。
  • TopicRouteData 主题路由数据,主要包含:
  • List topic队列,一个topic可能有多个Broker,每个Broker有多个队列,每个队列是一个QueueData
  • List topic分布的broker元数据,一个BrokderData代表一个Broker

2.2.2 查找过程

  1. 使用主题查询NameSever,获取TopicRouteData对象,然后跟本地TopicRouteData比较,如果变更了,则向下执行。
  2. 更新MQClientInstance中的本地broker缓存,将TopicRouteData封装成TopicPublishInfo。
  3. 封装过程是遍历QueueData列表,找到所有可写的QueueData和合法的QueueData,创建MessageQueue,在topicRouteData2TopicPublishInfo方法中创建了messageQueueList。
  4. 然后更新MQClientInstance管理的生产者的TopicPublishInfo。

2.3 选择消息队列(MessageQueue)

  1. 根据TopicPublishInfo对象中的sendWhichQueue值从messageQueueList队列中选择一个MessageQueue,每选择一次消息队列加一。
  2. 选择消息队列采用重试机制,由retryTimesWhenSendFailed配置指定同步方式进行重试。每次失败后将选择下一个MessageQueue。
  3. 如果开启了Broker故障延迟机制,则将该Broker暂时排除在消息队列选择范围中。
  4. 当指定Selector时,就是从messageQueueList队列中选一个,这个是Selector中的一个参数

2.4 发送消息

发送消息有三种方式,同步发送,异步发送和单向发送。

发送消息的状态结果有4种,分别是:

  1. FLUSH_DISK_TIMEOUT。
  2. FLUSH_SLAVE_TIMEOUT.
  3. SLAVE_NOT_AVAILABLE.
  4. SEND_OK
    这四种状态需要结合配置的刷盘策略(同步刷盘,异步刷盘)和主从同步策略来分析。

同步和异步发送的区别:

  1. 同步发送时,需要阻塞等待netty中的 channel.writeAndFlush(request).addListener()方法中对ResponseFuture设置结果,
    而异步发送是在接收到服务端的响应后,在SimpleChannelInboundHandler类中的channelRead0方法中触发,EventLoop线程中执行回调函数或者在EventLoop线程中将任务放到一个ExecutorService中执行,不会同步等待结果。
  2. 同步发送重试会使用retryTimesWhenSendFailed进行重试,就算是服务端超时了也会重试,是在外层。 而异步重试必须是收到了的服务端的异常响应包才会重试,受到retryTimesWhenSendAsyncFailed参数影响。

三、关键源码

3.1 启动源码

启动主要是MQClientInstance对象的实例化和注册,MQClientInstance封装了RocketMQ网络处理API,是生产者消费者和NameServer,Broker打交道的网络通道。
DefaultMQProducerImpl类中的start方法

public void start(final boolean startFactory) throws MQClientException {
        switch (this.serviceState) {
            case CREATE_JUST:
                this.serviceState = ServiceState.START_FAILED;

                this.checkConfig();

                if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
                    this.defaultMQProducer.changeInstanceNameToPID();
                }
                // 这里使用单例模式获得了MQClientInstance对象
                this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);
                // 同时将当前生产者注册到MQClientInstance对象中
                boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
                if (!registerOK) {
                    this.serviceState = ServiceState.CREATE_JUST;
                    throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
                        + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                        null);
                }
                
               //....后面代码省略

3.2 查找主题路由元素

这一块是贯穿了整个发送过程中的关键代码,是DefaultMQProducerImpl类中的sendDefaultImpl方法

// 一、查找主题路由信息
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    if (topicPublishInfo != null && topicPublishInfo.ok()) {
        boolean callTimeout = false;
        MessageQueue mq = null;
        Exception exception = null;
        SendResult sendResult = null;
        // 注意这里,外层同步发送时会考虑getRetryTimesWhenSendFailed,异步时不会重试
        int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
        int times = 0;
        String[] brokersSent = new String[timesTotal];
        for (; times < timesTotal; times++) {
            System.out.println("同步重试第:{}此, 一共:{}次"  + times + "  "  + timesTotal);
            log.info("同步重试第:{}此, 一共:{}次", times, timesTotal);
            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;
                    System.out.println("超时时间是"+ timeout);
                    // 如果重试后调用超时则直接break不再重试
                    if (timeout < costTime) {
                        System.out.println("已经超时了,耗时"+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) {
                // 后面代码省略.....

从上面代码可以看出,查找路由信息的方法是在tryToFindTopicPublishInfo中,它返回了TopicPublishInfo元数据。

private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    // 1. 从缓存中获取
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    // 2. 如果缓存中无有效路由信息,则从NameServer中获取
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        // 3. 这个方法是由MQClientInstance去调用NameServer同时更新所有生产者和消费者的路由信息,这个过程需要上锁进行同步
        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;
    }
}

Namserver是有多个的,每次选一个,就算每个NameServer存的路由信息不一致也没关系。

3.3 选择消息队列

在3.2中我们可以看到这一段代码,可以看到,同步发送时这里用到了getRetryTimesWhenSendFailed来计算重试次数,异步时只执行一次。每次都调用selectOneMessageQueue来获取一个MessageQueue元数据,它代表一个Broker的一个队列。同时整体重试时间不超过timeout,如果超过直接break。

// 注意这里,外层同步发送时会考虑getRetryTimesWhenSendFailed,异步时不会重试
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
String[] brokersSent = new String[timesTotal];
for (; times < timesTotal; times++) {
    log.info("同步重试第:{}此, 一共:{}次", times, timesTotal);
    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;
            System.out.println("超时时间是"+ timeout);
            // 如果重试后调用超时则直接break不再重试
            if (timeout < costTime) {
                System.out.println("已经超时了,耗时"+costTime);
                callTimeout = true;
                break;
            }
    //.... 省略循环体后面的代码

这里selectOneMessageQueue有三种方式,一种是开启了Broker规避,一种是未开启。如果未开启则很简单,只是根据sendWhichQueue值从messageQueueList队列中选择一个MessageQueue,每选择一次消息队列加一,依次遍历所有的MessageQueue。
如果开始了Broker规避,则会跳过失败的Broker。

另外一种是直接用户根据参数指定选择某一个队列,也就是直接从messageQueueList选择一个队列进行消息发送。

为什么要规避:
Broker不可用后,路由信息短期内还是包含这个Broker的,因为

  1. 首先NameServer检测Broker是否可用是有间隔的,一般是10秒。
  2. NameServer检测到Broker挂了之后不会马上推送消息给客户端,而是客户端每隔30秒更新一次路由信息,也就是Broker挂了之后客户端最快要等30秒才能将这个Broker从客户端移除。
  3. 所以一次消息发送失败后,可以将Broker暂时排除在消息队列的选择范围中。