Rocketmq源码分析09:producer 消息发送流程

1,009 阅读7分钟

注:本系列源码分析基于RocketMq 4.8.0,gitee仓库链接:gitee.com/funcy/rocke….

接上文,我们继续分析producer消息发送流程。

3. DefaultMQProducer#send(...):发送消息

接下来我们来看看producer发送消息的流程,进入方法DefaultMQProducer#send(...)

public SendResult send(
    Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    Validators.checkMessage(msg, this);
    msg.setTopic(withNamespace(msg.getTopic()));
    // 调用 defaultMQProducerImpl 方法
    return this.defaultMQProducerImpl.send(msg);
}

调用的是DefaultMQProducerImpl#send(...)方法,继续:

public SendResult send(Message msg) throws MQClientException, RemotingException, 
        MQBrokerException, InterruptedException {
    return send(msg, this.defaultMQProducer.getSendMsgTimeout());
}

public SendResult send(Message msg, long timeout) throws MQClientException, 
        RemotingException, MQBrokerException, InterruptedException {
    // 默认使用异步方法发送
    return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
}

最终来到了DefaultMQProducerImpl#sendDefaultImpl方法:

private SendResult sendDefaultImpl(
        Message msg,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final long timeout
    ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        ...
        // 1. 找到了一个topic
        TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
        if (topicPublishInfo != null && topicPublishInfo.ok()) {
            ...
            // 2. 重试次数,同步发送时才会重试
            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();
                // 3. 找到一个消息队列
                MessageQueue mqSelected 
                    = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
                if (mqSelected != null) {
                    ...
                    try {
                        // 4. 发送
                        sendResult = this.sendKernelImpl(msg, mq, communicationMode, 
                            sendCallback, topicPublishInfo, timeout - costTime);
                        ...
                    } catch (...) {
                        // 省略异常处理
                        ...
                    }
                } else {
                    break;
                }
            }

            ...

            throw mqClientException;
        }

        validateNameServerSetting();

        throw new MQClientException(...);
    }

以上方法就是发送消息的方法,该方法还是非常长,不过关键点就3个:

  1. 根据topic找到对应的发布信息
  2. 获取重试次数,同步发送时才会重试
  3. 选择一个要发送的消息队列
  4. 发送消息

接下来我们就来分析这几个步骤。

3.1 根据topic找到对应的发布信息

获取topic发布信息的方法为DefaultMQProducerImpl#tryToFindTopicPublishInfo

/**
 * 根据topic获取发布信息
 * 如果topic信息不存在,就先从`nameServer`获取topic信息
 * @param topic
 * @return
 */
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        // 更新 topicRouteInfo
        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;
    }
}

这里主要是调用了MQClientInstance#updateTopicRouteInfoFromNameServer方法来更新topic的订阅信息,更新时会向broker发送一条codeGET_ROUTEINFO_BY_TOPIC的请求(方法为MQClientAPIImpl#getTopicRouteInfoFromNameServer),具体内容就不展开了.

3.2 获取重试次数

在使用同步模式发送消息时,当消息发送失败时,rocketmq会有重试机制,发送前会获取重试次数:

int timesTotal = communicationMode == CommunicationMode.SYNC 
    ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;

这是一个三目运算符,一开始便会判断是否为同步模式,如果是同步模式,发送次数就为this.defaultMQProducer.getRetryTimesWhenSendFailed()+1,其中1为真正要发送的次数,retryTimesWhenSendFailed为失败时重试的次数,在值在DefaultMQProducer#retryTimesWhenSendFailed中维护,默认为2,也就是说,一条消息最多发送3次。

3.3 找到一个消息队列:

在发送消息前,需要找到一个消息送达的队列,方法为DefaultMQProducerImpl#selectOneMessageQueue

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, 
        final String lastBrokerName) {
    return this.mqFaultStrategy.selectOneMessageQueue(tpInfo, lastBrokerName);
}

继续跟进,来到 MQFaultStrategy#selectOneMessageQueue

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, 
        final String lastBrokerName) {
    if (this.sendLatencyFaultEnable) {
        try {
            // 当前topic发送的消息次数
            int index = tpInfo.getSendWhichQueue().getAndIncrement();
            // 获取一个可用的broker
            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);
                // 如果mq所在broker可用,直接返回
                if (latencyFaultTolerance.isAvailable(mq.getBrokerName()))
                    return mq;
            }
            // 经过上面的步骤还没找到 broker,从不可用的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().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. 如果启用了发送到最近故障MessageQueue的功能,获取到MessageQueue后,会判断该MessageQueue所在的broker是否可用,不可用时会再次获取
  2. 如果以上操作获取失败,或未启用发送到最近故障MessageQueue的功能,获取到MessageQueue后就直接返回

获取MessageQueue时,最核心的操作为%(取模):

// 当前topic发送的消息次数
int index = tpInfo.getSendWhichQueue().getAndIncrement();
...
// 取模运算,得到MessageQueue的下标
int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (pos < 0)
    pos = 0;
// 根据下标索引得到 MessageQueue
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);

根据取模运算的规律,如果所有的MessageQueue所在的broker都无故障,则消息会均匀分布在各个队列上。

3.4 发送消息

消息的发送方法为 DefaultMQProducerImpl#sendKernelImpl,代码如下:

private SendResult sendKernelImpl(final Message msg,
        final MessageQueue mq,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final TopicPublishInfo topicPublishInfo,
        final long timeout) throws MQClientException, RemotingException, 
        MQBrokerException, InterruptedException {

    long beginStartTime = System.currentTimeMillis();
    // 根据 broker 名称获取 broker 地址
    String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
    if (null == brokerAddr) {
        // 如果找不到 broker 的地址,就再一次从 nameServer 获取主题发布信息
        tryToFindTopicPublishInfo(mq.getTopic());
        brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
    }

    SendMessageContext context = null;
    if (brokerAddr != null) {
        brokerAddr = MixAll.brokerVIPChannel(
            this.defaultMQProducer.isSendMessageWithVIPChannel(), brokerAddr);
        // 消息内容
        byte[] prevBody = msg.getBody();
        try {
            ...

            int sysFlag = 0;
            boolean msgBodyCompressed = false;
            // 数据压缩
            if (this.tryToCompressMessage(msg)) {
                sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
                msgBodyCompressed = true;
            }

            ...
            // 构建 RequestHeader
            SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
            // 省略好多的set操作
            ...

            SendResult sendResult = null;
            switch (communicationMode) {
                case ASYNC:
                    ...
                    // 发送消息
                    sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                        // 省略一大堆的参数
                        ...
                        );
                    break;
                case ONEWAY:
                case SYNC:
                    long costTimeSync = System.currentTimeMillis() - beginStartTime;
                    if (timeout < costTimeSync) {
                        throw new RemotingTooMuchRequestException("...");
                    }
                    sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                        // 省略一大堆的参数
                        ...
                        );
                    break;
                default:
                    assert false;
                    break;
            }

            ...
        } catch (...) {
            ...
        } finally {
            msg.setBody(prevBody);
            msg.setTopic(NamespaceUtil.withoutNamespace(
                msg.getTopic(), this.defaultMQProducer.getNamespace()));
        }
    }

    throw new MQClientException(...);
}

这个方法中主要是组装参数,然后调用this.mQClientFactory.getMQClientAPIImpl().sendMessage(...)方法处理消息发送,发送消息时,还区分了消息模式:ASYNCONEWAYSYNC,不过不管哪种模式,最终调用的方法都是MQClientAPIImpl#sendMessage(...)方法:

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);
    // 构造request命令
    if (isReply) {
        if (sendSmartMsg) {
            SendMessageRequestHeaderV2 requestHeaderV2 = 
                SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader);
            request = RemotingCommand.createRequestCommand(
                RequestCode.SEND_REPLY_MESSAGE_V2, requestHeaderV2);
        } else {
            request = RemotingCommand.createRequestCommand(
                RequestCode.SEND_REPLY_MESSAGE, requestHeader);
        }
    } else {
        if (sendSmartMsg || msg instanceof MessageBatch) {
            SendMessageRequestHeaderV2 requestHeaderV2 
                = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader);
            request = RemotingCommand.createRequestCommand(msg instanceof MessageBatch 
                ? RequestCode.SEND_BATCH_MESSAGE : RequestCode.SEND_MESSAGE_V2, requestHeaderV2);
        } else {
            request = RemotingCommand
                .createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader);
        }
    }
    request.setBody(msg.getBody());

    // 调用发送方法
    switch (communicationMode) {
        case ONEWAY:
            this.remotingClient.invokeOneway(addr, request, timeoutMillis);
            return null;
        case ASYNC:
            ...
            this.sendMessageAsync(addr, brokerName, msg, timeoutMillis - costTimeAsync, 
                request, sendCallback, topicPublishInfo, instance,
                retryTimesWhenSendFailed, times, context, producer);
            return null;
        case SYNC:
            ...
            return this.sendMessageSync(addr, brokerName, msg, 
                timeoutMillis - costTimeSync, request);
        default:
            assert false;
            break;
    }

    return null;
}

可以看到,在这个方法里,先是指定了请求的code,然后分别处理了ASYNCONEWAYSYNC三种类型的消息发送。

4. 消息的发送模式

在消息的发送过程中,我们发现消息发送有如下三种模式:

  1. SYNC:同步模式,消息发送完成后会返回发送的结果
  2. ASYNC:异步模式,不会返回发送结果,不过可以注意监听器监听消息发送结果
  3. ONEWAY:只发送一次,不管结果还是失败

下面我们分别来看看这三种消息是如何发送的。

4.1 同步模式

同步模式的发送示例如下:

public static void main(String[] args) throws MQClientException, InterruptedException {
    String nameServer = "localhost:9876";
    DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
    producer.setNamesrvAddr(nameServer);
    producer.start();

    for (int i = 0; i < 1; i++)
        try {
            Message msg = new Message("TopicTest",
                "TagA",
                "OrderID188",
                "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
            // 同步发送,等待发送完成并返回结果
            SendResult sendResult = producer.send(msg);
            System.out.printf("%s%n", sendResult);
        } catch (Exception e) {
            e.printStackTrace();
        }

    producer.shutdown();
}

这种模式下,发送完成后,会返回发送结果,当结果为失败时,我们可以对其进行额外的处理。

另外,在同步模式下,如果消息发送失败了,rocketMq会自动重试(默认重试2次)。

处理同步发送的方法为NettyRemotingAbstract#invokeSyncImpl

public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
        final long timeoutMillis) throws InterruptedException, RemotingSendRequestException, 
        RemotingTimeoutException {

    final int opaque = request.getOpaque();
    try {
        final ResponseFuture responseFuture = new ResponseFuture(
            channel, opaque, timeoutMillis, null, null);
        this.responseTable.put(opaque, responseFuture);
        final SocketAddress addr = channel.remoteAddress();
        // 发送请求
        channel.writeAndFlush(request)
        // 监听结果
        .addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture f) throws Exception {
                if (f.isSuccess()) {
                    responseFuture.setSendRequestOK(true);
                    return;
                } else {
                    responseFuture.setSendRequestOK(false);
                }

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

        // 等待返回结果
        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);
    }
}

可以看到,在底层处理消息的发送时,netty使用的是异步监听的模式获得返回结果,而所谓的同步模式,就是人为地等待netty完成请求与响应操作,得到结果。

4.2 异步模式

异步模式的发送示例如下:

public static void main(String[] args) throws MQClientException, 
        InterruptedException, UnsupportedEncodingException {
    String nameServer = "localhost:9876";
    DefaultMQProducer producer = new DefaultMQProducer("Jodie_Daily_test");
    producer.setNamesrvAddr(nameServer);
    producer.start();
    producer.setRetryTimesWhenSendAsyncFailed(0);

    int messageCount = 100;
    final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
    for (int i = 0; i < messageCount; i++) {
        try {
            final int index = i;
            Message msg = new Message("Jodie_topic_1023",
                "TagA",
                "OrderID188",
                "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
            // 发送,异步发送的精髓在于 SendCallback
            producer.send(msg, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    countDownLatch.countDown();
                    System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                }

                @Override
                public void onException(Throwable e) {
                    countDownLatch.countDown();
                    System.out.printf("%-10d Exception %s %n", index, e);
                    e.printStackTrace();
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    countDownLatch.await(5, TimeUnit.SECONDS);
    producer.shutdown();
}

与同步模式相比,该模式下,发送方法不会返回发送结果,不过发送方法的参数多了一个SendCallback,当我们想要监听消息的发送结果时,可以重写该类的onSuccess(...)onException(...)方法,从而达到监听发送结果的目的。

在该模式下,如果消息发送失败了,我们可以重写SendCallback#onException方法,在其中定制失败处理的逻辑。

处理异步发送的方法为NettyRemotingAbstract#invokeAsyncImpl

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();
    // 获取锁
    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");
        }

        final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, 
            timeoutMillis - costTime, invokeCallback, once);
        // 添加responseTable中
        this.responseTable.put(opaque, responseFuture);
        try {
            // netty的异步操作
            channel.writeAndFlush(request)
            // 监听结果
            .addListener(new ChannelFutureListener() {
                // 处理完成的操作
                @Override
                public void operationComplete(ChannelFuture f) throws Exception {
                    if (f.isSuccess()) {
                        responseFuture.setSendRequestOK(true);
                        return;
                    }
                    // 处理失败
                    requestFail(opaque);
                    log.warn(...);
                }
            });
        } catch (Exception e) {
            ...
        }
    } else {
        ...
    }
}

从以上方法可以看到,在监听执行结果的逻辑中,如果成功,就调用responseFuture.setSendRequestOK(true),然后返回;如果失败了,就调用requestFail,这些操作是怎么与SendCallback关联起来的呢?我们接下来就好好分析一番。

在调用writeAndFlush(...)方法前,会先this.responseTable.put(opaque, responseFuture)方法,将responseFuture添加到responseTable中,这是个Map结构,rocketMq正是定时从responseTable中获取responseFuture并判断其状态来决定调用SendCallback的哪个方法的。

让我们回到NettyRemotingClient的启动流程,方法为NettyRemotingClient#start

public void start() {
    ...
    // 扫描消息获取结果,每秒执行1次
    this.timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            try {
                NettyRemotingClient.this.scanResponseTable();
            } catch (Throwable e) {
                log.error("scanResponseTable exception", e);
            }
        }
    }, 1000 * 3, 1000);
    ...
}

在这个方法中,启动了一个定时任务,每秒执行1次,所做的工作就是扫描在responseTable中的responseFuture,我们再进入NettyRemotingAbstract#scanResponseTable方法:

public void scanResponseTable() {
    //本次要处理的返回
    final List<ResponseFuture> rfList = new LinkedList<ResponseFuture>();
    Iterator<Entry<Integer, ResponseFuture>> it = this.responseTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<Integer, ResponseFuture> next = it.next();
        ResponseFuture rep = next.getValue();

        // 判断时间,时间到了才转移到 rfList 中
        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 {
            executeInvokeCallback(rf);
        } catch (Throwable e) {
            log.warn("scanResponseTable, operationComplete Exception", e);
        }
    }
}

在这个方法里,先遍历所有的ResponseFuture,然后判断每个ResponseFuture的时间,时间到了才会进行处理,从这里可以看出,并不是一有结果就立即处理,而是在消息发送后过了4秒(rep.getTimeoutMillis()的值为3)才去处理结果,处理方法为NettyRemotingAbstract#executeInvokeCallback,我们继续跟进去:

private void executeInvokeCallback(final ResponseFuture responseFuture) {
    boolean runInThisThread = false;
    // 如果有线程池,就提交到线程池中执行
    ExecutorService executor = this.getCallbackExecutor();
    if (executor != null) {
        try {
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 这是是具体操作
                        responseFuture.executeInvokeCallback();
                    } catch (Throwable e) {
                        log.warn(...);
                    } finally {
                        responseFuture.release();
                    }
                }
            });
        } catch (Exception e) {
            runInThisThread = true;
            log.warn(...);
        }
    } else {
        runInThisThread = true;
    }

    if (runInThisThread) {
        try {
            // 直接执行
            responseFuture.executeInvokeCallback();
        } catch (Throwable e) {
            log.warn("executeInvokeCallback Exception", e);
        } finally {
            responseFuture.release();
        }
    }
}

这个方法没做啥事,只是判断要不要在线程池中执行操作,之后就调用了 ResponseFuture#executeInvokeCallback方法:

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

到这个就是关键了,我们跟进invokeCallback.operationComplete(this)方法,发现来到了MQClientAPIImpl#sendMessageAsync(...)方法:

private void sendMessageAsync(
    ...
    // sendCallback 由参数传入
    final SendCallback sendCallback,
    // 省略其他参数
    ...
    ) throws InterruptedException, RemotingException {
    final long beginStartTime = System.currentTimeMillis();

    // remotingClient.invokeAsync 最终会调用到 NettyRemotingAbstract#invokeAsyncImpl
    this.remotingClient.invokeAsync(addr, request, timeoutMillis, 
        // InvokeCallback 参数
        new InvokeCallback() {
            @Override
            public void operationComplete(ResponseFuture responseFuture) {
                long cost = System.currentTimeMillis() - beginStartTime;
                RemotingCommand response = responseFuture.getResponseCommand();
                ...

                if (response != null) {
                    try {
                        SendResult sendResult = MQClientAPIImpl.this
                            .processSendResponse(brokerName, msg, response, addr);
                        ...

                        try {
                            // 这里执行 sendCallback 的 onSuccess(...) 方法
                            sendCallback.onSuccess(sendResult);
                        } catch (Throwable e) {
                        }

                    } catch (Exception e) {
                        onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, 
                            sendCallback, topicPublishInfo, instance,
                            retryTimesWhenSendFailed, times, e, context, false, producer);
                    }
                } else {
                    // 处理各异常
                    ...
                        // 调用异常处理方法
                        onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, 
                            sendCallback, topicPublishInfo, instance,
                            retryTimesWhenSendFailed, times, ex, context, true, producer);
                    ...
                }
            }
        });
}

这个方法主要调用了remotingClient.invokeAsync()方法,而remotingClient.invokeAsync()最终会调用到 NettyRemotingAbstract#invokeAsyncImpl,也就是uemd提到的最终使用异常发送的netty方法。

remotingClient.invokeAsync()方法共有3个参数,这里我们只关注最后一个参数:InvokeCallback,该参数的operationComplete(...)方法中,就是处理操作完成的逻辑了,这里我们重点关注两个操作就可以了:

  • sendCallback.onSuccess(sendResult):响应成功时调用,执行的就是 sendCallbackonSuccess(...) 方法,sendCallback由参数传入
  • onExceptionImpl(...):方法,出现异常时调用

我们继续进入MQClientAPIImpl#onExceptionImpl方法:

private void onExceptionImpl(final String brokerName,
    ...
    // sendCallback 由方法参数传入
    final SendCallback sendCallback,
    ...
) {
    int tmp = curTimes.incrementAndGet();
    // 处理重试操作
    if (needRetry && tmp <= timesTotal) {
        ...
    } else {
        ...

        try {
            // 执行 sendCallback的onException(..,) 方法
            sendCallback.onException(e);
        } catch (Exception ignored) {
        }
    }
}

可以看到,sendCallbackonException(..,) 方法就是在这里调用的。

在这个方法里,也会对消息进行重试操作,这个重试次数是在DefaultMQProducerImpl#sendKernelImpl中传入的:

4.3 oneway 模式

该模式的使用示例如下:

public static void main(String[] args) throws MQClientException, InterruptedException {
    String nameServer = "localhost:9876";
    DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
    producer.setNamesrvAddr(nameServer);
    producer.start();

    for (int i = 0; i < 1; i++)
        try {
            Message msg = new Message("TopicTest",
                "TagA",
                "OrderID188",
                "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
            // oneway模式,发送方法没有返回值
            producer.sendOneway(msg);
        } catch (Exception e) {
            e.printStackTrace();
        }

    producer.shutdown();
}

不同于前面两种模式,该模式下,消息只会发送一次,且不会返回任何结果,也无监听参数可以监听消息结果,总之,该模式下,消息只发送一次且不管结果。

处理该模式的方法为 NettyRemotingAbstract#invokeOnewayImpl

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 {
            channel.writeAndFlush(request)
            // 监听结果,操作完成,仅打印了一条日志
            .addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture f) throws Exception {
                    once.release();
                    if (!f.isSuccess()) {
                        log.warn(...);
                    }
                }
            });
        } catch (Exception e) {
            ...
        }
    } else {
        ...
    }
}

在这个方法中可以看到,消息发送结果的监听逻辑中,当消息发送失败了,仅仅只是打了一条warn级别的日志。

5. 总结

本文主要是梳理了producer启动及发送消息的流程,这里我们来做一个总结。

rocketMq在启动时,启动所做的工作如下:

  1. 装配了netty客户端相关配置
  2. 启动定时任务,如获取nameserver地址,定时更新topic的路由信息,定时发送心跳信息

发送消息时,rocketmq主要支持三种发放方式:同步(sync),异步(async)及单向(oneway)

  • 同步:消息发放后,线程会阻塞,直到返回结果
  • 异步:在发送消息时,可以设置消息发送结果的监听,消息发送后,线程不会阻塞,消息发送完成后,发送结果会被监听到
  • 单向:消息发送完成后,线程不会阻塞,不会有结果返回,也无法设置发送结果的监听,即发送就可以,不关心发送结果,不关心是否发送成功

在消息可靠性方面,

  • 同步发送:消息发送失败时,内部会重试(默认1次发送+2次失败重试,共3次),另外,由于发送完成后可以得到发送结果,因此也可对失败的结果进行自主处理
  • 异步发送:消息发送失败时,同时有内部重试(默认1次发送+2次失败重试,共3次),另外,发送消息时可以设置消息的监听规则,当发送失败时,可以在监听代码中自主对失败的消息进行处理
  • 单向发送:该模式下,消息发送失败时无重试(只是打出一条warn级别的日志),且无发送结果返回、无结果监听

好了,关于producer就介绍到这里了。


限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。

本文首发于微信公众号 Java技术探秘,如果您喜欢本文,欢迎关注该公众号,让我们一起在技术的世界里探秘吧!