2. RocketMQ生产者:如何启动和发送不同消息

1,100 阅读6分钟

1. 概述

在业务中,我们通常考虑如何发送可靠的消息?如何评估消息系统的发送能力?基于这2个问题,我们来阅读rocketmq的生产者模块。主要分2个部分的内容:1. 生产者如何启动和关闭 2. 生产者如何发送各类消息:普通消息、事务消息和延迟消息。普通消息:主要用来解耦业务;事务消息,是在普通消息的能力上,加上了消息发送的可靠性;延迟消息,主要为了来解决一些延迟发生的事情,例如:订单自动关闭等问题。这里已rocketmq为实例,探索mq的设计,假如有机会,可以应用对其他mq组件的学习。

2. 生产者启动和关闭

RocketMQ的生产者源码阅读是通过阅读MQProducer(org.apache.rocketmq.client.producer.MQProducer)的接口是如何被实现。这里我们主要通过start(),shutdown()和send()三个方法。MQProducer的send()主要扩展三个维度:发送方式(同步、异步、oneway)、发送事务消息sendMessageInTransaction()和是否批量。同时,我们通过消息的本身的三种类型(org.apache.rocketmq.common.message.MessageType):正常消息,事务(half和commit)和延迟(delay),来更加深入的理解RocketMQ的能力。

目标:
1. MQ本身是用来解决系统之间的解耦、削峰的目的。
2. 事务消息是为了解决消息发送可靠性的问题
3. 延迟消息是为了提升性能,满足更多复杂的业务场景,例如:支付定时取消,订单超时关闭等等
4. 有序消息(待定)

2.1 生产者启动过程

后续补;核心几个实例的创建:

  • MQClientManager
  • MQClientInstance
  • MQClientApiImpl
  • 自定义serviceThread
  • RequestFutureTable的处理
  • MQ的tracer

2.2 生产者关闭过程

关闭过程相对简单,就直接贴代码了。理论上一个应用的关闭主要做几件事情:

  • 告诉别人要要关闭
  • 各个业务线程池或线程进行关闭(易于java正常杀死)
  • 核心对象赋值为null(易于gc)

而从下面代码来看,确实做了这件事情

 public void shutdown(final boolean shutdownFactory) {
        switch (this.serviceState) {
            case CREATE_JUST:
                break;
            case RUNNING:
                this.mQClientFactory.unregisterProducer(this.defaultMQProducer.getProducerGroup());
                this.defaultAsyncSenderExecutor.shutdown();
                if (shutdownFactory) {
                    this.mQClientFactory.shutdown();
                }
                this.timer.cancel();
                log.info("the producer [{}] shutdown OK", this.defaultMQProducer.getProducerGroup());
                this.serviceState = ServiceState.SHUTDOWN_ALREADY;
                break;
            case SHUTDOWN_ALREADY:
                break;
            default:
                break;
        }
    }

3. 生产者的消息发送

RocketMQ的消费发送我们从方法org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl开始。看它是在发送中做了哪些处理。在这之前,最好去了解Message的结构和TopicPublishInfo的结构。所有的消息发送逻辑都是从这2个地方开始。核心解决的是:

  1. 现在topic对应队列的环境是什么样的
  2. 目前选择哪个队列,可以选择哪个broker,策略是什么
  1. 发送失败了,策略又应该是什么

3.1 普通消息发送逻辑

3.1.1 业务流程

3.1.1.1 选队列

selectOneMessageQueue是TopicPublishInfo的核心方法之一,下面是它的一个默认方法。通过对代码的理解,它选队列的默认策略是轮询。而更加复杂的策略是通过org.apache.rocketmq.client.latency.MQFaultStrategy类来实现。

 public MessageQueue selectOneMessageQueue() {
        int index = this.sendWhichQueue.incrementAndGet();
        int pos = Math.abs(index) % this.messageQueueList.size();
        if (pos < 0)
            pos = 0;
        return this.messageQueueList.get(pos);
    }

这里逻辑一些选队列的场景:

  • 失败重选
  • 时延(sendLatencyFaultEnable)

3.1.1.2 发送内核实现

发送内核方法主要解决几件事情:

  • Broker Address的获取
  • 封装SendMessageRequestHeader
  • mQClientFactory .getMQClientAPIImpl().sendMessage

这里说为什么主要说这三件事情。是因为通过这里的学习,你可能能够看到大多数的RocketMQ的源码。

Broker Address

对于一次通信来说,RocketMQ保证了是通过点对点通信。但是这里里面有没有什么优化,可以更加深入的去理解。例如连接共享,比如Broker Address同步。 这不去细说,可以去看代码。

封装SendMessageRequestHeader

这个才是这部分的重点,也许会觉得也是 最没有技术含量的。因为我们点进它所在的包路径:org.apache.rocketmq.common.protocol。它定义了整个RocketMQ的通信协议,假如能够看懂这个,其他的交互指令有什么困难。

mQClientFactory .getMQClientAPIImpl().sendMessage

这个是rocketmq在Remote之上封装的一层api。remote主要定义RemoteCommand和RemoteService的实现。如何将Protocol中的定义转换成RemoteCommand都是通过这一层来做。这里层解决了协议(Protocol)的问题。

下面这段代码,是对以上逻辑的一段验证。最终,MQClientAPIImpl转换成了一个RemotingCommand。

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

这里要关注一个关键信息是RequestCode.SEND_MESSAGE。它帮助我们追踪Broker是如何进行处理生产者发送的消息的。


3.1.1.3 SendResult处理

根据RocketMQ的发送模式:Asyn、OneWay和Sync。只有Sync对SendResult做了特殊处理。

  • 返回结果
  • 返回结果失败时,可以通过配置,让其重试
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;
    }

SendResult 也是经过了 MQClientAPIImpl处理。具体可以阅读如下代码org.apache.rocketmq.client.impl.MQClientAPIImpl#processSendResponse

 SendMessageResponseHeader responseHeader =
                (SendMessageResponseHeader) response.decodeCommandCustomHeader(SendMessageResponseHeader.class);

这里普通消息的发送过程已经讲完了,更加细节的内容,大家可以去代码中领略。

3.2 事务消息

上文我们讲了阅读发送正常消息的过程。接下来我们看看RocketMQ是如何实现事务消息的。在接口MQProducer中有如下接口。可以去看他的实现,发现一个问题:DefaultMQProducerImpl是没有实现这个功能的,只有TransactionMQProducer实现。我始终认为:事务消息不是一个很好的概念,容易跟数据库acid特性搞混或者隐喻,可以把它定义为"可靠消息"似乎更加合理。其核心目的保证消息发送成功和业务执行的一致。(这里的场景需要细细分析,这里先忽略)

/**
     * This method is used to send transactional messages.
     *
     * @param msg Transactional message to send.
     * @param arg Argument used along with local transaction executor.
     * @return Transaction result.
     * @throws MQClientException
     */
    @Override
    public TransactionSendResult sendMessageInTransaction(Message msg,
        Object arg) throws MQClientException {
        throw new RuntimeException("sendMessageInTransaction not implement, please use TransactionMQProducer class");
    }

TransactionMQProducer

在深入分析sendMessageInTransaction之前,可以先看看TransactionMQProducer的结构

public class TransactionMQProducer extends DefaultMQProducer {
    private int checkThreadPoolMinSize = 1;
    private int checkThreadPoolMaxSize = 1;
    private int checkRequestHoldMax = 2000;
    private ExecutorService executorService;
    private TransactionListener transactionListener;
}

移除废弃的字段,就新增2个关键信息:1. 线程执行器,2. TransactionListener。 TransactionListener是用来回调本地事务的。

\

sendMessageInTransaction

事务发送核心逻辑与正常消息发送的逻辑差异是:

  • 发送前,添加了实现相关属性(PROPERTY_TRANSACTION_PREPARED
  • 发送后,封装了事务SendResult
  • 调用了TransactionListener    
  • 调用endTransaction方法

这里我给自己提了了2个问题:1. RemotingCommand有什么差异 2. Broker是如何处理事务消息

RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, requestHeader);
request.setRemark(remark);
this.remotingClient.invokeOneway(addr, request, timeoutMillis);

这段代码展示了RequestCode.END_TRANSACTION,这个指令要记住,看它如何被broker处理。

经典问题:二段提交如何解决一致性问题?

3.3 延迟消息

延迟消息没有专门的发送方法,主要在RocketMQ中broker中实现延迟机制。而发送端,只能靠Message的方法setDelayTimeLevel()。 总共18个级别,可自定义。具体查看引用的文章[1]

4 总结

这篇主要是通过抓住MQProducer这个接口,通过解析其核心能力发送消息(Send)和Message对象来分开解释了其实现原理。其中,事务消息主要通过二段提交的经典解决方案来保证一致性,正常消息和延迟消息居然只是个别属性的差异。底层依赖于Remoting模块,在这模块构建了MQClientAPIImpl。 合理的分层和封装,简化了业务复杂度,可以模仿和学习。 这里少了异步消息的处理方式。

引用:

  1. RocketMQ 延迟队列