1

RocketMQ 消息消费

消息消费总览

消息消费以组的模式开展,一个消费组内可以包含多个消费者,1个消费组可订阅多个主题。消费组之间有集群模式与广播模式两种。

集群模式下,主题下的同一消息只允许被消费组内的一个消费者消费,消费进度存储在 broker 端。广播模式下,则每个消费者都可以消费该消息,消费进度存储在消费者端。

消息服务器与消费者的消息传输有 2 种方式:推模式、拉模式。拉模式,即消费者主动向消息服务器发送请求;推模式,即消息服务器向消费者推送消息。推模式,是基于拉模式实现的。

集群模式下,一个消费队列同一时间,只允许被一个消费者消费,1个消费者,可以消费多个消息队列。

RocketMQ 只支持局部顺序消息消费,即保证同一个消息队列上的消息顺序消费。如果想保证一个 Topic 下的顺序消费,那么只能将该主题的消息队列设置为 1。

认识消息消费者

DefaultMQPushConsumer

// 消费者组
private String consumerGroup;
// 消费模式,默认集群
private MessageModel messageModel = MessageModel.CLUSTERING;
// 根据消息进度从消息服务器拉取不到消息时重新计算消费策略
// CONSUME_FROM_LAST_OFFSET:从队列当前最大偏移量开始消费
// CONSUME_FROM_FIRST_OFFSET 从最早可用的消息开始消费
// CONSUME_FROM_TIMESTAMP 从指定的时间戳开始消费
private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET;
// 集群模式下消息队列负载策略
private AllocateMessageQueueStrategy allocateMessageQueueStrategy;
// 订阅消息
private Map<String /* topic */, String /* sub expression */> subscription = new HashMap<String, String>();
// 消息业务监听器
private MessageListener messageListener;
// 消息消费进度存储器
private OffsetStore offsetStore;
// 消费者最小线程数
private int consumeThreadMin = 20;
// 消费者最大线程数
private int consumeThreadMax = 20;
// 并发消息消费时处理队列最大跨度。表示如果消息处理队列中偏移量最大的消息与偏移量最小的消息的跨度超过2000则延迟50毫秒再拉取消息
private int consumeConcurrentlyMaxSpan = 2000;
// 每 1000 次流控后打印流控日志
private int pullThresholdForQueue = 1000;
// 推模式下拉取任务时间间隔,默认一次拉取任务完成继续拉取
private long pullInterval = 0;
// 消息并发消费时一次消费消息条数
private int consumeMessageBatchMaxSize = 1;
// 每次消息拉取条数
private int pullBatchSize = 32;
// 是否每次拉取消息都更新订阅信息
private boolean postSubscriptionWhenPull = false;
// 最大消费重试次数,消息消费次数超过 maxReconsumeTimes 还未成功,则将该消息转移到一个失败队列,等待删除
private int maxReconsumeTimes = -1;
// 消费超时时间,单位 分钟。
private long consumeTimeout = 15;

消费者启动流程

代码位置:DefaultMQPushConsumerImpl#start

  1. 构建 SubscriptionData 加入到 RebalanceImpl 订阅消息中。

DefaultMQPushConsumerImpl#copySubscription

private void copySubscription() throws MQClientException {
  try {
    Map<String, String> sub = this.defaultMQPushConsumer.getSubscription();
    if (sub != null) {
      for (final Map.Entry<String, String> entry : sub.entrySet()) {
        final String topic = entry.getKey();
        final String subString = entry.getValue();
        // 构建 SubscriptionData
        SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPushConsumer.getConsumerGroup(), topic, subString);
        // 加入 RebalanceImpl 订阅消息中
        this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData);
      }
    }
        // ...
    switch (this.defaultMQPushConsumer.getMessageModel()) {
       // ...
      case CLUSTERING:
        // 获取重试的主题,格式: %RETRY% + 消费组名
        final String retryTopic = MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup());
        // 构建重试主题的订阅消息
        SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPushConsumer.getConsumerGroup(), retryTopic, SubscriptionData.SUB_ALL);
        // 加入 RebalanceImpl 订阅消息中
        this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData);
        break;
       // ...
    }
  } catch (Exception e) {
    throw new MQClientException("subscription exception", e);
  }
}
  1. 初始化 MQClientInstance、RebalanceImpl 等。
  2. 初始化消息消费进度,集群模式,进度放在 broker,广播模式,本地
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
  this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
  switch (this.defaultMQPushConsumer.getMessageModel()) {
    case BROADCASTING:
      // 从本地获取消费进度
      this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
      break;
    case CLUSTERING:
      // 从 broker 获取消费进度
      this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
      break;
    default:
      break;
  }
  this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
this.offsetStore.load();
  1. 根据是否是顺序消费,创建不同的 ConsumeMessageService。该类主要负责消息的消费
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
  this.consumeOrderly = true;
  this.consumeMessageService = new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
  this.consumeOrderly = false;
  this.consumeMessageService = new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}
this.consumeMessageService.start();
  1. 向 MQClientInstance 注册消费者,并启动 MQClientInstance。在一个 JVM 中的所有消费者、生产者持有同一个 MQClientInstance,MQClientInstance 只会启动一次。
boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
// ...
mQClientFactory.start();

消息拉取

PullMessageService 消息拉取

RocketMQ 通过 PullMessageService 拉取消息。

PullMessageService#run

public void run() {
  // stopped 是 volidate 修饰的变量,用于线程间通信。
  while (!this.isStopped()) {
  // .. 
      // 阻塞队列, 如果 pullRequestQueue 没有元素,则阻塞
      PullRequest pullRequest = this.pullRequestQueue.take();
      // 消息拉取 
      this.pullMessage(pullRequest);
   // ...
  }
}
认识 PullRequest
// 消费者组
private String consumerGroup;
// 消息队列
private MessageQueue messageQueue;
// 消息处理队列,从 Broker 拉取到的消息先存入 ProcessQueue,然后再提交到消费者消费池消费
private ProcessQueue processQueue;
// 待拉取的 MessageQueue 偏移量
private long nextOffset;
// 是否被锁定
private boolean lockedFirst = false;
PullMessageService 添加 PullRequest 的方式
  • 延时添加
public void executePullRequestLater(final PullRequest pullRequest, final long timeDelay) {
  if (!isStopped()) {
    this.scheduledExecutorService.schedule(new Runnable() {
      @Override
      public void run() {
        PullMessageService.this.executePullRequestImmediately(pullRequest);
      }
    }, timeDelay, TimeUnit.MILLISECONDS);
  } else {
    log.warn("PullMessageServiceScheduledThread has shutdown");
  }
}
  • 立即添加
public void executePullRequestImmediately(final PullRequest pullRequest) {
  try {
    this.pullRequestQueue.put(pullRequest);
  } catch (InterruptedException e) {
    log.error("executePullRequestImmediately pullRequestQueue.put", e);
  }
}

认识 ProcessQueue

ProcessQueue 是 MessageQueue 在消费端的重现、快照。PullMessageService 从消息服务器默认每次拉取 32 条消息,按消息的队列偏移量顺序存放在 ProcessQueue 中,PullMessageService 再将消息提交到消费者消费线程池。消息消费成功后,从 ProcessQueue 中移除。

// 读写锁
private final ReadWriteLock lockTreeMap = new ReentrantReadWriteLock();
// 消息存储容器, k:消息偏移量,v:消息实体
private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>();
// ProcessQueue 中消息总数
private final AtomicLong msgCount = new AtomicLong();
// ProcessQueue 中消息总大小
private final AtomicLong msgSize = new AtomicLong();
// 当前 ProcessQueue 中包含的最大队列偏移量
private volatile long queueOffsetMax = 0L;
// 当前 ProcessQueue 是否被丢弃
private volatile boolean dropped = false;
// 上一次开始消息拉取时间戳
private volatile long lastPullTimestamp = System.currentTimeMillis();
// 上一次消息消费时间戳
private volatile long lastConsumeTimestamp = System.currentTimeMillis();

消息拉取流程

代码位置:DefaultMQPushConsumerImpl#pullMessage

消息客户端拉取消息
  1. 获取 ProcessQueue,并对 ProcessQueue 做校验
final ProcessQueue processQueue = pullRequest.getProcessQueue();
// 被删除
if (processQueue.isDropped()) {
  return;
}
// 设置拉取时间
pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());

try {
  this.makeSureStateOK();
} catch (MQClientException e) {
  // 状态非法,延迟 3 秒拉取消息
  this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
  return;
}
// 消费者被挂起 
if (this.isPause()) {
  // 延迟 1 秒拉取消息
  this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
  return;
}
  1. 对消息拉取进行流量控制

流控控制有以下几个维度:

  • processQueue 的消息数量 大于 1000, processQueue 的消息大小 大于 100 MB,将延迟 50 毫秒后拉取消息
  • processQueue 中偏移量最大的消息与偏移量最小的消息的跨度超过 2000 则延迟 50 毫秒再拉取消息
long cachedMessageCount = processQueue.getMsgCount().get();
long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);

// processQueue 的消息数量 大于 1000,触发流控
if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
  // 50 毫秒再拉取消息
  this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
  if ((queueFlowControlTimes++ % 1000) == 0) {
        // ... 流控输出语句
  }
  return;
}

// processQueue 的消息大小 大于 100 MB,触发流控
if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
  // 50 毫秒再拉取消息
  this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
  if ((queueFlowControlTimes++ % 1000) == 0) {
     // ... 流控输出语句
  }
  return;
}

if (!this.consumeOrderly) {
  // 顺序消费
  // processQueue 中偏移量最大的消息与偏移量最小的消息的跨度超过2000则延迟50毫秒再拉取消息
  if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
    this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
    if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
         // ... 流控输出语句
    }
    return;
  }
} else {
  if (processQueue.isLocked()) {
    if (!pullRequest.isLockedFirst()) {
      final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
      boolean brokerBusy = offset < pullRequest.getNextOffset();
      // ..
      if (brokerBusy) {
        // ... 
      }

      pullRequest.setLockedFirst(true);
      pullRequest.setNextOffset(offset);
    }
  } else {
    // 确保 processQueue 在消费之前必须被锁定。
    this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
    log.info("pull message later because not locked in broker, {}", pullRequest);
    return;
  }
}
  1. 根据主题拉取订阅的消息,如果为空,延迟 3 秒,再拉取
final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
if (null == subscriptionData) {
  this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
  log.warn("find the consumer's subscription failed, {}", pullRequest);
  return;
}
  1. 构建消息拉取系统标记

拉消息系统标记:
image.png

  • FLAG_COMMIT_OFFSET:表示从内存中读取的消费进度大于0,则设置该标记位
  • FLAG_SUSPEND:表示消息拉取时支持挂起
  • FLAG_SUBSCRIPTION:消息过滤机制为表达式,设置该标记位
  • FLAG_CLASS_FILTER:消息过滤机制为类过滤模式
  • FLAG_LITE_PULL_MESSAGE:精简拉取消息
// 从内存中读取消费进度,如果大于0,则设置 commitOffsetEnable = true
boolean commitOffsetEnable = false;
long commitOffsetValue = 0L;
if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) {
  commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);
  if (commitOffsetValue > 0) {
    commitOffsetEnable = true;
  }
}

String subExpression = null;
boolean classFilter = false;
SubscriptionData sd = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
if (sd != null) {
  if (this.defaultMQPushConsumer.isPostSubscriptionWhenPull() && !sd.isClassFilterMode()) {
    subExpression = sd.getSubString();
  }

  classFilter = sd.isClassFilterMode();
}

int sysFlag = PullSysFlag.buildSysFlag(
  commitOffsetEnable, // commitOffset
  true, // suspend
  subExpression != null, // subscription
  classFilter // class filter
);
  1. 向消息服务端拉取消息
this.pullAPIWrapper.pullKernelImpl(
  pullRequest.getMessageQueue(), // 从哪个队列拉取消息 
  subExpression, // 消息过滤表达式
  subscriptionData.getExpressionType(), // 消息表达式类型 TAG、SQL92
  subscriptionData.getSubVersion(), 
  pullRequest.getNextOffset(), // 本次拉取消息偏移量
  this.defaultMQPushConsumer.getPullBatchSize(), // 本次拉取最大消息条数,默认 32 
  sysFlag, // 拉消息 系统标记
  commitOffsetValue, // 当前 MessageQueue 消费进度
  BROKER_SUSPEND_MAX_TIME_MILLIS, // 消息拉取过程中 允许 broker 挂起时间,默认 15 s
  CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND, // 消息拉取超时时间,默认 30 s
  CommunicationMode.ASYNC, // 消息拉取模式,默认为异步拉取
  pullCallback // 从 broker 拉取到消息后的回调方法
);

最终调用 MQClientAPIImpl#pullMessageAsync 拉取消息

消息服务端 broker 组装消息

代码位置:PullMessageProcessor#processRequest

  1. 根据订阅消息,构建消息过滤器
  2. 调用 MessageStore.getMessage 查找消息
  3. 根据主题名与队列编号获取消息消费队列
  4. 消息偏移量异常情况校对下一次拉取偏移量
  5. 根据 PullRequest 填充 responseHeader 的 nextBeginOffset、minOffset、maxOffset
  6. 根据主从同步延迟,如果从节点数据包含下一次拉取的偏移量,设置下一次拉取任务的 brokerId
  7. 如果 commitlog 标记可用并且当前节点为主节点,则更新消息消费进度
消息拉取客户端处理消息

代码入口: MQClientAPIImpl#pullMessageAsync

处理请求回调,获取 PullResult。并通过 PullCallback 回调至 DefaultMQPushConsumerImpl#pullMessage

  1. 处理拉取到的消息,将结果封装为 PullResult,并通过 PullCallback 将 PullResult 回调
private PullResult processPullResponse(
  final RemotingCommand response) throws MQBrokerException, RemotingCommandException {
  PullStatus pullStatus = PullStatus.NO_NEW_MSG;
  switch (response.getCode()) {
    case ResponseCode.SUCCESS:
      pullStatus = PullStatus.FOUND;
      break;
    case ResponseCode.PULL_NOT_FOUND:
      pullStatus = PullStatus.NO_NEW_MSG;
      break;
    case ResponseCode.PULL_RETRY_IMMEDIATELY:
      pullStatus = PullStatus.NO_MATCHED_MSG;
      break;
    case ResponseCode.PULL_OFFSET_MOVED:
      pullStatus = PullStatus.OFFSET_ILLEGAL;
      break;

    default:
      throw new MQBrokerException(response.getCode(), response.getRemark());
  }
  
  PullMessageResponseHeader responseHeader =
    (PullMessageResponseHeader) response.decodeCommandCustomHeader(PullMessageResponseHeader.class);

  return new PullResultExt(pullStatus, responseHeader.getNextBeginOffset(), responseHeader.getMinOffset(), responseHeader.getMaxOffset(), null, responseHeader.getSuggestWhichBrokerId(), response.getBody());
}
  1. 处理回调的 PullResult ,解码消息,并过滤消息

代码入口:PullAPIWrapper#processPullResult

  1. 假设消息被找到,处理消息被找到的逻辑

代码入口:DefaultMQPushConsumerImpl$PullCallback#onSuccess

// 设置下一次拉取的偏移量
long prevRequestOffset = pullRequest.getNextOffset();
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
long pullRT = System.currentTimeMillis() - beginTimestamp;
DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), pullRT);

long firstMsgOffset = Long.MAX_VALUE;
if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
  // 消息为null,立即拉取新的消息
  DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
} else {
  firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
 DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
    
  // 将消息放入 ProcessQueue,并将消息交由 ConsumeMessageService 消费。
  boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
  DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
    pullResult.getMsgFoundList(),
    processQueue,
    pullRequest.getMessageQueue(),
    dispatchToConsume);
 
// 根据 pullInterval 参数,等待 pullInterval 毫秒将 PullRequest 放入 pullRequestQueue 中。
// 推模式下, pullInterval 默认为 0
  if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
  } else {
    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
  }
}
消息拉取流程图

image.png

消息拉取长轮询机制

RocketMQ 推模式是循环向消息服务端发送消息拉取请求。消费者向 broker 拉取消息时,如果消息未到达消费队列,并且未启用 长轮询机制,则会在服务端等待 shortPollingTimeMills(默认1秒) 时间后再去判断消息是否已经到达消息队列,如果消息未到达,则提示消息拉取客户端 PULL_NOT_FOUND。如果开启长轮询模式,rocketMQ 会每 5s 轮询检查一次消息是否可达,同时一有新消息到达后立马通知挂起线程再次验证新消息是否是自己感兴趣的消息,如果是则从 commitlog 文件提取消息返回给消息拉取客户端,否则直到挂起超时,超时时间由消息拉取方在消息拉取时封装在请求参数中,PUSH 模式默认 15s。 PULL 模式通过 DefaultMQPullConsumer#setBrokerSuspendMaxTimeMillis 设置。RocketMQ 通过在 Broker 端配置 longPollingEnable 为 true 来开启长轮询模式。

RocketMQ 的长轮询机制由 2 个线程共同完成。PullRequestHoldService、ReputMessageService。

  • PullRequestHoldService 添加消息拉取任务
    代码入口:PullRequestHoldService#suspendPullRequest

    1. 通过 topic + '&' + queueId 构建 key
    2. 从 ConcurrentMap<String/ topic@queueId /, ManyPullRequest> pullRequestTable 中获取对应的 ManyPullRequest。ManyPullRequest 中存储了队列堆积的消息拉取任务
    3. 将新的消息拉取任务,放入 ManyPullRequest 中。
  • PullRequestHoldService 执行消息拉取任务
    代码入口:PullRequestHoldService#run

    如果 broker 支持 长轮询,则每 5s 尝试一次,如果未开启,则每 1s 尝试一次

  • PullRequestHoldService 检查是否可拉取消息
    代码入口:PullRequestHoldService#notifyMessageArriving

    1. 遍历所有拉取任务,获取该主题下队列的最大偏移量,如果大于待拉取偏移量,说明有新的消息到达,调用 notifyMessageArriving 触发消息拉取
    2. 如果队列的最大偏移量大于待拉取偏移量,且消息匹配,则调用 executeRequestWhenWakeup 将消息返回给消息拉取客户端,否则等待下一次拉取
    3. 如果挂起超时,则不继续等待将直接返回客户端消息未找到。
      (不继续等待客户端,直接将消息返回的代码入口:PullMessageProcessor#executeRequestWhenWakeup)
  • ReputMessageService 线程每 1毫秒,调用 PullRequestHoldService#notifyMessageArriving。长轮询模式,消息准实时。

消息拉取长轮询是指:rocketMQ 每 5s 轮询检查一次消息是否可达。有消息后,立马通知挂起的线程处理消息。

需要通过在 broker 配置 longPollingEnable=true 开启长轮询模式。

消息队列负载与重新分布机制

RocketMQ 消息队列重新分布由 RebalanceService 线程来实现的。RebalanceService 随着 MQClientInstance 的启动而启动。RebalanceService 默认每 20 秒,执行一次 MQClientInstance#doRebalance

集群模式下,主题的消息队列负载

代码入口:RebalanceImpl#rebalanceByTopic

  1. 获取主题的队列,向 broker 发送请求,获取主题下,消费组所有消费者客户端ID。
  2. 只有当 2 者均不为空时,才有必要进行 rebalance。
  3. 在 rebalance 时,需要对 队列,还有消费者客户端 ID 进行排序,以确保同一个消费组下的视图是一致的。
  4. 根据 分配策略 AllocateMessageQueueStrategy 为 消费者分配队列。
// 获取主题的队列
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
// 向 broker 发送请求,获取该主题下,该消费组的所有 消费者客户端ID
List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
if (null == mqSet) {
  if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
    log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic);
  }
}
if (null == cidAll) {
  log.warn("doRebalance, {} {}, get consumer id list failed", consumerGroup, topic);
}


if (mqSet != null && cidAll != null) {
  List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
  mqAll.addAll(mqSet);
    
  // 排序,同一消费组内,视图一致,确保同一个消费者队列不会被多个消费者分配
  Collections.sort(mqAll);
  Collections.sort(cidAll);

  // 根据分配策略为消费者分配队列。
  AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
  List<MessageQueue> allocateResult = null;
  try {
    allocateResult = strategy.allocate(
      this.consumerGroup,
      this.mQClientFactory.getClientId(),
      mqAll,
      cidAll);
  } catch (Throwable e) {
    log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),
              e);
    return;
  }

  Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
  if (allocateResult != null) {
    allocateResultSet.addAll(allocateResult);
  }
    
  // 更新消费者消息队列信息
  boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
  if (changed) {
    this.messageQueueChanged(topic, mqSet, allocateResultSet);
  }
}
  1. 更新消费者消息队列信息

代码入口:RebalanceImpl#updateProcessQueueTableInRebalance

如果 processQueue 中的 MessageQueue 不在刚分配的 MessageQueue 中, 那么表示,该 MessageQueue 已分配给别的消费者,那么需要删除此 PullRequest

private boolean updateProcessQueueTableInRebalance(final String topic, final Set<MessageQueue> mqSet, final boolean isOrder) {                                                                                                                      
  boolean changed = false;                                                                                                                      
    
  Iterator<Entry<MessageQueue, ProcessQueue>> it = this.processQueueTable.entrySet().iterator();                                                
  while (it.hasNext()) {                                                                                                                        
    Entry<MessageQueue, ProcessQueue> next = it.next();                                                                                       
    MessageQueue mq = next.getKey();                                                                                                          
    ProcessQueue pq = next.getValue();                                                                                                        
         // 如果 processQueue 中的 MessageQueue 不在刚分配的 MessageQueue 中,则删除 MessageQueue。
    if (mq.getTopic().equals(topic)) {                                                                                                        
      if (!mqSet.contains(mq)) {                                                                                                            
        pq.setDropped(true);                                                                                                              
        if (this.removeUnnecessaryMessageQueue(mq, pq)) {                                                                                 
          it.remove();                                                                                                                  
          changed = true;                                                                                                                    
        }                                                                                                                                 
      } else if (pq.isPullExpired()) {     
        switch (this.consumeType()) {                                                                                                     
          case CONSUME_ACTIVELY:                                                                                                        
            break;                                                                                                                    
          case CONSUME_PASSIVELY:                                                                                                       
            pq.setDropped(true);                                                                                                      
            if (this.removeUnnecessaryMessageQueue(mq, pq)) {                                                                         
              it.remove();                                                                                                          
              changed = true;                                                                                                       
            }                                                                                                                         
            break;                                                                                                                    
          default:                                                                                                                      
            break;                                                                                                                    
        }                                                                                                                                 
      }                                                                                                                                     
    }                                                                                                                                         
  }                                                                                                                                             

遍历刚分配的 MessageQueue,如果 processQueueTable 中不包含,则表示是刚添加的。

首先队列删除消费进度,再为之创建 ProcessQueue,计算下次拉取的偏移量。创建对应的 PullRequest,并加入到 pullRequestList 中, 唤醒 PullMessageService。

List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
// 遍历刚分配的 MessageQueue,如果 processQueue 中不包含,则表示是刚添加的。
for (MessageQueue mq : mqSet) {
  if (!this.processQueueTable.containsKey(mq)) {
    if (isOrder && !this.lock(mq)) {
      log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
      continue;
    }
      // 移除消费进度
    this.removeDirtyOffset(mq);
    // 创建新的 ProcessQueue
    ProcessQueue pq = new ProcessQueue();
    // 计算下次从哪里拉取消息
    long nextOffset = this.computePullFromWhere(mq);
    if (nextOffset >= 0) {
      ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
      if (pre != null) {
        log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
      } else {
        log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
        PullRequest pullRequest = new PullRequest();
        pullRequest.setConsumerGroup(consumerGroup);
        pullRequest.setNextOffset(nextOffset);
        pullRequest.setMessageQueue(mq);
        pullRequest.setProcessQueue(pq);
        pullRequestList.add(pullRequest);
        changed = true;
      }
    } else {
      log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
    }
  }
}

在计算下次拉取偏移量时, RocketMQ 提供了 3 种方式。可在消费者启动时,调用DefaultMQPushConsumer#setConsumeFromWhere 设置

  • ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET:从队列当前最大偏移量开始消费
  • ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET 从最早可用的消息开始消费
  • ConsumeFromWhere.CONSUME_FROM_TIMESTAMP 从指定的时间戳开始消费

注意: ConsumeFromWhere 消费进度校验只有在从磁盘中获取的消费进度返回 -1 时才失效。即刚创建的 消费组,如果 broker 中已经有记录该消费组的消费进度,那么该值的设置是无效的

PullMessageService 与 RebalanceService 线程交互图

image.png

消息消费过程

消息消费

总代码入口:ConsumeMessageConcurrentlyService#submitConsumeRequest

  1. 如果消息数量大于 32 则分页处理

代码位置:ConsumeMessageConcurrentlyService#submitConsumeRequest

  1. 每次进行消费时,都会判断 processQueue 是否被删除,阻止消费者 消费 不属于自己的 队列
  2. 恢复重试消息主题名, rocketMQ 消息重试机制,决定了,如果发现消息的延时级别 delayTimeLevel 大于 0,会首先将重试主题存入消息的属性中,然后设置主题名称为 SCHEDULE_TOPIC ,以便时间到后重新参与消息消费。
  3. 在消费之前,执行 hock
  4. 执行,我们编写的消费代码
  5. 在消费之后,执行 hock
  6. 消费完毕后,再次验证 processQueue 是否被删除,如果被删除,不处理结果

ConsumeMessageConcurrentlyService$ConsumeRequest#run

public void run() {
  // 阻止消费者 消费 不属于自己的 队列
  if (this.processQueue.isDropped()) {
    log.info("the message queue not be able to consume, because it's dropped. group={} {}", ConsumeMessageConcurrentlyService.this.consumerGroup, this.messageQueue);
    return;
  }

  MessageListenerConcurrently listener = ConsumeMessageConcurrentlyService.this.messageListener;
  ConsumeConcurrentlyContext context = new ConsumeConcurrentlyContext(messageQueue);
  ConsumeConcurrentlyStatus status = null;
  // 恢复重试消息主题名, rocketMQ 消息重试机制,决定了,如果发现消息的延时级别 delayTimeLevel 大于 0,
  // 会首先将重试主题存入消息的属性中,然后设置主题名称为 SCHEDULE_TOPIC ,以便时间到后重新参与消息消费。
  defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());

  // 消息消费前 hock
  // ...

  long beginTimestamp = System.currentTimeMillis();
  boolean hasException = false;
  ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
  try {
    if (msgs != null && !msgs.isEmpty()) {
      for (MessageExt msg : msgs) {
        MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis()));
      }
    }
    // 消费消息,这里是调用我们的业务代码。
    status = listener.consumeMessage(Collections.unmodifiableList(msgs), context);
  } catch (Throwable e) {
    // 记录异常
    hasException = true;
  }
  // ... 

  // 执行消费后 hock
  // ...

  // 再次对 processQueue dropped 进行校验,如果为 true,那么不对结果进行处理。因为消息会被别的消费者重新消费
  if (!processQueue.isDropped()) {
    ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this);
  } else {
    // 记录异常
  }
}
  1. 对消费者返回的结果,进行处理
  2. 如果消费成功,那么 ack = consumeRequest.getMsgs().size() - 1。会直接更新消费进度。如果消费失败,那么 ack = -1,重新发送消息。如果在重新发送消息时,又失败了,那么会延迟 5 秒在继续消费。
  3. 不管是消费成功,还是失败,都会更新消费进度

ConsumeMessageConcurrentlyService$processConsumeResult

// 默认 Integer.MAX_VALUE
int ackIndex = context.getAckIndex();

if (consumeRequest.getMsgs().isEmpty())
  return;

switch (status) {
   // 消费成功, ackIndex 被设置为 consumeRequest.getMsgs().size() - 1
  case CONSUME_SUCCESS:
    if (ackIndex >= consumeRequest.getMsgs().size()) {
      ackIndex = consumeRequest.getMsgs().size() - 1;
    }
    int ok = ackIndex + 1;
    int failed = consumeRequest.getMsgs().size() - ok;
    this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), ok);
    this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), failed);
    break;
   // 消费失败, ackIndex 被设置为 -1. 
  case RECONSUME_LATER:
    ackIndex = -1;
    this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(),consumeRequest.getMsgs().size());
    break;
  default:
    break;
}

switch (this.defaultMQPushConsumer.getMessageModel()) {
  case BROADCASTING:
    for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
      MessageExt msg = consumeRequest.getMsgs().get(i);
      log.warn("BROADCASTING, the message consume failed, drop it, {}", msg.toString());
    }
    break;
    // 集群模式
  case CLUSTERING:
    List<MessageExt> msgBackFailed = new ArrayList<MessageExt>(consumeRequest.getMsgs().size());
    // for 循环内的代码,只有在消费失败的时候,才会执行到
    // 消费失败, ackIndex = -1。因此该批次消息都需要发 ack
    // 消息消费成功, ackIndex = consumeRequest.getMsgs().size() - 1。因此不会执行 for 循环。
    for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
      MessageExt msg = consumeRequest.getMsgs().get(i);
      // 调用 producer 重新发送消息
      boolean result = this.sendMessageBack(msg, context);
      // 消息发送 ACK 失败,
      if (!result) {
        msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
        msgBackFailed.add(msg);
      }
    }
    // 如果存在发送 ACK 失败的消息, 延迟 5 秒后,重新消费。
    if (!msgBackFailed.isEmpty()) {
      consumeRequest.getMsgs().removeAll(msgBackFailed);
      this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
    }
    break;
  default:
    break;
}
// 移除这批消息,返回 移除该批消息后最小的偏移量
long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());
if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
  // 不管是消费成功,还是消费失败,都会更新消费进度
 this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
}

消息确认

总代码入口:SendMessageProcessor#consumerSendMsgBack

客户端在发送重试消息时,封装了 ConsumerSendMsgBackRequestHeader。先看看这个类的属性

// 消息物理偏移量
private Long offset;
// 消费组
private String group;
// 延迟等级
private Integer delayLevel;
// 消息ID
private String originMsgId;
// 消息主题
private String originTopic;
// 最大重新消费次数,默认 16 次   SubscriptionGroupConfig.retryMaxTimes 中定义
private Integer maxReconsumeTimes;

当客户端调用 MQClientAPIImpl#consumerSendMessageBack ,发送消息时,服务由 SendMessageProcessor#consumerSendMsgBack 接收此次请求。

代码入口:SendMessageProcessor#consumerSendMsgBack

  1. 先获取消费组订阅配置信息,不存在则直接返回
  2. 创建主题:%RETRY% + group,并随机选择一个队列
  3. 用原来的消息,创建一个新的消息
  4. 如果重试消息的最大重试次数超过 16 次(默认),则将消息放入 %DLQ% 队列。等待人工处理
  5. 由 Commitlog.putMessage 存入消息。

注:如果以上某个环节出现错误,会导致消息客户端重新封装新的 ConsumeRequest,并延迟 5s 执行。

private RemotingCommand consumerSendMsgBack(final ChannelHandlerContext ctx, final RemotingCommand request)
        throws RemotingCommandException {
  // ...

  // 钩子函数
  // ... 
    
  // 先获取消费组订阅配置信息
  SubscriptionGroupConfig subscriptionGroupConfig =
 this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(requestHeader.getGroup());
  // 不存在,直接返回
  if (null == subscriptionGroupConfig) {
    response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST);
    response.setRemark("subscription group not exist, " + requestHeader.getGroup() + " "
                       + FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST));
    return response;
  }

  
  // 队列权限判断
  // ...
 
  // 创建新的主题,并随机选择一个队列
  // %RETRY% + group
  String newTopic = MixAll.getRetryTopic(requestHeader.getGroup());
  // 随机选择一个队列
  int queueIdInt = Math.abs(this.random.nextInt() % 99999999) % subscriptionGroupConfig.getRetryQueueNums();

  // ...
  // ...

  // 根据 偏移量 获取消息
  MessageExt msgExt = this.brokerController.getMessageStore().lookMessageByOffset(requestHeader.getOffset());
  if (null == msgExt) {
    response.setCode(ResponseCode.SYSTEM_ERROR);
    response.setRemark("look message by offset failed, " + requestHeader.getOffset());
    return response;
  }

  // ...

  int delayLevel = requestHeader.getDelayLevel();
  // 消息超过最大重试次数,默认最大重试 16 次
  int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes();
  if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal()) {
    maxReconsumeTimes = requestHeader.getMaxReconsumeTimes();
  }

  // 如果消息重试次数超过 maxReconsumeTimes,再次改写 newTopic 主题为 %DLQ%,
  // 该主题的权限为只写,说明消息一旦进入到 DLQ 队列中,RocketMQ 将不在负责再次调度
  // 消费,需要人工干预。
  if (msgExt.getReconsumeTimes() >= maxReconsumeTimes
      || delayLevel < 0) {
    newTopic = MixAll.getDLQTopic(requestHeader.getGroup());
    queueIdInt = Math.abs(this.random.nextInt() % 99999999) % DLQ_NUMS_PER_GROUP;

    topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic,
DLQ_NUMS_PER_GROUP, PermName.PERM_WRITE, 0);
    
    // ... 
  } else {
    if (0 == delayLevel) {
      delayLevel = 3 + msgExt.getReconsumeTimes();
    }
    msgExt.setDelayTimeLevel(delayLevel);
  }

  // 按照原消息,创建一个新的消息,主题: %RETRY% + group
  MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
  msgInner.setTopic(newTopic);
  msgInner.setBody(msgExt.getBody());
  msgInner.setFlag(msgExt.getFlag());
  MessageAccessor.setProperties(msgInner, msgExt.getProperties());
  msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
  msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(null, msgExt.getTags()));

  msgInner.setQueueId(queueIdInt);
  msgInner.setSysFlag(msgExt.getSysFlag());
  msgInner.setBornTimestamp(msgExt.getBornTimestamp());
  msgInner.setBornHost(msgExt.getBornHost());
  msgInner.setStoreHost(this.getStoreHost());
  msgInner.setReconsumeTimes(msgExt.getReconsumeTimes() + 1);
  String originMsgId = MessageAccessor.getOriginMessageId(msgExt);
  MessageAccessor.setOriginMessageId(msgInner, UtilAll.isBlank(originMsgId) ? msgExt.getMsgId() : originMsgId);
  
  // 交由 commitlog 存入消息
  PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
  if (putMessageResult != null) {
    switch (putMessageResult.getPutMessageStatus()) {
      case PUT_OK:
        // ... 返回正确结果
      default:
        break;
    }

    // ... 返回错误结果
    return response;
  }

  // ... 返回错误结果
  return response;
}

消息消费,消息确认 流程图

消费者消费流程.png

定时消息

代码入口:ScheduleMessageService#start

定时消息拉取流程

逻辑比较简单,直接说主要的流程。

  1. 根据 队列ID,延迟主题,找到消息队列
  2. 遍历消息队列,找到消息偏移量
  3. 根据消息偏移量查找消息

(以上,是查找消息的流程)

  1. 找到消息后,从消息属性中,获取原消息主题,消息队列,重新封装一个消息(在发送延迟消息时,会将原消息的主题、队列存入消息的属性中。key 分别是 PROPERTY_REAL_TOPIC、PROPERTY_REAL_QUEUE_ID)
  2. 将消息放入 commitlog,并转发到对应的消息消费队列。

其中需要注意的一个细节是:ScheduleMessageService 每隔 10 秒钟 持久化一次 延迟消息消费进度。

定时消息拉取流程图

image.png

顺序消息

  1. 顺序消息在 消息队列负载时,就需要向 broker 请求加锁该队列,加锁成功后,才会分配该队列,否则不会分配。如果当次队列负载未加锁成功,则会在下一次继续尝试加锁。

    代码入口:RebalanceImpl#updateProcessQueueTableInRebalance 以及加锁逻辑 RebalanceImpl#lock

  2. 在拉取消息时(由PullMessageService 负责消息拉取),如果消息处理队列未被锁定,则延迟 3s 后再将 PullRequest 对象放入到拉取任务中。如果是第一次拉取,则计算拉取偏移量。

    代码入口:DefaultMQPushConsumerImpl#pullMessage

  3. 顺序消费者,由 ConsumerMessageOrderly 实现。在启动 ConsumerMessageOrderly 时, 会启动一个线程,每隔 20秒,就锁定当前分配的队列。

    代码入口:ConsumerMessageOrderly#start

  4. ConsumerMessageOrderly 消费流程

    1. ConsumerMessageOrderly 在消费时,消费线程池中,只会有一个线程在消费
    2. ConsumerMessageOrderly 的消费是以时间为单位,一个消费组内的线程,默认最多消费 60s
    3. 每次从处理队列中拉取 1 条(默认)消息,如果为null,则表示没有消息,退出本次循环
    4. 在消费消息前,执行 钩子函数 (ConsumeMessageHook)
    5. 真正开始消费消息时,需要将消息上锁。如果消息未被丢弃,则执行,我们编写的业务代码
    6. 执行消费后 钩子函数(ConsumeMessageHook)
    7. 如果消费成功,则提交消费进度(即从 ProcessQueue 中删除该批消息)
    8. 如果消费失败,若 消息重试次数大于或等于允许的最大重试次数,消息最终会被送入 DLQ 队列。若不允许重试,那么该批消息将会被提交(即消费成功)
    9. 消费成功后,保存消费进度

代码入口:ConsumerMessageOrderly$ConsumeRequest#run


心无私天地宽
513 声望22 粉丝