RocketMQ源码详解 | Producer篇 · 其一:Start,然后 Send 一条消息

概述


DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");

producer.start();

try {
  /*
   * Create a message instance, specifying topic, tag and message body.
   */
  Message msg = new Message("TopicTest" /* Topic */,
                            "TagA" 			/* Tag */,
                            ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
                           );
  
  /*
   * Call send message to deliver message to one of brokers.
   */
  SendResult sendResult = producer.send(msg);
  System.out.printf("%s%n", sendResult);
} catch (Exception e) {
  e.printStackTrace();
}

/*
 * Shut down once the producer instance is not longer in use.
 */
producer.shutdown();



Start

DefaultMQProducer#start

首先进入 start 方法,可以看出主要的功能实现在于 defaultMQProducerImpl.start(),先忽略细枝末节,接着进去看看

public void start() throws MQClientException {
  this.setProducerGroup(withNamespace(this.producerGroup));
  this.defaultMQProducerImpl.start();
  if (null != traceDispatcher) {
    try {
      traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
    } catch (MQClientException e) {
      log.warn("trace dispatcher start failed ", e);
    }
  }
}

DefaultMQProducerImpl#start()

然后,我们可以看到会根据当前生产者的状态来进行不同的行为

记得在设计模式里,这叫做"状态模式"

具体的状态有:

  • CREATE_JUST
  • RUNNING
  • START_FAILED
  • SHUTDOWN_ALREADY

在进入 start 后状态会变成 START_FAILED ,完成后变成 RUNNING 状态

public void start() throws MQClientException {
  this.start(true);
}

public void start(final boolean startFactory) throws MQClientException {
  switch (this.serviceState) {
    case CREATE_JUST:
      this.serviceState = ServiceState.START_FAILED;
      // -- 跳过 --
      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;
  }
}

进入 CREATE_JUST 后开始对元信息进行检查与注册

this.checkConfig();

// 如果自定义了生产者组,则修改PID
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
  this.defaultMQProducer.changeInstanceNameToPID();
}

// 从MQ工厂获取实例
// MQ工厂保证ClientID唯一
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);

// 注册生产者组
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);
}

this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());

if (startFactory) {
  mQClientFactory.start();
}

log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(), 
         this.defaultMQProducer.isSendMessageWithVIPChannel());

然后具体看 mQClientFactory.start() 方法


MQClientInstance#start

MQClientInstance 是由一个 JAVA 程序所共用的,其可以从 ClientId 的生成方法看出

public String buildMQClientId() {
  StringBuilder sb = new StringBuilder();
  sb.append(this.getClientIP());

  sb.append("@");
  sb.append(this.getInstanceName());
  if (!UtilAll.isBlank(this.unitName)) {
    sb.append("@");
    sb.append(this.unitName);
  }

  return sb.toString();
}

由以上代码可以得知,一台机器上的一个 JVM 进程只拥有一个实例,所以以下的初始化方法也是全局的


首先对当前对象加锁来避免多线程带来的问题,然后又进行了一次状态判断来保证状态正确。然后就启动了一堆服务。

synchronized (this) {
  switch (this.serviceState) {
    case CREATE_JUST:
      this.serviceState = ServiceState.START_FAILED;
      // NameSrv 地址为空时,尝试通过设定的地址使用HTTP获取NameSrv地址
      if (null == this.clientConfig.getNamesrvAddr()) {
        this.mQClientAPIImpl.fetchNameServerAddr();
      }
      // 开启 Netty 的请求响应的 Channel
      this.mQClientAPIImpl.start();
      // 开启调度任务
      this.startScheduledTask();
      // 开启拉取服务
      this.pullMessageService.start();
      // 开启再均衡服务
      this.rebalanceService.start();
      // 开启push服务
      this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
      log.info("the client factory [{}] start OK", this.clientId);
      this.serviceState = ServiceState.RUNNING;
      break;
    case START_FAILED:
      throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
    default:
      break;
  }
}

其中的 pullMessageServicerebalanceService 等服务都是继承于 ServiceThread 抽象类,这个类被用于在多线程的情况下保证服务启动的正确性。

public void start() {
  log.info("Try to start service thread:{} started:{} lastThread:{}", getServiceName(), started.get(), thread);
  // 只会被启动一次
  if (!started.compareAndSet(false, true)) {
    return;
  }
  stopped = false;
  // 启动子类的实现线程,从子类获取服务名
  this.thread = new Thread(this, getServiceName());
  this.thread.setDaemon(isDaemon);
  this.thread.start();
}

其中 RebalanceServicePullMessageService 我们在其他章节再具体分析。


此外,其中的 startScheduledTask() 又开启了一些定时运行的任务

// 从远程服务器不断更新 NameServer 地址
if (null == this.clientConfig.getNamesrvAddr()) {
  this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
      try {
        MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
      } catch (Exception e) {
        log.error("ScheduledTask fetchNameServerAddr exception", e);
      }
    }
  }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
}

// 定时从NameServer更新Topic的路由信息
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

  @Override
  public void run() {
    try {
      MQClientInstance.this.updateTopicRouteInfoFromNameServer();
    } catch (Exception e) {
      log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);
    }
  }
}, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);

// 定期清除离线的Broker地址,同时发送心跳
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

  @Override
  public void run() {
    try {
      MQClientInstance.this.cleanOfflineBroker();
      MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
    } catch (Exception e) {
      log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
    }
  }
}, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);

// 持久化所有的拥有的消费者偏移量
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

  @Override
  public void run() {
    try {
      MQClientInstance.this.persistAllConsumerOffset();
    } catch (Exception e) {
      log.error("ScheduledTask persistAllConsumerOffset exception", e);
    }
  }
}, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

// 动态对所有消费者的线程池容量进行调整
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

  @Override
  public void run() {
    try {
      MQClientInstance.this.adjustThreadPool();
    } catch (Exception e) {
      log.error("ScheduledTask adjustThreadPool exception", e);
    }
  }
}, 1, 1, TimeUnit.MINUTES);

从这里可以看出,消费者不仅是在内存保存了偏移量,还会定期持久化以保证不丢失。

运行这些任务的,是一个 SingleThreadScheduledExecutor ,这是一个由一个线程去执行需要被定时执行的任务的线程池。


在完成以上任务后,回到我们的 DefaultMQProducerImpl.start() 方法看剩下的两段

// 为所有Broker发送心跳
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();

// 开启所有的调度任务
this.startScheduledTask();

首先来看第一个方法

public void sendHeartbeatToAllBrokerWithLock() {
  if (this.lockHeartbeat.tryLock()) {
    try {
      // 发送心跳,但第一次的时候是空的,所以不用考虑
      this.sendHeartbeatToAllBroker();
      // 上传过滤器Class,消费者相关
      this.uploadFilterClassSource();
    } catch (final Exception e) {
      log.error("sendHeartbeatToAllBroker exception", e);
    } finally {
      this.lockHeartbeat.unlock();
    }
  } else {
    log.warn("lock heartBeat, but failed. [{}]", this.clientId);
  }
}

在发送心跳的时候,由于这时候还没有从 NameServer 获取 Broker 地址,所以不会发送,而上传过滤器 Class 我们留到消费者的章节再讲。


第二个方法比较简单,开启一个调度任务来处理所有的 Request 状态,对异步的请求根据状态处理回调函数。

private void startScheduledTask() {
  // 原子增加生产者数量
  if (RequestFutureTable.getProducerNum().incrementAndGet() == 1) {
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        try {
          // 处理异步回调请求,扫描所有Request状态
          RequestFutureTable.scanExpiredRequest();
        } catch (Throwable e) {
          log.error("scan RequestFutureTable exception", e);
        }
      }
    }, 1000 * 3, 1000, TimeUnit.MILLISECONDS);
  }
}

需要注意的是,这个异步请求指的并不是在 send 中的异步回调机制,而是在 rockmq-4.7.0 后加入的 Request-Reply 特性,用来支持 RPC 调用。

实现的方法是:

  1. Producer 投递消息到 Broker,然后阻塞直到收到 返回结果
  2. Broker 收到后像正常消息一样 ack,Consumer 通过 pull\push 从 Broker 中获取
  3. Consumer 获取后进行处理,然后响应 返回结果
  4. Broker 收到 返回结果 后将其发回对应的 Producer

核心的方法与类有:

DefaultMQProducerImpl#request()
RequestResponseFuture#waitResponseMessage()#putResponseMessage()
MessageUtil#createReplyMessage()
ReplyMessageProcessor#processReplyMessageRequest()
ClientRemotingProcessor#receiveReplyMessage()

自此,Start 方法就完成了。


Start 总体流程

  1. 设置生产者组

  2. 检查当前状态,只允许为 CreateJust

  3. 从 MQClientManage 获取 MQClient 工厂

    3.1 已经被创建则直接返回

  4. 注册生产者组

  5. 启动 MQClient 工厂

    5.1 NameSrv 地址为空时,尝试通过设定的地址使用HTTP获取NameSrv地址

    5.2 开启 Netty 的请求响应的 Channel

    5.3 开启调度任务

     5.3.1 从远程服务器不断更新 NameServer 地址

     5.3.2 定时从NameServer更新Topic的路由信息

     5.3.3 定期清除离线的Broker地址,同时发送心跳

     5.3.4 持久化所有的拥有的消费者偏移量

     5.3.5 动态对所有消费者的线程池容量进行调整

    5.4 开启拉取服务

    5.5 开启再均衡服务

    5.6 开启push服务

  6. 启动 trace dispatcher 服务




Send

进入 send 后会进行完整检查,且默认工作模式为 Sync,send 的主要工作的方法如下


DefaultMQProducerImpl#sendDefaultImpl

这个方法很长,我们一段段来看

// 确保是 Start 状态
this.makeSureStateOK();

Validators.checkMessage(msg, this.defaultMQProducer);

final long invokeID = random.nextLong();
long beginTimestampFirst = System.currentTimeMillis();
long beginTimestampPrev = beginTimestampFirst;
long endTimestamp = beginTimestampFirst;

// 得到Topic信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());

// 返回为Null的结果只有Topic不存在且自动创建Topic没有打开
if (topicPublishInfo != null && topicPublishInfo.ok()) {  /* pass */  }

// 检查下是不是NameSev地址填错了
validateNameServerSetting();

throw new MQClientException("No route info of this topic: " + msg.getTopic() + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO),
                            null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION);

首先是确保状态正确,然后记录启动时间作为度量和超时检查,然后调用 tryToFindTopicPublishInfo 获得具体的 Topic 信息以发送消息,如果找不到则抛异常。

然后进入 tryToFindTopicPublishInfo 看看具体实现

// 寻找消息应该被发到哪
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
  TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);

  // 本地没有、或未就绪,从NameServ请求
  if (null == topicPublishInfo || !topicPublishInfo.ok()) {
    this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
    // 没有信息则从NameServ拉取
    this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
    topicPublishInfo = this.topicPublishInfoTable.get(topic);
  }

  if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
    return topicPublishInfo;
  } else {
    // 拉取不到?说明我们要发送的目标Topic不存在
    // 那就打开isDefault开关,向默认Topic发送
    this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
    topicPublishInfo = this.topicPublishInfoTable.get(topic);
    return topicPublishInfo;
  }
}

这个方法可以看到,在本地会有一个本地 Topic 表,没有会尝试去 NameServer 拉取。

而拉取分为两阶段,第一次拉取会去找对应的 Topic ,失败则第二次会去找 Default Topic。为什么会这样做呢?我们都知道自动创建 Topic 只会在 Broker 打开自动创建 Topic 的开关才有效,而具体的实现方法需要我们再往下看


MQClientInstance#updateTopicRouteInfoFromNameServer

又进入了 MQClientInstance ,我们刚刚已经了解到它会由 MQClientManager 创建一个全局的实例,而它内部有几个重要的 Map。

private final ConcurrentMap<String/* group */, MQProducerInner> producerTable;
private final ConcurrentMap<String/* group */, MQConsumerInner> consumerTable;
private final ConcurrentMap<String/* group */, MQAdminExtInner> adminExtTable;
private final ConcurrentMap<String/* Topic */, TopicRouteData> topicRouteTable;
private final ConcurrentMap<String/* Broker Name */, HashMap<Long/* brokerId */, String/* address */>> brokerAddrTable;
private final ConcurrentMap<String/* Broker Name */, HashMap<String/* address */, Integer>> brokerVersionTable;

从变量名和和注释我们不难理解它们是做什么的。我们可以发现, Client 的元信息是由所有的消费者和生产者共享。

回归正题,接着看更新路由信息的方法

TopicRouteData topicRouteData;
// 使用默认 Topic ,因为目标Topic不存在,所以需要新建
if (isDefault && defaultMQProducer != null) {
  // 获取默认Topic路由信息
  topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
                                                                               1000 * 3);
  // 获得了信息后,接下来都是新的Topic都是继承于默认Topic的信息

  if (topicRouteData != null) {
    // 修正读写Queue数量
    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, 1000 * 3);
}

这里分为直接从 NameServer 获取和获取 default topic 后对其元信息进行继承。

关于 ReadQueue 和 WriteQueue

可读 Queue 代表消费者可以读取的 Queue,可写 Queue 代表生产者可以写入的 Queue。

将它们进行分离的主要是为了方便的动态调整 Queue 的大小

其中 getTopicRouteInfoFromNameServer 主要是 Netty 使用 RPC 获取 Topic,具体内容在其他章看看


在获取了路由信息后,就开始对其进行检查更改的项目

if (topicRouteData != null) {
  // 检查路由信息是否发生改变
  TopicRouteData old = this.topicRouteTable.get(topic);
  boolean changed = topicRouteDataIsChange(old, topicRouteData);
  if (!changed) {
    changed = this.isNeedUpdateTopicRouteInfo(topic);
  } else {
    log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
  }

  // 发生改变则进行更改
  if (changed) {
    TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();

    // 更新路由地址
    for (BrokerData bd : topicRouteData.getBrokerDatas()) {
      this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
    }

    // Update Pub info
    {
      TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
      publishInfo.setHaveTopicRouterInfo(true);
      Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
      while (it.hasNext()) {
        Entry<String, MQProducerInner> entry = it.next();
        MQProducerInner impl = entry.getValue();
        if (impl != null) {
          // 在这,被新建的Topic放入了本地生产者表
          impl.updateTopicPublishInfo(topic, publishInfo);
        }
      }
    }

    // Update sub info
    {
      Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
      Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
      while (it.hasNext()) {
        Entry<String, MQConsumerInner> entry = it.next();
        MQConsumerInner impl = entry.getValue();
        if (impl != null) {
          impl.updateTopicSubscribeInfo(topic, subscribeInfo);
        }
      }
    }
    log.info("topicRouteTable.put. Topic = {}, TopicRouteData[{}]", topic, cloneTopicRouteData);
    // 重新放入
    this.topicRouteTable.put(topic, cloneTopicRouteData);
    return true;
  }
} else {
  log.warn("updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}. [{}]", topic, this.clientId);
}

如果是 default topic 的话,就会在这一步放入本地。

这是因为,在 Broker 中,如果开启了自动创建 Topic 的选项,便会创建一个和 default 同名(default 的默认名称为 TBW102)的 Topic,并以这个 Topic 来创建新的 Topic ,但若没有开启,则会因为找不到 TopicName 而返回错误。


DefaultMQProducerImpl#sendDefaultImpl

我们再回到发送方法来,在获取到要发送的 Topic 的元信息后,就可以开始发送了

boolean callTimeout = false;
MessageQueue mq = null;
Exception exception = null;
SendResult sendResult = null;

// 最多发送timesTotal次
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();
  // 选择一个Queue
  MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
  if (mqSelected != null) {
    /*   pass   */
  } else {
    break;
  }
}

在这里,Producer 会根据发送类型来选择发送次数,同步会选择默认的重试次数加一,而异步和oneway则只会尝试一次

在发送流程中,首先需要选择 Topic 中的 Queue


MQFaultStrategy#selectOneMessageQueue

从类名可以看出,这个类主要实现故障退避功能,同时使用轮询的方式来选择 Queue

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
  // 故障退避: 当要发送的Broker在上一次发送中延迟了较久的时间或发送失败,会进行一段时间的退避
  if (this.sendLatencyFaultEnable) {
    try {
      // 首先,增加线程本地的轮询计数
      int index = tpInfo.getSendWhichQueue().incrementAndGet();
      for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
        int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
        if (pos < 0)
          pos = 0;
        MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
        // 如果可用(不需要进行退避),则直接使用
        if (latencyFaultTolerance.isAvailable(mq.getBrokerName()))
          return mq;
      }

      // 在所有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().incrementAndGet() % writeQueueNums);
        }
        return mq;
      } else {
        // 如果可写 Queue 已经为零,说明已经不在了
        latencyFaultTolerance.remove(notBestBroker);
      }
    } catch (Exception e) {
      log.error("Error occurred when selecting message queue", e);
    }

    return tpInfo.selectOneMessageQueue();
  }

  // 如果没有开启开关,则选择一个不是上一次发送的Broker来发送
  return tpInfo.selectOneMessageQueue(lastBrokerName);
}

大多数内容直接看代码就能理解,所以我们主要看下 pickOneAtLeast 方法


LatencyFaultToleranceImpl#pickOneAtLeast

这个方法用于在所有 Broker 都需要"故障退避"的时候,选择一个可能最好的

public String pickOneAtLeast() {
  final Enumeration<FaultItem> elements = this.faultItemTable.elements();
  List<FaultItem> tmpList = new LinkedList<FaultItem>();
  while (elements.hasMoreElements()) {
    final FaultItem faultItem = elements.nextElement();
    tmpList.add(faultItem);
  }

  if (!tmpList.isEmpty()) {
    Collections.shuffle(tmpList);

    Collections.sort(tmpList);

    final int half = tmpList.size() / 2;
    if (half <= 0) {
      return tmpList.get(0).getName();
    } else {
      final int i = this.whichItemWorst.incrementAndGet() % half;
      return tmpList.get(i).getName();
    }
  }

  return null;
}

这个方法主要的策略是:

对当前的处于"故障退避"状态的 Broker 使用洗牌算法(这有啥意义...),然后进行排序,然后选择所有元素的前半段使用轮询策略

排序方案:是否可用 > 上次响应时间(短>长)(发生网络分区的时候会相同)> 未来可用时间点(近>远)


DefaultMQProducerImpl#sendDefaultImpl

选择完 Queue 后,就要进行实际的发送了

mq = mqSelected;
brokersSent[times] = mq.getBrokerName();
try {
  beginTimestampPrev = System.currentTimeMillis();
  if (times > 0) {
    // 在重新发送期间使用命名空间重置主题
    msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()));
  }

  // 消耗时间的度量
  long costTime = beginTimestampPrev - beginTimestampFirst;
  if (timeout < costTime) {
    callTimeout = true;
    break;
  }

  // 发送MSG
  sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);


  endTimestamp = System.currentTimeMillis();
  // 更新故障退避功能
  this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);

  // 根据发送方式选择返回结果
  switch (communicationMode) {
    case ASYNC:
      return null;
    case ONEWAY:
      return null;
    case SYNC:
      if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
        if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
          continue;
        }
      }
      return sendResult;
    default:
      break;
  }
} catch (XXX e) {
	/* 这里删除了一堆Expection catch, 它们都主要做了更新"故障退避", 然后抛出 */
}

可以看出,实际的发送方法是 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();
  String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
  if (null == brokerAddr) {
    // 不在内存中,尝试获取或创建
    tryToFindTopicPublishInfo(mq.getTopic());
    brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
  }

  SendMessageContext context = null;
  if (brokerAddr != null) {
    // 是否使用vip,broker有两个端口共同服务
    brokerAddr = MixAll.brokerVIPChannel(this.defaultMQProducer.isSendMessageWithVIPChannel(), brokerAddr);

    byte[] prevBody = msg.getBody();
    try {
      // 对于MessageBatch,生成过程中已经设置了ID
      if (!(msg instanceof MessageBatch)) {
        MessageClientIDSetter.setUniqID(msg);
      }

      // 将实例名设置为命名空间
      boolean topicWithNamespace = false;
      if (null != this.mQClientFactory.getClientConfig().getNamespace()) {
        msg.setInstanceId(this.mQClientFactory.getClientConfig().getNamespace());
        topicWithNamespace = true;
      }

      // 是否为压缩消息
      int sysFlag = 0;
      boolean msgBodyCompressed = false;
      if (this.tryToCompressMessage(msg)) {
        sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
        msgBodyCompressed = true;
      }

      final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
      if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
        sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
      }

      // 发送消息的校验钩子
      if (hasCheckForbiddenHook()) {
        CheckForbiddenContext checkForbiddenContext = new CheckForbiddenContext();
        checkForbiddenContext.setNameSrvAddr(this.defaultMQProducer.getNamesrvAddr());
        checkForbiddenContext.setGroup(this.defaultMQProducer.getProducerGroup());
        checkForbiddenContext.setCommunicationMode(communicationMode);
        checkForbiddenContext.setBrokerAddr(brokerAddr);
        checkForbiddenContext.setMessage(msg);
        checkForbiddenContext.setMq(mq);
        checkForbiddenContext.setUnitMode(this.isUnitMode());
        this.executeCheckForbiddenHook(checkForbiddenContext);
      }

      // 发送消息前的钩子
      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);
      }

      // 组装消息头
      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);
      // 发往重发Topic的消息
      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);
        }
      }

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

      // 发送消息后的钩子
      if (this.hasSendMessageHook()) {
        context.setSendResult(sendResult);
        this.executeSendMessageHookAfter(context);
      }

      return sendResult;
    } catch (RemotingException e) {
      if (this.hasSendMessageHook()) {
        context.setException(e);
        this.executeSendMessageHookAfter(context);
      }
      throw e;
    } catch (MQBrokerException e) {
      if (this.hasSendMessageHook()) {
        context.setException(e);
        this.executeSendMessageHookAfter(context);
      }
      throw e;
    } catch (InterruptedException e) {
      if (this.hasSendMessageHook()) {
        context.setException(e);
        this.executeSendMessageHookAfter(context);
      }
      throw e;
    } finally {
      msg.setBody(prevBody);
      msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQProducer.getNamespace()));
    }
  }

  throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);
}

这个方法干了不少脏活,不过发送的具体实现还是通过 MQClientAPIImpl.sendMessage 来实现。



send 发送流程

  1. 检查消息

  2. 从消息的目的 TopicName 中获取元信息;若获取不到 Topic,则抛异常

    2.1 从本地获取,没有则从 NameServer 获取

     2.1.1 从 NameServer 获取 Topic 元信息,没有则直接返回

     2.1.2 更新获取的 Topic 的路由信息

    2.2 将获取的 Topic 直接返回,若 NameServer 也没,则进行创建

     2.2.1 获取默认 Topic;获取失败直接返回

     2.2.2 继承该 Topic 的信息来进行更改以作为新 Topic

  3. 从 Topic 中选择 Queue

    3.1 排除掉在故障退避的 Broker 后,将下一个 Broker 所在的 Queue 返回

    3.2 所有 Broker 都需要退避下,选择次优 Broker

  4. 发送消息;失败则退回第三步

    4.1 Vip 检查

    4.2 消息类型检查

    4.3 调用钩子

    4.4 组装消息头

    4.5 发送消息

  5. 更新故障退避信息

  6. 根据发送方式返回结果



posted @ 2021-10-17 14:55  en_oc  阅读(1054)  评论(1编辑  收藏  举报