【RocketMQ】消息的生产以及发送过程

1  前言

上节我们主要看了下 NameServer 的启动以及与 Broker 之间的通信比如路由的注册、发现与删除,那么本节我们将从消息的数据结构开始,逐步介绍生产者的启动流程和消息发送的流程。

看之前可以先准备个测试发送消息的,一方面不懂的可以随时调试,另一方面可以帮助更好的理解:

public class RocketMQProducerTest {

    @SneakyThrows
    public static void main(String[] args) {
        DefaultMQProducer producer = new DefaultMQProducer("test");
        // nameserver 地址
        producer.setNamesrvAddr("0.0.0.0:9876");
        producer.start();

        // 创建一个消息实例
        Message msg = new Message(
                "TopicTest", // topic
                "TagA",      // tag
                "OrderID18", // key
                "Hello RocketMQ".getBytes(RemotingHelper.DEFAULT_CHARSET)); // body

        // 发送消息
        SendResult sendResult = producer.send(msg);
        System.out.printf("%s%n", sendResult);

        producer.shutdown();

    }
}

2  消息

2.1  消息发送方式

RocketMQ 支持 3 种消息发送方式:同步(sync)、 异步(async)、单向(oneway)。

同步: 发送者向MQ执行发送消息API时,同步等待, 直到消息服务器返回发送结果。

异步: 发送者向MQ执行发送消息API时,指定消息发送成功后的回掉函数,然后调 用消息发送API后,立即返回,消息发送者线程不阻塞,直到运行结束,消息发送成功或 失败的回调任务在一个新的线程中执行。

单向:消息发送者向MQ执行发送消息API时,直接返回,不等待消息服务器的结果, 也不注册回调函数,简单地说,就是只管发,不在乎消息是否成功存储在消息服务器上。

三种方式应该比较好理解哈。

2.2  消息实体

RocketMQ 消息封装类是 org.apache.rocketmq.common.message.Message。

Message 的基础属性主要包括消息所属主题Topic, 消息Flag(RocketMQ 不做处理)、 扩展属性、消息体。

Message 扩展属性主要包含下面几个:

(1)Tag :消息 TAG,用于消息过滤。

(2)keys: Message 索引键, 多个用空格隔开, RocketMQ 可以根据这些 key快速检索到消息。

(3)waitStoreMsgOK:消息发送时是否等消息存储完成后再返回。

(4)delayTimeLevel : 消息延迟级别,用于定时消息或消息重试。 这些扩展属性存储在Message 的 properties 中。 

3  生产者启动流程

消息生产者的代码都在client模块中,相对于RocketMQ来说,它就是客户端,也是消 息的提供者,我们在应用系统中初始化生产者的一个实例即可使用它来发消息。

3.1  初识 DefaultMQProducer 消息发送者

DefaultMQProducer 是默认的消息生产者实现类,它实现 MQAdmin 的接口,其主要接口一览如下:

下面看下DefaultMQProducer 的主要方法:

(1)创建主题 void createTopic(String key, String newTopic, int queueNum, int topicSysF!ag) key :目前未实际作用,可以与newTopic相同 newTopic: 主题名称 queueNum:队列数量 topicSysF!ag :主题系统标签,默认为 0

(2)根据时间戳从队列中查找其偏移量 long searchOffset(final M巳ssageQueue mq, final long timestamp)

(3)查找该消息队列中最大的物理偏移量 long maxOffset(final MessageQueue mq)

(4)查找该消息队列中最小物理偏移量 long minOffset(final MessageQueue mq)

(5)根据消息偏移量查找消息 MessageExt viewMessage(final String offsetMsgld)

(6)根据条件查询消息 QueryResult queryM巳ssage(final String topic, final String key, final int maxNum, final long begin, final long end) topic :消息主题 key :消息索引字段 maxNum:本次最多取出消息条数 begin :开始时间 end :结束时间

(7)根据主题与消息ID查找消息 MessageExt viewMessage(String topic,String msgld)

(8)查找该主题下所有的消息队列 List<MessageQueue> fetchPublishMessageQueues(final String topic) 

(9)同步发送消息,具体发送到主题中的哪个消息队列由负载算法决定 SendResult send(final Message msg)

(10)同步发送消息,如果发送超过timeout则抛出超时异常 SendResult send(final Message msg, final long timeout)

(11)异步发送消息, sendCallback参数是消息发送成功后的回调方法 void send(final Message msg, final SendCallback sendCallback)

(12)异步发送消息,如果发送超过timeout指定的值,则抛出超时异常 void send(final Message msg, final SendCallback sendCallback, final long timeout)

(13)单向消息发送,就是不在乎发送结果,消息发送出去后该方法立即返回 void sendOneway(final Message msg)

(14)同步方式发送消息,发送到指定消息队列 SendResult send(final Message msg, final MessageQueue mq)

(15)异步方式发送消息,发送到指定消息队列 void send(final Message msg, final MessageQueue mq, final SendCallback send Callback)

(16)单向方式发送消息,发送到指定的消息队列 void sendOneway(final Message msg, final MessageQueue mq)

(17)消息发送,指定消息选择算法,覆盖消息生产者默认的消息队列负载 SendResult send(final Message msg, final MessageQueueSelector selector, final Object arg)

(18)同步批量消息发送 SendResult send(final Collection msgs, final MessageQueu巳 mq, final long timeout)

继续看下 DefaultMQProducer  的主要属性:

// 生产者所属组,消息服务器在回查事务状态时会随机选择该组中任何一个生产者发起事务回查请求 
private String producerGroup;
// 默认 topicKey:TBW102
private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC;
// 默认主题在每一个 Broker 队列数量
private volatile int defaultTopicQueueNums = 4;
// 发送消息默认超时时间, 默认 3s
private int sendMsgTimeout = 3000;
// 消息体超过该值则启用压缩,默认 4K
private int compressMsgBodyOverHowmuch = 1024 * 4;
// 同步方式发送消息重试次数,默认为 2,总共执行 3 次
private int retryTimesWhenSendFailed = 2;
// 异步方式发送消息重试次数,默认为 2
private int retryTimesWhenSendAsyncFailed = 2;
// 消息重试时选择另外一个 Broker 时 是否不等待存储结果就返回, 默认为false
private boolean retryAnotherBrokerWhenNotStoreOK = false;
// 允许发送的最大消息长度,默认为 4M
private int maxMessageSize = 1024 * 1024 * 4; // 4M
// 允许生产者在发送消息时自动将多条消息打包成一个批次进行发送,从而提高消息发送的效率 默认为false
private boolean autoBatch = false;
// 主要用于在发送消息时累积消息,以便进行批量发送,从而提高消息发送的效率
private ProduceAccumulator produceAccumulator = null;
// 是否开启异步发送消息时帮助控制发送速率
private boolean enableBackpressureForAsyncMode = false;
// 控制生产者最多同时发送多少个消息 默认10000
private int backPressureForAsyncSendNum = 10000;
// 控制生产者最多同时发送消息的最大大小 默认100M
private int backPressureForAsyncSendSize = 100 * 1024 * 1024;
// 继承的ClientConfig 配置类的属性
protected String namespace;

producerGroup 如何理解?源码里注释说的这个属性值跟事务消息很重要:

在 RocketMQ 中,producerGroup 对于事务消息(Transactional Message)确实至关重要。事务消息是指那些需要在本地事务完成之后才能确定是否发送的消息。事务消息涉及两个阶段:

  1. 半消息(Half Message):这是事务消息的第一个阶段,在这个阶段,生产者会发送一条半消息到 Broker,但此时消息还没有完全提交。
  2. 确认阶段:生产者完成本地事务处理后,会告诉 Broker 这条消息应该被确认(Commit)还是回滚(Rollback)。

在这个过程中,producerGroup 的作用体现在以下几个方面:

事务消息流程中的作用

  1. 消息发送:
    • 当生产者发送一条事务消息时,它首先会发送一条半消息到 Broker,并附带一个本地事务执行的回调(CheckCallback)。这个回调会在 Broker 请求确认时被执行。
  2. Broker 请求确认:
    • Broker 收到半消息后,会异步请求生产者确认该消息的状态(Commit 或 Rollback)。这个请求会发送到发送这条半消息的 producerGroup 中的任意一个生产者实例。
  3. 确认或回滚:
    • 生产者根据本地事务的状态执行 CheckCallback,然后告诉 Broker 这条消息应该被确认还是回滚。
    • 如果确认,则 Broker 将半消息标记为可消费的状态;如果回滚,则 Broker 将这条消息删除或标记为不可消费的状态。

为什么 producerGroup 至关重要

  1. 唯一标识:producerGroup 是 Broker 用来识别和联系发送事务消息的生产者的重要标识。由于事务消息需要后续确认,Broker 必须能够找到正确的生产者来确认消息的状态。
  2. 容错处理:如果发送事务消息的生产者实例在确认阶段之前崩溃或不可用,Broker 可能需要联系同一 producerGroup 中的另一个生产者实例来完成确认流程。因此,producerGroup 不仅标识了一组逻辑上的生产者,还提供了一种容错机制。
  3. 一致性保证:事务消息的设计目的是为了保证消息的一致性。通过 producerGroup,RocketMQ 确保了事务消息的处理逻辑在分布式环境下的一致性和可靠性。

namespace 属性是继承的 ClientConfig,这个属性主要是 RocketMQ 中用于逻辑隔离和组织消息的重要机制。通过 namespace,可以实现不同应用或环境的消息隔离,提高系统的灵活性和可管理性。合理配置和使用 namespace 可以帮助更好地管理和优化消息传递系统。

3.2  消息生产者启动流程

在看启动之前,我们需要先看下 DefaultMQProducer 的实例化,因为它的启动其实是交给内部的 defaultMQProducerImpl。所以我们先看下实例化:

// DefaultMQProducer
public DefaultMQProducer(final String producerGroup) {
    // 内部重载
    // 第一个参数 namespace
    // 第二个参数 producerGroup
    // 第三个参数 rpcHook
    this(null, producerGroup, null);
}

其中 RPCHook 类似钩子函数,是 RocketMQ 提供的一个扩展点,允许用户在关键的 RPC 调用前后插入自定义逻辑,比如日志记录等。

public interface RPCHook {
    // 请求前
    void doBeforeRequest(final String remoteAddr, final RemotingCommand request);
    // 请求后
    void doAfterResponse(final String remoteAddr, final RemotingCommand request, final RemotingCommand response);
}

继续看实例化:

public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) {
    this.namespace = namespace;
    this.producerGroup = producerGroup;
    // 实例化 DefaultMQProducerImpl
    defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
    produceAccumulator = MQClientManager.getInstance().getOrCreateProduceAccumulator(this);
}

那我们看看 DefaultMQProducerlmpl  的实例化内部都做了什么:

public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) {
    // 互相引用
    this.defaultMQProducer = defaultMQProducer;
    this.rpcHook = rpcHook;
    // 异步消息队列 队列大小默认 5万
    this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue<>(50000);
    // 创建异步发送消息线程池
    this.defaultAsyncSenderExecutor = new ThreadPoolExecutor(
        Runtime.getRuntime().availableProcessors(),
        Runtime.getRuntime().availableProcessors(),
        1000 * 60,
        TimeUnit.MILLISECONDS,
        this.asyncSenderThreadPoolQueue,
        new ThreadFactoryImpl("AsyncSenderExecutor_"));
    // 信号量锁 控制最多允许同时发送多少个消息 默认1万
    if (defaultMQProducer.getBackPressureForAsyncSendNum() > 10) {
        semaphoreAsyncSendNum = new Semaphore(Math.max(defaultMQProducer.getBackPressureForAsyncSendNum(), 10), true);
    } else {
        semaphoreAsyncSendNum = new Semaphore(10, true);
        log.info("semaphoreAsyncSendNum can not be smaller than 10.");
    }
    // 信号量锁 控制最多同时发送消息的大小 默认100M
    if (defaultMQProducer.getBackPressureForAsyncSendSize() > 1024 * 1024) {
        semaphoreAsyncSendSize = new Semaphore(Math.max(defaultMQProducer.getBackPressureForAsyncSendSize(), 1024 * 1024), true);
    } else {
        semaphoreAsyncSendSize = new Semaphore(1024 * 1024, true);
        log.info("semaphoreAsyncSendSize can not be smaller than 1M.");
    }
    ...
}

可以看到主要初始化一些异步消息发送需要的线程池以及并发量控制。

那么接下来我们就看生产者的启动,我们从 DefaultMQProducer 的 start 方法开始看起:

public void start() throws MQClientException {
    // 设置生产者组,应用命名空间前缀
    this.setProducerGroup(withNamespace(this.producerGroup));
    // 启动实际的生产者实现类
    this.defaultMQProducerImpl.start();
    // 如果生产者累积器不为空,则启动它,用于累积生产者相关的数据
    if (this.produceAccumulator != null) {
        this.produceAccumulator.start();
    }
    // 如果消息追踪调度器不为空,则尝试启动它,用于追踪消息发送过程
    if (null != traceDispatcher) {
        try {
            // 启动追踪调度器,传入Name Server地址和访问通道
            traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
        } catch (MQClientException e) {
            // 如果启动失败,记录警告日志,但不抛出异常,确保生产者能够继续启动
            logger.warn("trace dispatcher start failed ", e);
        }
    }
}

我们这里主要看下 DefaultMQProducerlmpl 的 start 方法:

public void start() throws MQClientException {
    // 调用重载启动
    this.start(true);
}
public void start(final boolean startFactory) throws MQClientException {
    // 根据当前的服务状态执行相应的启动逻辑
    // 初始值 = ServiceState.CREATE_JUST
    // private ServiceState serviceState = ServiceState.CREATE_JUST;
    switch (this.serviceState) {
        case CREATE_JUST:
            // 初始化服务状态为启动失败,以便在出现错误时能够恢复到创建状态
            this.serviceState = ServiceState.START_FAILED;

            // 检查配置的正确性
            // 主要是检查 producerGroup 不能为空 不能超过255个字符 不能跟默认的DEFAULT_PRODUCER名字相同
            this.checkConfig();

            // 如果生产者组不是内部使用的组,则修改实例名为PID + 当前的纳秒值
            // instanceName = UtilAll.getPid() + "#" + System.nanoTime()
            if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
                this.defaultMQProducer.changeInstanceNameToPID();
            }

            // 获取或创建MQ客户端工厂实例
            this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);

            // 尝试在MQ客户端工厂中注册生产者组
            boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
            if (!registerOK) {
                // 如果注册失败,恢复到创建状态,并抛出异常
                this.serviceState = ServiceState.CREATE_JUST;
                throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
                    + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                    null);
            }

            // 根据参数决定是否启动MQ客户端工厂
            if (startFactory) {
                mQClientFactory.start();
            }

            // 启动故障检测器
            this.mqFaultStrategy.startDetector();

            // 记录启动日志,并更新服务状态为运行中
            log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
                this.defaultMQProducer.isSendMessageWithVIPChannel());
            this.serviceState = ServiceState.RUNNING;
            break;
        case RUNNING:
        case START_FAILED:
        case SHUTDOWN_ALREADY:
            // 如果服务状态不允许启动,则抛出异常
            throw new MQClientException("The producer service state not OK, maybe started once, "
                + this.serviceState
                + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                null);
        default:
            // 其他状态不做处理
            break;
    }

    // 向所有Broker发送心跳包,以维持连接
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();

    // 启动定时任务,如心跳检测等
    RequestFutureHolder.getInstance().startScheduledTask(this);
}

可以看到主要做了几件事:

(1)检查配置,主要是检查 producerGroup 属性

(2)初始化当前生产者的 instanceName(按照进程PID + 纳秒值组成)

(3)获取或者创建 MQClientInstance,它封装了 RocketMQ 网络处理 API,是消息生产者( Producer)、消息消费者 ( Consumer)与 NameServer、 Broker 打交道的网络通道。

(4)registerProducer 注册生产者,这步很简单就是将将当前生产者加入到 MQClientlnstance 管理

(5)启动 MQClientInstance

(6)日志、检测以及发送心跳

我们先看下 MQClientInstance 的获取或者创建 getOrCreateMQClientInstance 方法:

private MQClientInstance mQClientFactory;
// 通过 MQClientManager 来创建当前生产者的 MQClientInstance
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
/**
 * 获取或创建MQ客户端实例
 * 此方法首先尝试根据客户端配置在已存在的实例表中查找MQ客户端实例如果找不到,则创建一个新的实例并添加到实例表中
 *
 * @param clientConfig 客户端配置,用于构建和识别MQ客户端实例
 * @param rpcHook RPC钩子,用于在MQ客户端实例中执行额外的逻辑
 * @return 返回找到或新创建的MQ客户端实例
 */
// MQClientManager
// 单例 private static MQClientManager instance = new MQClientManager();
// 缓存 private ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable = new ConcurrentHashMap<>();
public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
    // 根据客户端配置生成唯一的客户端ID
    String clientId = clientConfig.buildMQClientId();
    // 尝试从实例表中获取已存在的MQ客户端实例
    MQClientInstance instance = this.factoryTable.get(clientId);
    if (null == instance) {
        // 如果实例不存在,创建一个新的MQ客户端实例
        instance =
            new MQClientInstance(clientConfig.cloneClientConfig(),
                this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
        // 将新创建的实例添加到实例表中,如果clientId已存在,则返回已存在的实例
        MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);
        if (prev != null) {
            // 如果已存在实例,返回该实例,并记录日志
            instance = prev;
            log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
        } else {
            // 如果不存在,记录新创建实例的日志
            log.info("Created new MQClientInstance for clientId:[{}]", clientId);
        }
    }
    // 返回MQ客户端实例
    return instance;
}

整个 JVM 实例中只存在一个MQC!ientManager实例,维护一个 MQClientlnstance 缓存表 ConcurrentMap<String/*Clientld */, MQClientinstance> factoryTable = new ConcurrentHashMap<String, MQClientlnstance>(), 也就是同一个 clientld 只会创建一个MQClientinstance。

我们再看下创建 clientld 的方法:

public String buildMQClientId() {
    // 使用StringBuilder高效地构建字符串
    StringBuilder sb = new StringBuilder();
    // 首先添加客户端IP地址,作为MQ客户端ID的一部分
    sb.append(this.getClientIP());
    // 添加实例名称前缀,以分隔不同实例的请求
    sb.append("@");
    sb.append(this.getInstanceName());
    // 如果单元名称非空,则添加单元名称,进一步细化请求的来源
    if (!UtilAll.isBlank(this.unitName)) {
        sb.append("@");
        sb.append(this.unitName);
    }
    // 如果启用了流式请求类型,则在ID中添加流式请求标记
    if (enableStreamRequestType) {
        sb.append("@");
        sb.append(RequestType.STREAM);
    }
    // 返回构建完成的MQ客户端ID字符串
    return sb.toString();
}

clientld 为客户端 IP + instance(默认会替换成 PID + 当前纳秒值) + (unitname 可选)+ (流式可选)。

然后我们最后看下 MQClientInstance 的启动:

public void start() throws MQClientException {
    // 加锁
    synchronized (this) {
        // 根据当前服务状态执行相应操作
        switch (this.serviceState) {
            case CREATE_JUST:
                // 初始将服务状态标记为启动失败,以确保安全
                this.serviceState = ServiceState.START_FAILED;
                // 如果未指定 Name Server 地址,则从 NameServer 获取地址
                if (null == this.clientConfig.getNamesrvAddr()) {
                    this.mQClientAPIImpl.fetchNameServerAddr();
                }
                // 启动请求-响应通道
                this.mQClientAPIImpl.start();
                // 启动各种定时任务
                this.startScheduledTask();
                // 启动消息拉取服务
                this.pullMessageService.start();
                // 启动再平衡服务
                this.rebalanceService.start();
                // 启动推送服务
                this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
                log.info("客户端工厂 [{}] 启动成功", this.clientId);
                // 将服务状态设置为运行中
                this.serviceState = ServiceState.RUNNING;
                break;
            case START_FAILED:
                // 如果之前已创建且启动失败,抛出异常
                throw new MQClientException("Factory 对象 [" + this.getClientId() + "] 已经创建过,并且启动失败。", null);
            default:
                // 其他状态不做处理
                break;
        }
    }
}

好了,那我们简单捋一下生产者的启动过程:

4  消息发送基本流程

消息发送流程主要的步骤:验证消息、查找路由、 消息发送(包含异常处理机制)。

我们看一下同步消息的发送入口:

public SendResult send(Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    // Topic 用名称空间包一层
    msg.setTopic(withNamespace(msg.getTopic()));
    // 开启批量的话
    if (this.getAutoBatch() && !(msg instanceof MessageBatch)) {
        return sendByAccumulator(msg, null, null);
    } else {
        // 不开启批量走这里 我们看这个
        return sendDirect(msg, null, null);
    }
}
public SendResult sendDirect(Message msg, MessageQueue mq, SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
    // 可以看到最后都交给了 defaultMQProducerImpl 来发送
    if (sendCallback == null) {
        if (mq == null) {
            return this.defaultMQProducerImpl.send(msg);
        } else {
            return this.defaultMQProducerImpl.send(msg, mq);
        }
    } else {
        if (mq == null) {
            this.defaultMQProducerImpl.send(msg, sendCallback);
        } else {
            this.defaultMQProducerImpl.send(msg, mq, sendCallback);
        }
        return null;
    }
}
public SendResult send(Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    // 调用重载 发送超时时间 默认 3秒
    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);
}

4.1  消息验证

消息发送之前,首先确保生产者处于运行状态,然后验证消息是否符合相应的规范,具体的规范要求是主题名称、 消息体不能为空、 消息长度不能等于0且默认不能超过允许发送消息的最大长度4M (maxMessageSize=l024 * 1024 * 4 )。

Validators 类里的 checkMessage 用于验证消息是否合法:

// msg 当前要发送的消息
// defaultMQProducer 生产者
public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer) throws MQClientException {
    // 检查消息对象是否为空
    if (null == msg) {
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message is null");
    }
    
    // 检查消息的主题是否合法 主题是否为空  是否大于127个字符 
    Validators.checkTopic(msg.getTopic());
    // 检查消息的主题是否被禁止发送  RocketMQ 内置了一些 TOPIC 不能跟人家内置的一样 比如RMQ_SYS_TRANS_HALF_TOPIC 都在TopicValidator类里定义着
    Validators.isNotAllowedSendTopic(msg.getTopic());

    // 检查消息体是否为空
    if (null == msg.getBody()) {
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body is null");
    }

    // 检查消息体长度是否为零
    if (0 == msg.getBody().length) {
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body length is zero");
    }

    // 检查消息体大小是否超过最大限制 默认不能超过 4M
    if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) {
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL,
            "the message body size over max value, MAX: " + defaultMQProducer.getMaxMessageSize());
    }

    // 检查消息中的LMQ路径是否包含非法字符
    String lmqPath = msg.getUserProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH);
    if (StringUtils.contains(lmqPath, File.separator)) {
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL,
            "INNER_MULTI_DISPATCH " + lmqPath + " can not contains " + File.separator + " character");
    }
}

4.2  查找主题路由信息

消息发送之前,首先需要获取主题的路由信息,只有获取了这些信息我们才知道消息 要发送到具体的 Broker 节点。

// DefaultMQProducerImpl#tryToFindTopicPublishInfo
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    // 获取主题的信息
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    // 如果主题发布信息为空或状态不正常,则尝试更新
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        // 尝试将一个新的主题发布信息对象放入表中,以避免并发更新
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        // 请求名称服务器更新主题的路由信息
        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;
    }
}

tryToFindTopicPublishlnfo 是查找主题的路由信息的方法。 如果生产者中缓存了 topic 的路由信息,如果该路由信息中包含了消息队列,则直接返回该路由信息,如果没有缓存或没有包含消息队列, 则向NameServer查询该topic 的路由信息。 如果最终未找到路由信 息,则抛出异常: 无法找到主题相关路由信息异常。

先看一下 TopicPublishlnfo:

下面我们来一一介绍下 TopicPublishinfo 的属性:

orderTopic : 是否是顺序消息

List<MessageQueue> messageQueueList: 该主题队列的消息队列

sendWhichQueue : 每选择一次消息队列, 该值会自增 l,如果 Integer.MAX_VALUE, 则重置为0,用于选择消息队列

TopicRouteData topicRouteData topic 路由元数据:

private List<QueueData> queueDatas; topic 队列元数据
private List<BrokerData> brokerDatas; topic 分布的 broker 元数据
private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable; broker 上过滤服务器地址列表

第一次发送消息时,本地没有缓存topic 的路由信息,查询NameServer尝试获取,如果路由信息未找到,再次尝试用默认主题DefaultMQProducerlmpl#createTopicKey 去查询,如果BrokerConfig#autoCreateTopicEnable 为 true 时, NameServer 将返回路由信息,如果autoCreateTopicEnable 为 false 将抛出无法找到 topic 路由异常。代码 MQClientlnstance#updateTopicRoutelnfoFromN ameServer 这个方法的功能是消息生产者更新和维护路由缓存,具体代码如下。

// MQClientlnstance#updateTopicRouteInfoFromNameServer
TopicRouteData topicRouteData;
// 根据是否是默认主题和是否有默认消息生产者,获取主题路由信息
if (isDefault && defaultMQProducer != null) {
    topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(clientConfig.getMqClientApiTimeout());
    // 调整队列数量为生产者配置的默认值
    if (topicRouteData != null) {
        for (QueueData data : topicRouteData.getQueueDatas()) {
            int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
            data.setReadQueueNums(queueNums);
            data.setWriteQueueNums(queueNums);
        }
    }
} else {
    topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, clientConfig.getMqClientApiTimeout());
}

(1)首先如果 isD巳fault 为 true,则使用默认主题去查询,如果查询到路由信息,则替 换路由信息中读写队列个数为消息生产者默认的队列个数(defaultTopicQueueNums);如果 isDefault 为 false ,则使用参数 topic 去查询;如果未查询到路由信息,则返回 false,表示 路由信息未变化。

// MQClientlnstance#updateTopicRouteInfoFromNameServer
TopicRouteData old = this.topicRouteTable.get(topic);
boolean changed = topicRouteData.topicRouteDataChanged(old);
// 检查路由信息是否改变,如果没有改变,检查是否需要更新
if (!changed) {
    changed = this.isNeedUpdateTopicRouteInfo(topic);
} else {
    log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
}

(2)如果路由信息找到,与本地缓存中的路由信息进行对比,判断路由信息是否发 生了改变, 如果未发生变化,则直接返回 false。

(3)更新 MQClientlnstance Broker 地址缓存表

// MQClientlnstance#updateTopicRouteInfoFromNameServer
TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
publishInfo.setHaveTopicRouterInfo(true);
for (Entry<String, MQProducerInner> entry : this.producerTable.entrySet()) {
    MQProducerInner impl = entry.getValue();
    if (impl != null) {
        impl.updateTopicPublishInfo(topic, publishInfo);
    }
}

(4)根据topicRouteData 中的 List<QueueData>转换成 topicPublicInfo 的 List<MessageQueue> 列表。 其具体实现在 topicRouteData2TopicPublishInfo, 然后会更新该 MQClientInstance所管辖的所有消息发送关于topic 的路由信息。

我这里画个图捋一下获取或者更新本地路由信息的执行过程:

4.3  选择消息队列

根据路由信息选择消息队列,返回的消息队列按照broker、序号排序。 举例说明,如果topicA 在 broker-a,broker-b 上分别创建了 4 个队列, 那么返回的消息队列:

[
{"broker-Name":"broker-a", "queueld":0},{"broker-Name":"broker-a", "queueld":1},{"broker-Name":"broker-a", "queueld":2},{"broker-Name":"broker-a", "queueld":3},
{"broker-Name":"broker-b", "queueld":0},{"broker-Name":"broker-b", "queueld":1},{"broker-Name":"broker-b", "queueld":2},{"broker-Name":"broker-b", "queueld":3}
]

那 RocketMQ 如何选择消息队列呢?

首先消息发送端采用重试机制,由 retryTimesWhenSendFailed 指定同步方式重试次数,异步重试机制在收到消息发送结构后执行回调之前进行重试。 由 retryTimesWhenSend-AsyncFailed 指定,接下来就是循环执行、选择消息队列、发送消息,发送成功则返回,收到异常则重试。选择消息队列有两种方式。

(1)sendLatencyFaultEnable = false ,默认不启用 Broker 故障延迟机制。

(2)sendLatencyFaultEnable = true ,启用 Broker 故障延迟机制。

4.3.1  默认机制

sendLatencyFaultEnable = false ,方法执行到 MQFaultStrategy#selectOneMessageQueue:

// MQFaultStrategy#selectOneMessageQueue
// tpInfo tryToFindTopicPublishInfo获取的路由信息
// lastBrokerName 就是上一次选择的执行发送消息失败的Broker名称 启用 Broker 故障延迟机制用到
// resetIndex 第一次发送为false 重试发送为true
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName, final boolean resetIndex) {
    // 获取当前线程的 Broker 过滤器
    BrokerFilter brokerFilter = threadBrokerFilter.get();
    // 重置当前线程的 Broker 过滤器
    brokerFilter.setLastBrokerName(lastBrokerName);
    // 是否开启Broker 故障延迟机制 默认关闭
    if (this.sendLatencyFaultEnable) {
        ...
        return;
    }
    // 如果未启用Broker 故障延迟机制,则直接使用Broker过滤器选择消息队列
    MessageQueue mq = tpInfo.selectOneMessageQueue(brokerFilter);
    if (mq != null) {
        return mq;
    }
    // 如果选择失败,退而求其次,选择任意一个消息队列
    return tpInfo.selectOneMessageQueue();
}

继续走到 TopicPublishInfo 的 selectOneMessageQueue 方法:

// TopicPublishInfo#selectOneMessageQueue 方法:
public MessageQueue selectOneMessageQueue(QueueFilter ...filter) {
    return selectOneMessageQueue(this.messageQueueList, this.sendWhichQueue, filter);
}
private MessageQueue selectOneMessageQueue(List<MessageQueue> messageQueueList, ThreadLocalIndex sendQueue, QueueFilter ...filter) {
    if (messageQueueList == null || messageQueueList.isEmpty()) {
        return null;
    }
    // 如果上次有执行失败的 broker 这里会过滤掉上次那个失败的
    if (filter != null && filter.length != 0) {
        for (int i = 0; i < messageQueueList.size(); i++) {
            int index = Math.abs(sendQueue.incrementAndGet() % messageQueueList.size());
            MessageQueue mq = messageQueueList.get(index);
            boolean filterResult = true;
            for (QueueFilter f: filter) {
                Preconditions.checkNotNull(f);
                filterResult &= f.filter(mq);
            }
            if (filterResult) {
                return mq;
            }
        }
        return null;
    }
    // 上次没有失败的话 取模选择一个消息队列
    int index = Math.abs(sendQueue.incrementAndGet() % messageQueueList.size());
    return messageQueueList.get(index);
}

首先在一次消息发送过程中,可能会多次执行选择消息队列这个方法, lastBrokerName 就是上一次选择的执行发送消息失败的Broker。 第一次执行消息队列选择时,lastBrokerName 为 null,此时直接用 sendWhichQueue 自增再获取值, 与当前路由表中消息队列个数取模, 返回该位置的消息队列,如果消息发送再失败的话, 下次进行消息队列选择时规避上次MesageQueue所在的Broker,否则还是很有可能再次失败。

该算法在一次消息发送过程中能成功规避故障的Broker,但如果Broker宕机,由于路由算法中的消息队列是按Broker排序的,如果上一次根据路由算法选择的是宕机的Broker的第一个队列,那么随后的下次选择的是宕机Broker的第二个队列,消息发送很有可能会失败,再次引发重试,带来不必要的性能损耗,那么有什么方法在一次消息发送失败后,暂时将该Broker排除在消息队列选择范围外呢?或许有朋友会问, Broker不可用后,路由信息中为什么还会包含该Brok町的路由信息呢?其实这不难解释:首先, NameServer检测Broker 是否可用是有延迟的,最短为一次心跳检测间隔(5s); 其次, NameServer不会检测到Broker宕机后马上推送消息给消息生产者,而是消息生产者每隔30s更新一次路由信息,所以消息生产者最快感知Broker最新的路由信息也需要30s。 如果能引人一种机制,在Broker 宕机期间,如果一次消息发送失败后,可以将该Broker暂时排除在消息队列的选择范围中。

另外:BrokerFilter 可以理解为名称过滤器,内部存放了上次异常的 Broker 名称。然后在选择队列的时候,排除掉这个 Broker。

4.3.1  Broker 故障延迟机制

还是上边的 MQFaultStrategy#selectOneMessageQueue:

// MQFaultStrategy#selectOneMessageQueue
// tpInfo tryToFindTopicPublishInfo获取的路由信息
// lastBrokerName 就是上一次选择的执行发送消息失败的Broker名称 启用 Broker 故障延迟机制用到
// resetIndex 第一次发送为false 重试发送为true
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName, final boolean resetIndex) {
    // 获取当前线程的 Broker 过滤器
    BrokerFilter brokerFilter = threadBrokerFilter.get();
    // 重置当前线程的 Broker 过滤器
    brokerFilter.setLastBrokerName(lastBrokerName);
    // 是否开启Broker 故障延迟机制 默认关闭
    if (this.sendLatencyFaultEnable) {
        // 重置索引 重置 TopicPublishInfo内部的sendWhichQueue队列的索引
        if (resetIndex) {
            tpInfo.resetIndex();
        }
        // 尝试选择一个满足可用性过滤器和Broker过滤器的消息队列
        MessageQueue mq = tpInfo.selectOneMessageQueue(availableFilter, brokerFilter);
        if (mq != null) {
            return mq;
        }
        // 如果上述选择失败,尝试选择一个满足可访问性过滤器和Broker过滤器的消息队列
        mq = tpInfo.selectOneMessageQueue(reachableFilter, brokerFilter);
        if (mq != null) {
            return mq;
        }
        // 如果都选择失败,退而求其次,选择任意一个消息队列
        return tpInfo.selectOneMessageQueue();
    }
    // 如果未启用Broker 故障延迟机制,则直接使用Broker过滤器选择消息队列
    MessageQueue mq = tpInfo.selectOneMessageQueue(brokerFilter);
    if (mq != null) {
        return mq;
    }
    // 如果选择失败,退而求其次,选择任意一个消息队列
    return tpInfo.selectOneMessageQueue();
}
// 取模选择一个消息队列
public MessageQueue selectOneMessageQueue() {
    int index = this.sendWhichQueue.incrementAndGet();
    int pos = index % this.messageQueueList.size();
    return this.messageQueueList.get(pos);
}

当开启了故障延迟机制,首先经过 availableFilter 过滤选择,如果没找到符合的,再经过 reachableFilter 过滤选择,如果还没找到符合的则调用兜底空参的 selectOneMessageQueue 方法,通过取模选择一个消息队列。

那我们看下这两个过滤器,都是位于 MQFaultStrategy 类内:

private QueueFilter availableFilter = new QueueFilter() {
    @Override public boolean filter(MessageQueue mq) {
        return latencyFaultTolerance.isAvailable(mq.getBrokerName());
    }
};
public boolean isAvailable(final String name) {
    final FaultItem faultItem = this.faultItemTable.get(name);
    if (faultItem != null) {
        return faultItem.isAvailable();
    }
    return true;
}
// FaultItem#isAvailable
public boolean isAvailable() {
    return reachableFlag && System.currentTimeMillis() >= startTimestamp;
}
private QueueFilter reachableFilter = new QueueFilter() {
    @Override public boolean filter(MessageQueue mq) {
        return latencyFaultTolerance.isReachable(mq.getBrokerName());
    }
};
// FaultItem#isReachable
public boolean isReachable() {
    return reachableFlag;
}

可以看到两个过滤器都跟 MQFaultStrategy 内部的 LatencyFaultTolerance息息相关,那我们看下核心类之间的关系:

LatencyFaultTolerance :延迟机制接口规范

(1)void updateFaultltem(final T name , final long currentLatency, final long notAvailableDuration) 更新失败条目 name: brokerName,currentLatency : 消息发送故障延迟时间,notAvailableDuration: 不可用持续时长, 在这个时间内, Broker将被规避

(2)boolean isAvailable(final T name) name: broker 名称 判断Broker 是否可用

(3)void remove(final T name) 移除Fault 条目,意味着 Broker重新参与路由计算

(4)T pickOneAtLeast() 尝试从规避的Broker 中选择一个可用的 Broker,如果没有找到,将返回 null

Faultltem : 失败条目 (规避规则条目)

(1)final String name 条目唯一键,这里为 brokerName。

(2)private volatile long currentLatency 本次消息发送延迟

(3)private volatile long startTimestamp 故障规避开始时间

MQFaultStrategy:消息失败策略, 延迟实现的门面类

(1)private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};

(2)private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};

latencyMax,根据 currentLatency 本次消息发送延迟,从 latencyMax 尾部向前找到第一个比currentLatency 小的索引 index,如果没有找到,返回 0。 然后根据这个索引从 notAvailableDuration 数组中取出对应的时间 ,在这个时长内, Broker 将设置为不可用。

下面从源码的角度分析updateFaultltem、 isAvailable 方法的实现原理,如下所示:

// DefaultMQProducerImpl#sendDefaultImpl
long beginTimestampFirst = System.currentTimeMillis();
long beginTimestampPrev = beginTimestampFirst;
long endTimestamp = beginTimestampFirst;
SendResult sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false, true);

上述代码如果发送过程中抛出了异常,调用DefaultMQProducerlmpl#updateFaultItem,该方法则直接调用MQFaultStrategy#updateFaultltem 方法,关注一下各个参数的含义。

第一个参数: broker名称。
第二个参数:本次消息发送延迟时间currentLatency。
第三个参数: isolation,是否隔离,该参数的含义如果为 true,则使用默认时长 30s来
第四个参数: reachable,是否可达,还未请求远程或者远程结果里不是broker异常则为 true 否则为false

计算Broker 故障规避时长,如果为 false, 则使用本次消息发送延迟时间来计算Broker故障规避时长。

public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation, final boolean reachable) {
    if (this.sendLatencyFaultEnable) {
        long duration = computeNotAvailableDuration(isolation ? 10000 : currentLatency);
        this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration, reachable);
    }
}
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;
}

如果isolation 为 true,则使用 10s 作为 computeNotAvailableDuration 方法的参数;如果isolation 为 false,则使用本次消息发送时延作为 computeNotAvailableDuration 方法的参数,那computeNotAvailableDuration 的作用是计算因本次消息发送故障需要将 Broker规避的时长,也就是接下来多久的时间内该Broker将不参与消息发送队列负载。 具体算法:从latencyMax 数组尾部开始寻找,找到第一个比 currentLatency小的下标, 然后从notAvailableDuration 数组中获取需要规避的时长,该方法最终调用 LatencyFaultTolerance的updateFaultltem。

// LatencyFaultToleranceImpl#updateFaultItem
public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration,
                            final boolean reachable) {
    FaultItem old = this.faultItemTable.get(name);
    if (null == old) {
        final FaultItem faultItem = new FaultItem(name);
        faultItem.setCurrentLatency(currentLatency);
        faultItem.updateNotAvailableDuration(notAvailableDuration);
        faultItem.setReachable(reachable);
        old = this.faultItemTable.putIfAbsent(name, faultItem);
    }
    if (null != old) {
        old.setCurrentLatency(currentLatency);
        old.updateNotAvailableDuration(notAvailableDuration);
        old.setReachable(reachable);
    }
    if (!reachable) {
        log.info(name + " is unreachable, it will not be used until it's reachable");
    }
}
// FaultItem#updateNotAvailableDuration 当前系统时间加上需要规避的时长
public void updateNotAvailableDuration(long notAvailableDuration) {
    if (notAvailableDuration > 0 && System.currentTimeMillis() + notAvailableDuration > this.startTimestamp) {
        this.startTimestamp = System.currentTimeMillis() + notAvailableDuration;
        log.info(name + " will be isolated for " + notAvailableDuration + " ms.");
    }
}

根据broker 名称从缓存表中获取Faultitem,如果找到则更新Faultltem,否则创建Faultltem。 这里有两个关键点。

(1)currentLatency、 startTimeStamp 被 volatile 修饰

(2)startTimeStamp 为当前系统时间加上需要规避的时长。 startTimeStamp 是判断broker 当前是否可用的直接一句,请看Faultltem#isAvailable 方法

public boolean isAvailable() {
    // 当前时间大于规避时长了 说明可用了
    return reachableFlag && System.currentTimeMillis() >= startTimestamp;
}

4.4  消息发送

消息发送 API 核心人口 : DefaultMQProducerimpl#sendKerne!Impl 。

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

消息发送参数详解:

(1)Message msg: 待发送消息

(2)MessageQueue mq: 消息将发送到该消息队列上

(3)CommunicationModecommunicationMode: 消息发送模式,SYNC、ASYNC、ONEWAY 。

(4)SendCallback sendCallback: 异步消息 回调函数

(5)TopicPublishinfo topicPublishlnfo: 主题路由信息

(6)long timeout: 消息发送超时时间

那我们看下发送的过程,都需要经过哪些流程:

(1)根据 MessageQueue获取 Broker的网络地址。 如果 MQC!ientlnstance的 brokerAddrTable禾缓存该 Broker 的信息,则从 NameServer 主动更新一 下 topic 的路由信息。 如果路由更新后还是找不到 Broker信息,则抛出 MQClientExc叩tio口,提示 Broker不存在 。

String brokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(mq);
String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(brokerName);
if (null == brokerAddr) {
    tryToFindTopicPublishInfo(mq.getTopic());
    brokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(mq);
    brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(brokerName);
}

(2)为消息分配全局唯一ID ,如果消息体默认超过 4K(compressMsgBodyOverHowmuch), 会对消息体采用 zip压缩,并设置消息的系统标记为 MessageSysFlag.COMPRESSED_FLAG。 如果是事务 Prepared消息,则设置消息的系统标记为 MessageSysFlag.TRANSACTION_ PREPARED TYPE。

//for MessageBatch,ID has been set in the generating process
if (!(msg instanceof MessageBatch)) {
    MessageClientIDSetter.setUniqID(msg);
}
int sysFlag = 0;
boolean msgBodyCompressed = false;
if (this.tryToCompressMessage(msg)) {
    sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
    sysFlag |= compressType.getCompressionFlag();
    msgBodyCompressed = true;
}
final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (Boolean.parseBoolean(tranMsg)) {
   

另外消息 ID的生成可以参考我这篇哈:【RocketMQ】【MsgId】MsgId 谁生成的?什么时候生成的?怎么生成的?一定唯一么?

(3)如果注册了消息发送钩子函数, 则执行消息发送之前的增强逻辑。 通过 DefaultMQProducerlmpl#registerSendMessageHook 注册钩子处理类,并且可以注册多个 。

if (this.hasSendMessageHook()) {
    context = new SendMessageContext();
    context.setProducer(this);
    context.setProducerGroup(this.defaultMQProducer.getProducerGroup());
    context.setCommunicationMode(communicationMode);
    context.setBornHost(this.defaultMQProducer.getClientIP());
    context.setBrokerAddr(brokerAddr);
    context.setMessage(msg);
    context.setMq(mq);
    context.setNamespace(this.defaultMQProducer.getNamespace());
    String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
    if (isTrans != null && isTrans.equals("true")) {
        context.setMsgType(MessageType.Trans_Msg_Half);
    }
    if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) {
        context.setMsgType(MessageType.Delay_Msg);
    }
    this.executeSendMessageHookBefore(context);
}

简单看一下钩子处理类接口:

public interface SendMessageHook {
    String hookName();

    void sendMessageBefore(final SendMessageContext context);

    void sendMessageAfter(final SendMessageContext context);
}

(4)构建消息发送请求包 。 主要包含如下重要信息:生产者组、主题名称、默认 创建主题 Key、该 主题在单个 Broker 默认队列数 、队列 ID (队列序号)、消息系统标记 ( MessageSysFlag)、 消息发送时间、消息标记(RocketMQ对消息中的 flag不做任何处理, 供应用程序使用)、 消息扩展属性、消息重试次数、是否是批量消息等。

SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
requestHeader.setTopic(msg.getTopic());
requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
requestHeader.setQueueId(mq.getQueueId());
requestHeader.setSysFlag(sysFlag);
requestHeader.setBornTimestamp(System.currentTimeMillis());
requestHeader.setFlag(msg.getFlag());
requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
requestHeader.setReconsumeTimes(0);
requestHeader.setUnitMode(this.isUnitMode());
requestHeader.setBatch(msg instanceof MessageBatch);
requestHeader.setBname(brokerName);
if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
    String reconsumeTimes = MessageAccessor.getReconsumeTime(msg);
    if (reconsumeTimes != null) {
        requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes));
        MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME);
    }
    String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(msg);
    if (maxReconsumeTimes != null) {
        requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes));
        MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES);
    }
}

(5)根据消息发送方式,同步、异步、单向方式进行网络传输 。

SendResult sendResult = null;
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,
            brokerName,
            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,
            brokerName,
            msg,
            requestHeader,
            timeout - costTimeSync,
            communicationMode,
            context,
            this);
        break;
    default:
        assert false;
        break;
}

(6)如果注册了消息发送钩子函数,执行 after逻辑。 注意,就算消息发送过程中发生 RemotingException、 MQBrokerException、 InterruptedException时该方法也会执行。

if (this.hasSendMessageHook()) {
    context.setSendResult(sendResult);
    this.executeSendMessageHookAfter(context);
}

5  小结

好啦,本节主要看了消息发送的整个轮廓,还有好多细节还没深入的去看哈,至少要了解一下生产者启动以及消息发送涉及到的一些主要类,理解MQClientlnstance、消息生产者之间的关系以及消息队列负载机制,消息生产者在发送消息时,如果本地路由表中未缓存topic的路由信息,向Name Server 发送获取路由信息请求,更新本地路由信息表,并且消息生产者每隔30s从Name Server 更新路由表。消息发送高可用主要通过两个手段: 重试与Broker规避。 Brok巳r规避就是在一次消息 发送过程中发现错误,在某一时间段内,消息生产者不会选择该Broker(消息服务器)上的 消息队列,提高发送消息的成功率。下节我们细看一下 Broker 延迟机制,以及核心类里的属性由来,更好的理解机制原理,下节继续。

posted @ 2024-10-28 21:03  酷酷-  阅读(314)  评论(0编辑  收藏  举报