RocketMQ源码分析之普通消息发送


前言

  本篇文章来分析producer发送消息的流程,包括Message对象属性详解、producer启动流程、消息发送流程、producer获取路由信息、producer选择MessageQueue机制以及producer端消息发送重试机制。在分析producer获取路由信息时会详细分析TBW102的作用。


一、Message对象

  在RocketMQ的客户端将消息封装为Message对象,其主要属性及其说明具体如下:

字段名说明
topic必填,topic的名称
flag选填,由应用来设置,RocketMQ不做干预
properties选填,存储了Message其余各项参数,比如tag、key等关键的消息属性。RocketMQ预定义了一组内置属性,除了内置属性之外,还可以设置任意自定义属性。
body必填,消息体
transactionId事务消息相关
keys选填,代表消息的业务关键词
tags选填,消息标签,方便服务器过滤使用,目前只支持每条消息设置一个tag

二、DefaultMQProducer启动流程

1.DefaultMQProducer启动流程图

  DefaultMQProducer的启动流程图如下图所示,其对应的代码是DefaultMQProducerImpl.java中的start(final boolean startFactory)方法。
在这里插入图片描述
  在DefaultMQProducer的启动过程有两个重要部分需要了解:
  (1)MQClientInstance的启动过程
  (2)为什么要在topicPublishInfoTable<topic,topicPublishInfoTable>中添加topic为TBW102的信息?这个问题将在producer获取路由信息部分详细解释。

2.MQClientInstance启动

  MQClientInstance启动的过程如下,其对应的是MQClientInstance.java的start()方法。

public void start() throws MQClientException {

        synchronized (this) {
            switch (this.serviceState) {
                case CREATE_JUST:
                    this.serviceState = ServiceState.START_FAILED;
                    // If not specified,looking address from name server
                    if (null == this.clientConfig.getNamesrvAddr()) {
                        this.mQClientAPIImpl.fetchNameServerAddr();
                    }
                    // Start request-response channel
                    this.mQClientAPIImpl.start();
                    // Start various schedule tasks
                    this.startScheduledTask();
                    // Start pull service
                    this.pullMessageService.start();
                    // Start rebalance service
                    this.rebalanceService.start();
                    // Start push service
                    this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
                    log.info("the client factory [{}] start OK", this.clientId);
                    this.serviceState = ServiceState.RUNNING;
                    break;
                case START_FAILED:
                    throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
                default:
                    break;
            }
        }
    }

  其执行过程可以使用下图来描述:
在这里插入图片描述
  在MQClientInstance的启动过程中需要关注以下问题:
  (1)客户端采用的通信模型是RocketMQ自定义的通信协议并在Netty的基础之上扩展的通信模块,其具体实现是NettyRemotingClient,详细内容可以参考笔者之前的学习笔记:RocketMQ源码分析之通信模块
  (2)PullMessageService与RebalanceService都是与consumer相关,其中RebalanceService负责consumer端负载均衡以及将重新分配的MessageQueue构建PullRequest请求并将其放到PullMessageService服务中的pullRequestQueue队列中,PullMessageService负责consumer端消息拉取。(这里可以参考笔者的学习笔记:RocketMQ源码分析之RebalanceServiceRocketMQ源码分析之消息拉取流程

3.producerGroup限制条件总结

(1)producerGroup不能为null
(2)producerGroup不能为DEFAULT_PRODUCER、CLIENT_INNER_PRODUCER
(3)producerGroup的长度不能超过255
(4)producerGroup可以包含%、字母大小写、数字、-、_


三、消息发送流程

  在RocketMQ中消息发送分为三种类型分别是同步、异步和单向,这三种发送方式的基本步骤是相同的,可以概括为以下步骤:
  (1)检查topic及发送的消息
  (2)获取topic路由信息
  (3)选择MessageQueue
  (4)按照发送消息的类型选择对应的方式发送消息
  根据发送过程中的第一步可以总结下topic以及消息的限制条件,具体如下:
  topic的限制条件:
  (1)topic不能为空
  (2)topic的名称可以包含%、字母大小写、数字、-、_
  (3)topic名称长度不能超过127
  (4)topic名称不能是TBW102、SCHEDULE_TOPIC_XXXX、BenchmarkTest、RMQ_SYS_TRANS_HALF_TOPIC、RMQ_SYS_TRACE_TOPIC、RMQ_SYS_TRANS_OP_HALF_TOPIC、TRANS_CHECK_MAX_TIME_TOPIC、SELF_TEST_TOPIC、OFFSET_MOVED_EVENT
  消息的限制条件:
  (1)消息不能为null
  (2)消息的body字段不能为null或者消息的body字段超度不能为0
  (3)消息的body字段长度不能超过maxMessageSize,默认是4M,客户端可以通过setMaxMessageSize方法来进行修改


四、producer获取路由信息

  在发送消息前producer会从namesrv来获取目标topic的路由信息,其具体实现是在tryToFindTopicPublishInfo(final String topic) 方法中,具体如下:

private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
        TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
        //producer第一次发送消息的情况
        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 {
        	//topicPublishInfo中没有找到topic路由信息的情况下,使用“TBW102”默认的topic的配置来构建目标topic的路由信息
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
            return topicPublishInfo;
        }
    }

  上面方法中从namesrv更新topic路由信息的方法调用的是同一个函数updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault, DefaultMQProducer defaultMQProducer),其区别是在调用时传递的参数是不一样的,在第一次调用是传递的参数是(topic, false, null)第二次调用时传递的是(topic, true, this.defaultMQProducer)接下来详细分析下该方法:
  1.producer在第一次发送消息时如果topicPublishInfoTable中没有目标topic的信息则会执行getTopicRouteInfoFromNameServer(topic, 1000 * 3)来获取目标topic的路由信息,请求类型是RequestCode.GET_ROUTEINFO_BY_TOPIC
  2.如果步骤1中获取的topicRouteData不为空,会先从producer端的缓存topicRouteTable中获取目标topic旧的路由信息,然后执行topicRouteDataIsChange方法来判断topic的路由信息是否发生变化,如果发生变化则更新brokerAddrTable、topicPublishInfoTable和topicRouteTable
  3.tryToFindTopicPublishInfo方法中如果在更新topic路由信息后topicPublishInfo中还没有目标topic的路由信息并且messageQueueList为空则在执行updateTopicRouteInfoFromNameServer方法从namesrv获取路由信息时获取“TBW102”topic的路由信息(前提是broker端autoCreateTopicEnable配置项的值为true),然后与本地缓存的路由信息进行对比是否发生变化,如果发生了变化则以该路由信息来更新brokerAddrTable、topicPublishInfoTable和topicRouteTable。这里有一点需要注意,在从namesrv获取“TBW102”的路由信息后会对其queueDatas进行遍历,然后以defaultTopicQueueNums和readQueueNums中较小的队列数更新其读写队列数

public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
        DefaultMQProducer defaultMQProducer) {
        try {
            if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
                try {
                    TopicRouteData topicRouteData;
                    //producer
                    if (isDefault && defaultMQProducer != null) {
                        topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
                            1000 * 3);
                        if (topicRouteData != null) {
                            for (QueueData data : topicRouteData.getQueueDatas()) {
                                int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
                                data.setReadQueueNums(queueNums);
                                data.setWriteQueueNums(queueNums);
                            }
                        }
                    //producer第一次发送消息情况
                    } else {
                        topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
                    }
                    if (topicRouteData != null) {
                        TopicRouteData old = this.topicRouteTable.get(topic);
                        boolean changed = topicRouteDataIsChange(old, topicRouteData);
                        if (!changed) {
                            changed = this.isNeedUpdateTopicRouteInfo(topic);
                        } else {
                            log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
                        }

                        if (changed) {
                            TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();

                            for (BrokerData bd : topicRouteData.getBrokerDatas()) {
                                this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
                            }

                            // Update Pub info
                            {
                                TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
                                publishInfo.setHaveTopicRouterInfo(true);
                                Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQProducerInner> entry = it.next();
                                    MQProducerInner impl = entry.getValue();
                                    if (impl != null) {
                                        impl.updateTopicPublishInfo(topic, publishInfo);
                                    }
                                }
                            }

                            // Update sub info
                            {
                                Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
                                Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQConsumerInner> entry = it.next();
                                    MQConsumerInner impl = entry.getValue();
                                    if (impl != null) {
                                        impl.updateTopicSubscribeInfo(topic, subscribeInfo);
                                    }
                                }
                            }
                            log.info("topicRouteTable.put. Topic = {}, TopicRouteData[{}]", topic, cloneTopicRouteData);
                            this.topicRouteTable.put(topic, cloneTopicRouteData);
                            return true;
                        }
                    } else {
                        log.warn("updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}. [{}]", topic, this.clientId);
                    }
                } catch (MQClientException e) {
                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                        log.warn("updateTopicRouteInfoFromNameServer Exception", e);
                    }
                } catch (RemotingException e) {
                    log.error("updateTopicRouteInfoFromNameServer Exception", e);
                    throw new IllegalStateException(e);
                } finally {
                    this.lockNamesrv.unlock();
                }
            } else {
                log.warn("updateTopicRouteInfoFromNameServer tryLock timeout {}ms. [{}]", LOCK_TIMEOUT_MILLIS, this.clientId);
            }
        } catch (InterruptedException e) {
            log.warn("updateTopicRouteInfoFromNameServer Exception", e);
        }

        return false;
    }

  到这里获取并更新topic路由信息就解释完了,但是还有一个疑问,“TBW102”的作用是什么呢?因为getDefaultTopicRouteInfoFromNameServer方法会从namesrv来获取其路由信息,而namesrv端的路由信息的来源是broker,所以这个问题可以先从broker端分析它是在什么情况下被创建的?通过查找发现broker在启动的过程中会初始化BrokerController对象,在其构造函数中初始化TopicConfigManager对象,在TopicConfigManager的构造函数中有以下代码,其逻辑是如果broker端的配置项autoCreateTopicEnable的值为true则创建“TBW102”并会将其缓存最终会通过心跳信息发给namesrv。

{
            if (this.brokerController.getBrokerConfig().isAutoCreateTopicEnable()) {
                String topic = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC;
                TopicConfig topicConfig = new TopicConfig(topic);
                TopicValidator.addSystemTopic(topic);
                topicConfig.setReadQueueNums(this.brokerController.getBrokerConfig()
                    .getDefaultTopicQueueNums());
                topicConfig.setWriteQueueNums(this.brokerController.getBrokerConfig()
                    .getDefaultTopicQueueNums());
                int perm = PermName.PERM_INHERIT | PermName.PERM_READ | PermName.PERM_WRITE;
                topicConfig.setPerm(perm);
                this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig);
            }
        }

  这样就不难理解producer为什么会从namesrv获取到“TBW102”的路由信息。到这里会有另一个疑问,producer在选择好MessageQueue后发送消息,broker接收请求后会不会创建topic?这个可以从SendMessageProcessor处理写消息请求来分析,在处理写请求的开始会调用msgCheck(final ChannelHandlerContext ctx, final SendMessageRequestHeader requestHeader, final RemotingCommand response)方法检查当前broker的权限是否可写、消息的topic是否有效。在该方法中有个很重要的处理逻辑:从broker的缓存中获取topic的配置信息topicConfig,如果topicConfig为空(即broker端没有创建该topic)则会调用createTopicInSendMessageMethod(final String topic, final String defaultTopic, final String remoteAddress, final int clientDefaultTopicQueueNums, final int topicSysFlag)方法来创建topic的配置信息将其添加到缓存中然后持久化,由于是新添加的所以还会调用registerBrokerAll方法来将其注册到namesrv中。

protected RemotingCommand msgCheck(final ChannelHandlerContext ctx,
        final SendMessageRequestHeader requestHeader, final RemotingCommand response) {
        if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission())
            && this.brokerController.getTopicConfigManager().isOrderTopic(requestHeader.getTopic())) {
            response.setCode(ResponseCode.NO_PERMISSION);
            response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
                + "] sending message is forbidden");
            return response;
        }

        if (!TopicValidator.validateTopic(requestHeader.getTopic(), response)) {
            return response;
        }
        if (TopicValidator.isNotAllowedSendTopic(requestHeader.getTopic(), response)) {
            return response;
        }

        TopicConfig topicConfig =
            this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic());
        if (null == topicConfig) {
            int topicSysFlag = 0;
            if (requestHeader.isUnitMode()) {
                if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
                } else {
                    topicSysFlag = TopicSysFlag.buildSysFlag(true, false);
                }
            }

            log.warn("the topic {} not exist, producer: {}", requestHeader.getTopic(), ctx.channel().remoteAddress());
            topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageMethod(
                requestHeader.getTopic(),
                requestHeader.getDefaultTopic(),
                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
                requestHeader.getDefaultTopicQueueNums(), topicSysFlag);

            if (null == topicConfig) {
                if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    topicConfig =
                        this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
                            requestHeader.getTopic(), 1, PermName.PERM_WRITE | PermName.PERM_READ,
                            topicSysFlag);
                }
            }

            if (null == topicConfig) {
                response.setCode(ResponseCode.TOPIC_NOT_EXIST);
                response.setRemark("topic[" + requestHeader.getTopic() + "] not exist, apply first please!"
                    + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
                return response;
            }
        }

        int queueIdInt = requestHeader.getQueueId();
        int idValid = Math.max(topicConfig.getWriteQueueNums(), topicConfig.getReadQueueNums());
        if (queueIdInt >= idValid) {
            String errorInfo = String.format("request queueId[%d] is illegal, %s Producer: %s",
                queueIdInt,
                topicConfig.toString(),
                RemotingHelper.parseChannelRemoteAddr(ctx.channel()));

            log.warn(errorInfo);
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark(errorInfo);

            return response;
        }
        return response;
    }

  最后来总结关于“TBW102”的问题:
  1.在broker端配置项autoCreateTopicEnable为true的情况下,broker在启动的过程中会创建“TBW102”
  2.producer在第一次发送消息获取topic路由信息时,由于topic的路由信息不存在,所以在producer端会获取TBW102的路由信息,并根据TBW102的路由信息来构建topic的路由信息。由于topic使用的是TBW102的路由信息,所以会将消息发送到TBW102所在的broker,这样在该topic的消息量大的情况下,会造成某些broker上的负载过大,存储就不会负载均衡了
  3.broker在接收到客户端的写请求后,会先对消息进行检查,在broker的缓存中查看是否存在topic的信息,如果不存在且autoCreateTopicEnable为true则首先会创建topic,这是自动创建topic的机制
  4.综上所述TBW102会起到topic配置模板的作用


五、producer选择MessageQueue机制

  producer端选择MessageQueue机制被封装在selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName)方法中,从该方法可以看出选择机制被分为两种,一种是sendLatencyFaultEnable为true,一种是sendLatencyFaultEnable为false。sendLatencyFaultEnable可以通过客户端的setSendLatencyFaultEnable方法进行设置,默认值是false,接下来对两种机制分别分析。

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

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

1.sendLatencyFaultEnable为false
  当sendLatencyFaultEnable为false时会执行selectOneMessageQueue(final String lastBrokerName)方法,其中lastBrokerName表示上次选择的MessageQueue的broker名称。这种机制也分为两种情况,一种是第一次选择MessageQueue,一种是消息重试的情况下再次选择MessageQueue。

public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
        if (lastBrokerName == null) {
            return selectOneMessageQueue();
        } else {
            for (int i = 0; i < this.messageQueueList.size(); i++) {
                int index = this.sendWhichQueue.getAndIncrement();
                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();
        }
    }
  • 第一次选择MessageQueue
    由于是第一次选择MessageQueue所以lastBrokerName为空,这种情况下会执行selectOneMessageQueue()方法。在该方法中sendWhichQueue变量是基于ThreadLocal类型的封装的变量,其初始值是一个随机值。selectOneMessageQueue方法的实现逻辑:sendWhichQueue自增值与消息队列数量取模,然后根据该值获取消息队列。
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);
    }
  • 消息重试的情况下再次选择MessageQueue
    如果第一次选择的消息队列发送消息失败了,那么此时会在消息重试机制下再次选择消息队列。在选择消息队列时同样是先获取sendWhichQueue的自增值,然后将其与消息队列数量取模,最后根据取模后的值来获取消息队列,到这里与第一种情况的处理方式是一样的,但是在选择出消息队列后还会进行一个判断,即当前选择的消息队列所在的broker是否是上次发送的broker,如果不是则直接返回,否则表示本次选择的消息队列还是上次发送失败的broker,此时则继续选择下一个消息队列。从这里可以看出该机制下通过将选择出的消息队列所在的broker与lastBrokerName进行对比的方式来规避有问题的broker。

2.sendLatencyFaultEnable为true
  在RocketMQ中除了上述比较本次选择的消息队列所在的broker与lastBrokerName是否相同的简单机制来规避故障broker外,还设计了另一种机制来规避故障broker,在该机制下客户端需要调用setSendLatencyFaultEnable方法将sendLatencyFaultEnable的值设置为true。接来下详细介绍这种机制的原理。

  首先在producer端有一张表faultItemTable<brokerName, FaultItem>,FaultItem记录了被选中的broker名称、本次发送消息耗费时间以及一个时间,该时间表示从哪个时间点开始broker上的消息队列可以参与到消息队列选择中。接着来看看这张表中的数据来源,当producer每次发送完消息时不论是否发送成功都会调用updateFaultItem(final String brokerName, final long currentLatency, boolean isolation)方法来更新faultItemTable,在该方法中有两个参数需要解释,其中currentLatency表示本次发送消息花费的时间,当消息发送成功时isolation为false,当消息发送失败时isolation为true。

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);
        }
    }
private long computeNotAvailableDuration(final long currentLatency) {
        for (int i = latencyMax.length - 1; i >= 0; i--) {
            if (currentLatency >= latencyMax[i])
                return this.notAvailableDuration[i];
        }

        return 0;
    }

  在updateFaultItem方法中有一个关键点需要注意,即调用computeNotAvailableDuration方法来计算当前broker不可用的时间。首先还是看下参数值是如何计算:当消息发送成功时isolation为false,此时参数值为本次发送消息花费的时间;当消息发送失败时isolation为true,此时参数值为30000。接着来看计算broker不可用时间的逻辑:这里是用两个数组latencyMax和notAvailableDuration来计算,通过将参数值与latencyMax数组中的数据进行比较,如果满足currentLatency >= latencyMax[i]关系则broker不可用的时间为notAvailableDuration[i]。通过观察latencyMax和notAvailableDuration可以得出以下结论:
  (1)当消息发送成功且发送花费时间在(0,550)毫秒范围内,broker不可用时间为0
  (2)当消息发送失败时broker不可用时间为10分钟

  有了faultItemTable表中的数据,我们来看看如何选择消息队列?
  (1)根据sendWhichQueue的自增值与消息队列数取模后的值获取消息队列,然后判断所选择的消息队列所在的broker是否可用,这里判断的条件具体如下,即当faultItemTable表中没有该broker数据或者有但是当前时间大于其startTimestamp。如果判断后可用则直接返回该消息队列否则将继续选择下一个消息队列

public boolean isAvailable(final String name) {
        final FaultItem faultItem = this.faultItemTable.get(name);
        if (faultItem != null) {
            return faultItem.isAvailable();
        }
        return true;
    }
    
public boolean isAvailable() {
            return (System.currentTimeMillis() - startTimestamp) >= 0;
        }

  (2)如果集群中所有的broker都不可用则调用pickOneAtLeast方法从不可用的broker中选择一个broker,至于消息队列则是再次调用selectOneMessageQueue,将sendWhichQueue的自增值与选中的broker中其topic的写队列数取模后的值最为QueueId。


六、producer端消息发送重试机制

  在RocketMQ中消息发送分为三种:同步、异步和单向,下面就三种不同发送方式下的消息发送重试机制来分析。

  1.同步
  (1)调用方法:SendResult send(Message msg)
  (2)消息发送重试机制分析:
  send方法最终是由sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout)实现的,在该方法中消息发送重试机制体现在以下两点:

   - 在消息发送开始前会先计算timesTotal,它表示发送次数,如果是同步发送其发送次数为1 + this.defaultMQProducer.getRetryTimesWhenSendFailed(),这里retryTimesWhenSendFailed默认是2,所以在默认情况下消息发送的重试次数是3次,当然也可以通过客户端调用setRetryTimesWhenSendFailed方法来修改其默认值

   - 在消息发送完成后,会对消息发送的结果进行判断,如果发送不成功即其发送结果的状态不是SendStatus.SEND_OK,此时会判断客户端producer的retryAnotherBrokerWhenNotStoreOK属性是否被设置为true,这个属性表示在发送失败的情况下是否尝试发送到另一个broker,其默认值是false,也就是在默认情况下是不会进行消息重试机制,所以客户端可以通过调用setRetryAnotherBrokerWhenNotStoreOK方法将该属性设置为true,这样当发送消息失败且发送次数不超过1 + this.defaultMQProducer.getRetryTimesWhenSendFailed()时,会主动触发消息发送重试机制

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

  这里需要注意:如果发送消息的方式是异步或者单向时timesTotal的值为1,也就是只发送一次,那么下面就这两张发送方式是否有消息重试机制进行分析。

  2.异步
  (1)调用方法:send(Message msg, SendCallback sendCallback)
  (2)消息发送重试机制分析:
  这里send方法最终是由sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout)实现的,也就是与同步发送一样,但是这里需要注意由于是异步发送所以timesTotal的值为1,也就是消息只发送一次,那么问题来了,异步发送消息到底是否有消息发送重试机制,答案是有,消息发送的核心实现是sendKernelImpl方法,在该方法中会调用sendMessage方法来完成消息发送,在这个方法调用中可以看到它传递了一个参数:this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(),进入sendMessage方法,可以看到最终完成异步发送的是sendMessageAsync方法,在该方法中当消息发送失败时会调用onExceptionImpl方法来重新选择MessageQueue并重新发送消息,这里需要注意:异步发送消息发送重试机制的次数是retryTimesWhenSendAsyncFailed,默认是2,可以通过客户端调用setRetryTimesWhenSendAsyncFailed方法按需设置,此外并不是所有的异常都会进行消息重发

sendKernelImpl方法:
switch (communicationMode) {
                    case ASYNC:
                        Message tmpMessage = msg;
                        boolean messageCloned = false;
                        if (msgBodyCompressed) {
                            //If msg body was compressed, msgbody should be reset using prevBody.
                            //Clone new message using commpressed message body and recover origin massage.
                            //Fix bug:https://github.com/apache/rocketmq-externals/issues/66
                            tmpMessage = MessageAccessor.cloneMessage(msg);
                            messageCloned = true;
                            msg.setBody(prevBody);
                        }

                        if (topicWithNamespace) {
                            if (!messageCloned) {
                                tmpMessage = MessageAccessor.cloneMessage(msg);
                                messageCloned = true;
                            }
                            msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQProducer.getNamespace()));
                        }

                        long costTimeAsync = System.currentTimeMillis() - beginStartTime;
                        if (timeout < costTimeAsync) {
                            throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");
                        }
                        sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                            brokerAddr,
                            mq.getBrokerName(),
                            tmpMessage,
                            requestHeader,
                            timeout - costTimeAsync,
                            communicationMode,
                            sendCallback,
                            topicPublishInfo,
                            this.mQClientFactory,
                            this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(),
                            context,
                            this);
                        break;
                    case ONEWAY:
                    case SYNC:
                        long costTimeSync = System.currentTimeMillis() - beginStartTime;
                        if (timeout < costTimeSync) {
                            throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");
                        }
                        sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                            brokerAddr,
                            mq.getBrokerName(),
                            msg,
                            requestHeader,
                            timeout - costTimeSync,
                            communicationMode,
                            context,
                            this);
                        break;
                    default:
                        assert false;
                        break;
                }

sendMessageAsync方法:
if (!responseFuture.isSendRequestOK()) {
                        MQClientException ex = new MQClientException("send request failed", responseFuture.getCause());
                        onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance,
                            retryTimesWhenSendFailed, times, ex, context, true, producer);
                    } else if (responseFuture.isTimeout()) {
                        MQClientException ex = new MQClientException("wait response timeout " + responseFuture.getTimeoutMillis() + "ms",
                            responseFuture.getCause());
                        onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance,
                            retryTimesWhenSendFailed, times, ex, context, true, producer);
                    } else {
                        MQClientException ex = new MQClientException("unknow reseaon", responseFuture.getCause());
                        onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance,
                            retryTimesWhenSendFailed, times, ex, context, true, producer);
                    }




private void onExceptionImpl(final String brokerName,
        final Message msg,
        final long timeoutMillis,
        final RemotingCommand request,
        final SendCallback sendCallback,
        final TopicPublishInfo topicPublishInfo,
        final MQClientInstance instance,
        final int timesTotal,
        final AtomicInteger curTimes,
        final Exception e,
        final SendMessageContext context,
        final boolean needRetry,
        final DefaultMQProducerImpl producer
    ) {
        int tmp = curTimes.incrementAndGet();
        if (needRetry && tmp <= timesTotal) {
            String retryBrokerName = brokerName;//by default, it will send to the same broker
            if (topicPublishInfo != null) { //select one message queue accordingly, in order to determine which broker to send
                MessageQueue mqChosen = producer.selectOneMessageQueue(topicPublishInfo, brokerName);
                retryBrokerName = mqChosen.getBrokerName();
            }
            String addr = instance.findBrokerAddressInPublish(retryBrokerName);
            log.info("async send msg by retry {} times. topic={}, brokerAddr={}, brokerName={}", tmp, msg.getTopic(), addr,
                retryBrokerName);
            try {
                request.setOpaque(RemotingCommand.createNewRequestId());
                sendMessageAsync(addr, retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance,
                    timesTotal, curTimes, context, producer);
            } catch (InterruptedException e1) {
                onExceptionImpl(retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, timesTotal, curTimes, e1,
                    context, false, producer);
            } catch (RemotingConnectException e1) {
                producer.updateFaultItem(brokerName, 3000, true);
                onExceptionImpl(retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, timesTotal, curTimes, e1,
                    context, true, producer);
            } catch (RemotingTooMuchRequestException e1) {
                onExceptionImpl(retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, timesTotal, curTimes, e1,
                    context, false, producer);
            } catch (RemotingException e1) {
                producer.updateFaultItem(brokerName, 3000, true);
                onExceptionImpl(retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, timesTotal, curTimes, e1,
                    context, true, producer);
            }
        } else {

            if (context != null) {
                context.setException(e);
                context.getProducer().executeSendMessageHookAfter(context);
            }

            try {
                sendCallback.onException(e);
            } catch (Exception ignored) {
            }
        }
    }

  3.单向
  (1)调用方法:sendOneway(Message msg)
  (2)消息发送重试机制分析:
  这里send方法最终是由sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout)实现的,也就是与同步发送一样,但是这里需要注意由于是单向发送所以timesTotal的值为1,也就是消息只发送一次,继续跟踪sendKernelImpl方法可以发现单向发送实现逻辑中并没有消息重发的实现

  4.总结
  综上分析可以看出producer端发送消息时,如果是同步或者异步发送,则在发送消息失败的情况下会触发消息重发,而单向发送没有该机制。此外还可以在客户端调用setRetryTimesWhenSendFailed或者setRetryTimesWhenSendAsyncFailed方法来按需设置重发次数。最后如果是同步发送且需要重发机制,那么需要在客户端调用setRetryAnotherBrokerWhenNotStoreOK方法将retryAnotherBrokerWhenNotStoreOK属性的值设置为true。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值