RocketMQ源码分析7:Producer消息发送流程

781 阅读11分钟

本文已参与[新人创作礼]活动,一路开启掘金创作之路。

基于rocketmq-4.9.0 版本分析rocketmq

承接上文,我们继续看Producer发送消息到Broker的流程:官方用例

我这里以同步发送为例:

public class SyncProducer {

    public static void main(String[] args) throws Exception {

        //TODO:实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("rocketmq-test-group");
        //TODO:设置NameServer的地址
        producer.setNamesrvAddr("127.0.0.1:9876");

        //TODO:启动Producer实例
        producer.start();

        //发送消息到broker
        for (int i = 0; i < 1; i++) {
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message("my-rockemq-topic",
                    "*",
                    ("Hello RocketMQ, producer is qiuguan " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );
            // 为消息指定key
            msg.setKeys("mq-" + i);

            SendResult sendResult = producer.send(msg);
            // 通过sendResult返回消息是否成功送达
            System.out.printf("%s%s%n", sendResult, i);
        }

        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}
  1. 创建Message对象,设置topic,tag,和消息内容,以及设置消息的keys

其中,tag 和 keys 设置到Message对象的properties属性中(Map),key分别是 TAGSKEYS

  1. 调用DefaultMQProduce对象的send(Message msg)方法发送消息

1.发送消息过程

@Override
public SendResult send(
    Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    Validators.checkMessage(msg, this);
    msg.setTopic(withNamespace(msg.getTopic()));
    return this.defaultMQProducerImpl.send(msg);
}
  1. 检查消息对象Message
  • 检查消息body是否为空,长度是否大于0
  • 检查消息size是否大于4M (producer和broker两端都会限制消息的最大为4M)
  • 检查topic是否为空,topic是否超过最大字符127个的限制,topci是否包含了特殊字符
  • 检查topic是否为SCHEDULE_TOPIC_XXXX,如果为它,则不允许发送
  1. 如果设置了namespace,则对topic进行包装,一般都不会设置namespace,所以返回源生topic
  2. 调用DefaultMQProducerImpl对象的send(msg)方法发送消息

我们继续往下看它的逻辑实现:

private SendResult sendDefaultImpl(
    Message msg,
    final CommunicationMode communicationMode,
    final SendCallback sendCallback,
    final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    //TODO:.....省略部分代码......
    
    //TODO:从nameserever获取topic路由信息
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    if (topicPublishInfo != null && topicPublishInfo.ok()) {
        boolean callTimeout = false;
        MessageQueue mq = null;
        Exception exception = null;
        SendResult sendResult = null;
        //TODO:计算重试次数
        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();
            //TODO:选择一个queue
            MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
            if (mqSelected != null) {
                mq = mqSelected;
                brokersSent[times] = mq.getBrokerName();
                
                beginTimestampPrev = System.currentTimeMillis();
                  
                //TODO:发送消息  
                sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                endTimestamp = System.currentTimeMillis();
                this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                
                
              //TODO:....省略
            }
        }

        if (sendResult != null) {
            return sendResult;
        }

      
}

这个方法代码比较多,其实主要就是三件事:

  1. 从nameserver获取topic路由信息
  2. 计算重试次数

同步发送默认重试3次(就是for循环3次)

  • 同步发送失败重试时,对于普通消息,消息发送默认采用round-robin策略来选择所发送到的队列。如果发送失败, 默认重试3次。但在重试时是不会选择上次发送失败的Broker,而是选择其它Broker,这是因为它具有失败隔离功能,使Producer尽量选择未发生过发送失败的Broker作为目标Broker。其可以保证其它消息尽量不发送到问题Broker,为了提升消息发送效率,降低消息发送耗时。
  • 异步发送失败重试时,异步重试不会选择其他broker,仅在同一个broker上做重试,所以该策略无法保证消息不丢。
  1. 选择一个MessageQueue

MessageQueue是逻辑消费队列,可以暂时理解为它就是消费者的ConsumeQueue。

  1. 发送消息

那么接下来我们就仔细看下它是如何完成以上步骤的:

1.1 从NameServer获取Topic路由信息

在我发送消息的代码中所指定的topic我并没有在发送前去创建,那么它是如何选择的呢? 我们先看它获取topic路由信息的代码:

private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    //TODO:我首次发送(也没有通过其他方式创建过该topic),这里肯定获取不到
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        //TODO:这里就是从nameserver获取topic路由信息,第一次肯定获取不到的,继续往下走
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
    }

    //TODO:第一次发送,虽然上面设置了对象,但是条件还是false,所以走else逻辑
    if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
        return topicPublishInfo;
    } else {
        //TODO:核心,注意看,第二个参数是true
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
        return topicPublishInfo;
    }
}

假设我第一次进行topic=test-topic的发送消息(而且也没有通过其他方式创建过该topic)

  1. 根据topic从topicPublishInfoTable表中获取Topic路由信息,获取不到
  2. if条件成立,在topicPublishInfoTable中放入一条数据
  3. 从nameserver获取topic路由信息,获取不到,抛出异常,但被catch了
  4. 再次根据topic从topicPublishInfoTable表中获取Topic路由信息(它不为空,但是没有填充数据)
  5. 判断 topicPublishInfo 对象是否有topic路由信息或者是否ok(),二者都返回false;进入else
  6. 再次从NameServer中获取topic路由信息,但是,请注意第二个参数为true,我们看下这个方法的实现
//TODO:第二个参数的含义为 是否为default,继续往下看
public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
    DefaultMQProducer defaultMQProducer) {
    try {
        if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
            try {
                TopicRouteData topicRouteData;
                //TODO:如果是true,则获取Rocketmq内部默认的topic路由信息,它是一个模板
                if (isDefault && defaultMQProducer != null) {
                    topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
                        1000 * 3);
                    if (topicRouteData != null) {
                        for (QueueData data : topicRouteData.getQueueDatas()) {
                            //TODO:broker端模板配置的读写队列都是8,生产者端默认配置的是4,所以取二者小的,就是4
                            int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
                            data.setReadQueueNums(queueNums);
                            data.setWriteQueueNums(queueNums);
                        }
                    }
                } else {
                   //TODO:从nameserver获取用户指定的topic的路由信息,如果获取不到,则抛出异常,不过被catch了
                   //TODO:第一次根据用户topic获取路由信息时,就会来到这里
                    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) {
                        //TODO:将模板克隆一份,这个将用作我们自己的topic配置信息
                        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);
                                }
                            }
                        }

                        //TODO:........
                        
                        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;
}

它其实主要就是做3件事:

  1. 根据模板topic(=TBW102)获取默认的topic路由信息,其内部保存了默认的队列数据(读写队列默认都是8个),以及Broker数据。
  2. 更新队列数,模板topic中的队列数是8个,而生产者端默认指定的是4个,二者取较小值,所以队列数是4。
  3. 将模板topic路由数据克隆一份,作为用户的topic路由数据,然后向其中更新生产者发布信息;最后将完整数据更新到topic路由信息TopicPublishInfo对象中。

TopicPublishInfo内部维护着队列数据(MessageQueue)以及路由数据(比如broker数据,以及queue属于哪个broker数据)

到这里,topic路由信息就有了(仅仅是本地有);Broker 和 NameServer还没有。

1.2 选择MessageQueue

在上面获取的topic路由信息对象TopicPublishInfo中,其内部包含了队列的信息

public class TopicPublishInfo {
    private boolean orderTopic = false;
    private boolean haveTopicRouterInfo = false;
    //TODO:保存着队列信息,默认是4个queue
    private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>();
    private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();
    private TopicRouteData topicRouteData;
    
    //TODO:.......
}    

queue的选择算法:

  • 轮询算法:该算法保证了每个Queue中可以均匀的获取到消息。
  • 最小投递延迟算法:该算法会统计每次消息投递的时间延迟,然后根据统计出的结果将消息投递到时间延迟最小的Queue。如果延迟相同,则采用轮询算法投递。该算法可以有效提升消息的投递性能。 默认使用的是轮询算法

所以,通过轮询算法从TopicPublishInfo对象的queue集合(messageQueueList)中获取一个MessageQueue

具备了以上信息后,就可以将消息发往broker了。

1.3 发送消息

首先将消息内容封装到SendMessageRequestHeader对象中:

{
    SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
    //TODO:设置消费者组
    requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
    //TODO:设置topic
    requestHeader.setTopic(msg.getTopic());
    //TODO:设置默认的topic:TBW102
    requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
    //TODO:设置默认的队列数量:4
    requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
    //TODO:设置queueid
    requestHeader.setQueueId(mq.getQueueId());
    requestHeader.setSysFlag(sysFlag);
    //TODO:设置消息的生产时间
    requestHeader.setBornTimestamp(System.currentTimeMillis());
    requestHeader.setFlag(msg.getFlag());
    //TODO:设置properties,比如TAGS,KEYS
    requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
    requestHeader.setReconsumeTimes(0);
    requestHeader.setUnitMode(this.isUnitMode());
    //TODO:是否为批量消息
    requestHeader.setBatch(msg instanceof MessageBatch);
}

然后选择发送模式:

  • 同步发送:Producer发出⼀条消息后,会在收到MQ返回的ACK之后才发下⼀条消息。该方式的消息可靠性最高,但消息发送效率太低。
  • 异步发送:Producer发出消息后无需等待MQ返回ACK,直接发送下⼀条消息。该方式的消息可靠性可以得到保障,消息发送效率也可以。
  • 单向发送:Producer仅负责发送消息,不等待、不处理MQ的ACK。该发送方式时MQ也不返回ACK。该方式的消息发送效率最高,但消息可靠性较差。
public SendResult sendMessage(
    final String addr,
    final String brokerName,
    final Message msg,
    final SendMessageRequestHeader requestHeader,
    final long timeoutMillis,
    final CommunicationMode communicationMode,
    final SendCallback sendCallback,
    final TopicPublishInfo topicPublishInfo,
    final MQClientInstance instance,
    final int retryTimesWhenSendFailed,
    final SendMessageContext context,
    final DefaultMQProducerImpl producer
) throws RemotingException, MQBrokerException, InterruptedException {
    long beginStartTime = System.currentTimeMillis();
    RemotingCommand request = null;
    String msgType = msg.getProperty(MessageConst.PROPERTY_MESSAGE_TYPE);
    boolean isReply = msgType != null && msgType.equals(MixAll.REPLY_MESSAGE_FLAG);
    
    //TODO:....省略部分代码......
    
    //TODO:创建远程命令,code=RequestCode.SEND_MESSAGE,前面分析Broker时已经多次看到了
    request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader);
    request.setBody(msg.getBody());

    switch (communicationMode) {
        //TODO:单向发送
        case ONEWAY:
            this.remotingClient.invokeOneway(addr, request, timeoutMillis);
            return null;
        //TODO:异步发送    
        case ASYNC:
            final AtomicInteger times = new AtomicInteger();
            long costTimeAsync = System.currentTimeMillis() - beginStartTime;
            if (timeoutMillis < costTimeAsync) {
                throw new RemotingTooMuchRequestException("sendMessage call timeout");
            }
            this.sendMessageAsync(addr, brokerName, msg, timeoutMillis - costTimeAsync, request, sendCallback, topicPublishInfo, instance,
                retryTimesWhenSendFailed, times, context, producer);
            return null;
        //TODO:同步发送
        case SYNC: 
            long costTimeSync = System.currentTimeMillis() - beginStartTime;
            if (timeoutMillis < costTimeSync) {
                throw new RemotingTooMuchRequestException("sendMessage call timeout");
            }
            return this.sendMessageSync(addr, brokerName, msg, timeoutMillis - costTimeSync, request);
        default:
            assert false;
            break;
    }

    return null;
}

其主要就是:

  1. 创建远程命令RemotingCommand对象,设置封装了消息内容的SendMessageRequestHeader,以及区分业务场景的code为RequestCode.SEND_MESSAGE

broker在接收到producer发送消息的请求后,根据code获取对应的处理器,然后解析封装了消息内容的SendMessageRequestHeader对象,然后存储消息。

  1. 通过netty客户端对象NettyRemotingClient,向Broker发送请求。

2.发送模式

2.1 同步发送

同步发送:Producer发出⼀条消息后,会在收到MQ返回的ACK之后才发下⼀条消息。该方式的消息可靠性最高,但消息发送效率太低。

//TODO:同步调用
public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis)
    throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException {
    long beginStartTime = System.currentTimeMillis();
    //TODO:启动netty客户端
    final Channel channel = this.getAndCreateChannel(addr);
    if (channel != null && channel.isActive()) {
        try {
            doBeforeRpcHooks(addr, request);
            long costTime = System.currentTimeMillis() - beginStartTime;
            if (timeoutMillis < costTime) {
                throw new RemotingTimeoutException("invokeSync call timeout");
            }
            //TODO:同步发送
            RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
            doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
            return response;
        } catch (RemotingSendRequestException e) {
            log.warn("invokeSync: send request exception, so close the channel[{}]", addr);
            this.closeChannel(addr, channel);
            throw e;
        } catch (RemotingTimeoutException e) {
            if (nettyClientConfig.isClientCloseSocketIfTimeout()) {
                this.closeChannel(addr, channel);
                log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr);
            }
            log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr);
            throw e;
        }
    } else {
        this.closeChannel(addr, channel);
        throw new RemotingConnectException(addr);
    }
}

我们继续往下看,它真正发起调用的逻辑:

public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
    final long timeoutMillis)
    throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
    final int opaque = request.getOpaque();

    try {
        //TODO:构建响应对象,第一个参数是channel
        final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
        //TODO:放入Map中
        this.responseTable.put(opaque, responseFuture);
        final SocketAddress addr = channel.remoteAddress();
        //TODO:向broker发起请求
        //TODO:ChannelFutureListener 监听回调结果
        channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture f) throws Exception {
                if (f.isSuccess()) {
                    //TODO:broker响应成功,设置到响应对象中
                    responseFuture.setSendRequestOK(true);
                    return;
                } else {
                    //TODO:broker响应失败,设置到响应对象中
                    responseFuture.setSendRequestOK(false);
                }

                responseTable.remove(opaque);
                responseFuture.setCause(f.cause());
                responseFuture.putResponse(null);
                log.warn("send a request command to channel <" + addr + "> failed.");
            }
        });

        //TODO:等待broker的返回结果,异步监听
        RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);
        if (null == responseCommand) {
            if (responseFuture.isSendRequestOK()) {
                throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis,
                    responseFuture.getCause());
            } else {
                throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());
            }
        }

        return responseCommand;
    } finally {
        this.responseTable.remove(opaque);
    }
}

从源码中,我们可以看到,它其实是通过异步监听的模式来获取响应结果的,通过主线程等待(CountDownLatch)将结果同步返回。

这里的同步,就是人为主动的等待netty完成请求,返回结果。

2.2 异步发送

异步发送:Producer发出消息后无需等待MQ返回ACK,直接发送下⼀条消息。该方式的消息可靠性可以得到保障,消息发送效率也可以。 我们先看下异步发送的代码:

public class AsyncProducer {

    public static void main(String[] args) throws Exception {

        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("p-group");
        // 设置NameServer的地址
        producer.setNamesrvAddr("127.0.0.1:9876");


        // 启动Producer实例
        producer.start();
        for (int i = 0; i < 1; i++) {
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message("my-test-topic",
                    "*",
                    ("Hello RocketMQ, producer is qiuguan " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );

            // 为消息指定key
            msg.setKeys("key-unique-" + i);

            //TODO:通过回调来处理响应结果
            producer.send(msg, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.println("sendResult : " + sendResult);
                }

                @Override
                public void onException(Throwable e) {
                    e.printStackTrace();
                }
            });
        }

        // sleep一会儿,由于采用的是异步发送,所以若这里不sleep,
        // 则消息还未发送就会将producer给关闭,报错
        TimeUnit.SECONDS.sleep(30);

        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}

我们来看下异步发送的底层逻辑:

public void invokeAsync(String addr, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback)
    throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException,
    RemotingSendRequestException {
    long beginStartTime = System.currentTimeMillis();
    //TODO:启动netty客户端
    final Channel channel = this.getAndCreateChannel(addr);
    if (channel != null && channel.isActive()) {
        try {
            doBeforeRpcHooks(addr, request);
            long costTime = System.currentTimeMillis() - beginStartTime;
            if (timeoutMillis < costTime) {
                throw new RemotingTooMuchRequestException("invokeAsync call timeout");
            }
            //TODO:发起异步调用
            this.invokeAsyncImpl(channel, request, timeoutMillis - costTime, invokeCallback);
        } catch (RemotingSendRequestException e) {
            log.warn("invokeAsync: send request exception, so close the channel[{}]", addr);
            this.closeChannel(addr, channel);
            throw e;
        }
    } else {
        this.closeChannel(addr, channel);
        throw new RemotingConnectException(addr);
    }
}

我们继续往下看,它真正发起调用的逻辑:

public void invokeAsyncImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis,
    final InvokeCallback invokeCallback)
    throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
    long beginStartTime = System.currentTimeMillis();
    final int opaque = request.getOpaque();
    //TODO:获取JUC的信号量(锁)
    boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
    if (acquired) {
        final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync);
        long costTime = System.currentTimeMillis() - beginStartTime;
        if (timeoutMillis < costTime) {
            once.release();
            throw new RemotingTimeoutException("invokeAsyncImpl call timeout");
        }

        //TODO:构建响应对象,异步重点关注:invokeCallback
        final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis - costTime, invokeCallback, once);
        //TODO:将响应对象放到Map中.......这里关注一下
        this.responseTable.put(opaque, responseFuture);
        try {
            //TODO:发送请求
            //TODO:ChannelFutureListener 监听响应结果
            channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture f) throws Exception {
                    if (f.isSuccess()) {
                        //TODO:如果响应成功,则将结果置为true
                        responseFuture.setSendRequestOK(true);
                        return;
                    }
                    requestFail(opaque);
                    log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel));
                }
            });
        } catch (Exception e) {
            responseFuture.release();
            log.warn("send a request command to channel <" + RemotingHelper.parseChannelRemoteAddr(channel) + "> Exception", e);
            throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
        }
    } else {
       //TODO:....忽略else逻辑.......
    }
}

那么它是如何执行回调的呢?\textcolor{Red}{那么它是如何执行回调的呢?}
前面在分析生产者启动-Netty客户端启动时,它内部会启动一个定时任务:

this.timer.scheduleAtFixedRate(new TimerTask() {
    @Override
    public void run() {
        try {
            NettyRemotingClient.this.scanResponseTable();
        } catch (Throwable e) {
            log.error("scanResponseTable exception", e);
        }
    }
}, 1000 * 3, 1000);

这个定时任务的内容就是扫描前面放入了响应对象的Map

//TODO:将响应对象放到Map中.......这里关注一下 
this.responseTable.put(opaque, responseFuture);

我们然后扫描的逻辑:

public void scanResponseTable() {
    final List<ResponseFuture> rfList = new LinkedList<ResponseFuture>();
    //TODO:这个responseTable就是我们前面看到的
    Iterator<Entry<Integer, ResponseFuture>> it = this.responseTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<Integer, ResponseFuture> next = it.next();
        ResponseFuture rep = next.getValue();

        if ((rep.getBeginTimestamp() + rep.getTimeoutMillis() + 1000) <= System.currentTimeMillis()) {
            rep.release();
            it.remove();
            rfList.add(rep);
            log.warn("remove timeout request, " + rep);
        }
    }

    for (ResponseFuture rf : rfList) {
        try {
            //TODO:核心,执行异步回调
            executeInvokeCallback(rf);
        } catch (Throwable e) {
            log.warn("scanResponseTable, operationComplete Exception", e);
        }
    }
}

它的核心就是发起异步回调:

private void executeInvokeCallback(final ResponseFuture responseFuture) {
    boolean runInThisThread = false;
    //TODO:获取线程池
    ExecutorService executor = this.getCallbackExecutor();
    if (executor != null) {
        try {
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        //TODO:通过这里执行回调.....
                        responseFuture.executeInvokeCallback();
                    } catch (Throwable e) {
                        log.warn("execute callback in executor exception, and callback throw", e);
                    } finally {
                        responseFuture.release();
                    }
                }
            });
        } catch (Exception e) {
            runInThisThread = true;
            log.warn("execute callback in executor exception, maybe executor busy", e);
        }
    } else {
        runInThisThread = true;
    }

    if (runInThisThread) {
        try {
            //TODO:如果没有线程池,则通过这里发起回调
            responseFuture.executeInvokeCallback();
        } catch (Throwable e) {
            log.warn("executeInvokeCallback Exception", e);
        } finally {
            responseFuture.release();
        }
    }
}

我们继续点进去看它的实现逻辑:

public void executeInvokeCallback() {
    if (invokeCallback != null) {
        if (this.executeCallbackOnlyOnce.compareAndSet(false, true)) {
            invokeCallback.operationComplete(this);
        }
    }
}

然后我们在点进去看:

private void sendMessageAsync(
    final String addr,
    final String brokerName,
    final Message msg,
    final long timeoutMillis,
    final RemotingCommand request,
    final SendCallback sendCallback,
    final TopicPublishInfo topicPublishInfo,
    final MQClientInstance instance,
    final int retryTimesWhenSendFailed,
    final AtomicInteger times,
    final SendMessageContext context,
    final DefaultMQProducerImpl producer
) throws InterruptedException, RemotingException {
    final long beginStartTime = System.currentTimeMillis();
    this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
        //TODO:执行上面的回调
        @Override
        public void operationComplete(ResponseFuture responseFuture) {
            long cost = System.currentTimeMillis() - beginStartTime;
            RemotingCommand response = responseFuture.getResponseCommand();
           
            //TODO:....省略部分代码......

            if (response != null) {
                try {
                    //TODO:处理响应结果
                    SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response, addr);
                    assert sendResult != null;
                    if (context != null) {
                        context.setSendResult(sendResult);
                        context.getProducer().executeSendMessageHookAfter(context);
                    }

                    try {
                        //TODO:执行用户回调,这个就是我们发送异步发送时的回调
                        sendCallback.onSuccess(sendResult);
                    } catch (Throwable e) {
                    }

                    producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), false);
                } catch (Exception e) {
                    producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), true);
                    //TODO:如果发生异常,其内部会有onException()的回调
                    onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance,
                        retryTimesWhenSendFailed, times, e, context, false, producer);
                }
            } else {
               //TODO:....
            }
        }
    });
}

所以,sendCallback.onSuccess(sendResult); 和 sendCallback.onException(e); 对应着用户的回调:

//TODO:.....略....

producer.send(msg, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        System.out.println("sendResult : " + sendResult);
    }

    @Override
    public void onException(Throwable e) {
        e.printStackTrace();
    }
});

到这里,异步发送就算是结束了,接下来我们看下单向发送。

2.3 单向发送

单向发送:Producer仅负责发送消息,不等待、不处理MQ的ACK。该发送方式时MQ也不返回ACK。该方式的消息发送效率最高,但消息可靠性较差。 我们先看下发送代码:

public class OnewayProducer {
	public static void main(String[] args) throws Exception{
    	// 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
    	// 设置NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
    	// 启动Producer实例
        producer.start();
    	for (int i = 0; i < 100; i++) {
        	// 创建消息,并指定Topic,Tag和消息体
        	Message msg = new Message("TopicTest" /* Topic */,
                "TagA" /* Tag */,
                ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
        	);
        	// 发送单向消息,没有任何返回结果
        	producer.sendOneway(msg);

    	}
    	// 如果不再发送消息,关闭Producer实例。
    	producer.shutdown();
    }
}

然后我们看下底层的发送逻辑:

public void invokeOnewayImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis)
    throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
    request.markOnewayRPC();
    boolean acquired = this.semaphoreOneway.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
    if (acquired) {
        final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreOneway);
        try {
            //TODO:发送消息
            channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture f) throws Exception {
                    once.release();
                    //TODO:如果失败,则仅仅打印一条日志,成功的话直接不管
                    if (!f.isSuccess()) {
                        log.warn("send a request command to channel <" + channel.remoteAddress() + "> failed.");
                    }
                }
            });
        } catch (Exception e) {
            once.release();
            log.warn("write send a request command to channel <" + channel.remoteAddress() + "> failed.");
            throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
        }
    } else {
        
    }
}

从代码中可以看到,如果发送成功,则不做任何处理;如果发送失败,则打印一条日志。


然后接下来就是Broker接收到Producer发送消息的请求后,开始处理消息。

3.Broker接收请求

这里,关于broker接收请求我就不在阐述了,前面在分析Broker处理请求一文中,里面记录了Broker是如何接收客户端请求,以及如何存储消息,持久化消息。

至此,producer的发送消息的流程就算是结束了。

4.总结

本文主要是分析了producer发送消息的三种模式:

  1. 同步发送:消息发送到broker后,主线程会阻塞,等待broker的响应结果。
  2. 异步发送:消息发送时,设置一个发送结果的回调监听器,消息发送后,主线程不会阻塞,回调监听器会监听broker的响应结果。
  3. 单向发送:只管发送,不管结果。线程不会阻塞,也无法设置监听器来监听发送结果。

关于重试:

  1. 同步发送:如果发送失败,默认重试2次(共计3次)。但在重试时是不会选择上次发送失败的Broker,而是选择其它Broker,这是因为它具有失败隔离功能,使Producer尽量选择未发生过发送失败的Broker作为目标Broker。其可以保证其它消息尽量不发送到问题Broker,为了提升消息发送效率,降低消息发送耗时。
  2. 异步发送:如果发送失败,默认重试2次(共计3次)。异步发送失败重试时,异步重试不会选择其他broker,仅在同一个broker上做重试,所以该策略无法保证消息不丢。
  3. 单向发送:只管发送,不管结果,不会重试。

限于作者个人水平,文中难免有错误之处,欢迎指正! 勿喷,感谢