RocketMQ源码解读--消费方式(上篇)

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情

1 RocketMQ的消费方式包含Pull和Push两种。

1.1 Pull方式

用户主动Pull消息,自主管理位点,可以灵活的掌握消费进度和速度。需要从代码层精准的控制消费。在新版本中 DefaultLitePullConsumer代替DefaultMQPullConsumer成为了默认的pull消费者实现类。

1.2 Push方式

代码接入简单,适合大部分业务场景,DefaultMQPushConsumer是默认的Push消费者类

2 Pull消费流程

image.png

2.1具体消费步骤

2.1.1 根据fetchSubscribeMessageQueues()方法拉去全部可以消费的Queue,

public Set<MessageQueue> fetchMessageQueues(String topic) throws MQClientException {
    checkServiceState();
    Set<MessageQueue> result = this.mQClientFactory.getMQAdminImpl().fetchSubscribeMessageQueues(topic);
    return parseMessageQueues(result);
}
复制代码

2.1.2 遍历全部queue,获取所有可以消费的消息。

private Set<MessageQueue> parseMessageQueues(Set<MessageQueue> queueSet) {
    Set<MessageQueue> resultQueues = new HashSet<MessageQueue>();
    for (MessageQueue messageQueue : queueSet) {
        String userTopic = NamespaceUtil.withoutNamespace(messageQueue.getTopic(),
            this.defaultLitePullConsumer.getNamespace());
        resultQueues.add(new MessageQueue(userTopic, messageQueue.getBrokerName(), messageQueue.getQueueId()));
    }
    return resultQueues;
}
复制代码

2.1.3 若是有消息,则执行用户代码的消费逻辑。

2.1.4 保存消费进度,可以使用updatePullOffset更新Offset位点

private void updatePullOffset(MessageQueue messageQueue, long nextPullOffset, ProcessQueue processQueue) {
    if (assignedMessageQueue.getSeekOffset(messageQueue) == -1) {
        assignedMessageQueue.updatePullOffset(messageQueue, nextPullOffset, processQueue);
    }
}
复制代码

3 Push消费流程

image.png

3.1 具体消费步骤是:

3.1.1 初始化Push消费者实例

业务代码初始化DefaultMQPushConsumer,启动pull服务PullMessageService。这个服务是个线程服务,不断执行run()方法拉取已经订阅的Topic的全部消息,并将消息缓存在本地队列中。

3.1.2 消费消息

由消费服务将本地缓存队列中的消息不断放入消费线程池。并异步回调业务消费代码。

3.1.3 保存消费进度

业务代码消费消息后,将消费结果返回给消费服务,再由消费服务将消费进度保存在本地,并由消费进度管理服务定时持久化到本地或者Broker。对于消费失败的消息,客户端处理后发回Broker并告知消费失败。

下边详细说说,消费流程中的消费消息

4 消费消息

4.1 PullMessageService拉取消息

messageRequestQueue中保存着待拉取的TopicQueue信息,程序会不停的从messageRequestQueue中获取messageRequest并执行拉取消息的方法。

@Override
public void run() {
    log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        try {
            MessageRequest messageRequest = this.messageRequestQueue.take();
            if (messageRequest.getMessageRequestMode() == MessageRequestMode.POP) {
                this.popMessage((PopRequest)messageRequest);
            } else {
                this.pullMessage((PullRequest)messageRequest);
            }
        } catch (InterruptedException ignored) {
        } catch (Exception e) {
            log.error("Pull Message Service Run Method exception", e);
        }
    }

    log.info(this.getServiceName() + " service end");
}
复制代码

4.2 消费者拉取消息并消费

第一步的时候会调用org.apache.rocketmq.client.impl.consumer.PullMessageService#pullMessage方法。 这个方法比较长,我们拆开来看。

4.2.1 基础校验

4.2.1.1 校验ProcessQueue是否dropped

final ProcessQueue processQueue = pullRequest.getProcessQueue();
if (processQueue.isDropped()) {
    log.info("the pull request[{}] is dropped.", pullRequest.toString());
    return;
}
复制代码

4.2.1.2 校验消费者服务是否正常

try {
    this.makeSureStateOK();
} catch (MQClientException e) {
    log.warn("pullMessage exception, consumer state not ok", e);
    this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
    return;
}
复制代码

4.2.1.3 校验消费者是否被挂起

if (this.isPause()) {
    log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
    this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
    return;
}
复制代码

4.2.2 拉取数量,字数限制等检查

long cachedMessageCount = processQueue.getMsgCount().get();
long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);

if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
    this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
    if ((queueFlowControlTimes++ % 1000) == 0) {
        log.warn(
            "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
            this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
    }
    return;
}

if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
    this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
    if ((queueFlowControlTimes++ % 1000) == 0) {
        log.warn(
            "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
            this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
    }
    return;
}
复制代码

从代码中可以看出,当缓存中的消息数量大于配置中的最大拉取条数的时候,则延迟50毫秒之后再拉取。 当缓存中的缓存消息字节数大于配置的最大缓存字节数,则延迟50毫秒拉取。

4.2.3 并发消息和顺序消息校验

4.2.3.1 并发消息

if (!this.consumeOrderly) {
    if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
        if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
            log.warn(
                "the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}",
                processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(),
                pullRequest, queueMaxSpanFlowControlTimes);
        }
        return;
    }
} 
复制代码

这里有一个processQueue.getMaxSpan()它的意思是本地缓存队列中第一个消息和最后一个消息的offset差值。如下所示:

public long getMaxSpan() {
    try {
        this.treeMapLock.readLock().lockInterruptibly();
        try {
            if (!this.msgTreeMap.isEmpty()) {
                return this.msgTreeMap.lastKey() - this.msgTreeMap.firstKey();
            }
        } finally {
            this.treeMapLock.readLock().unlock();
        }
    } catch (InterruptedException e) {
        log.error("getMaxSpan exception", e);
    }
    return 0;
}
复制代码

若是这里的maxSpan大于设置的值(默认2000),则认为本地消费过慢,需要进行本地流控。

4.2.3.2 顺序消息

if (processQueue.isLocked()) {
    if (!pullRequest.isPreviouslyLocked()) {
        long offset = -1L;
        try {
            //如果之前没有被锁定过,也就是第一次拉取,那么就需要计算最新的offset并修正本地最新的待拉取信息,在执行拉取。
            offset = this.rebalanceImpl.computePullFromWhereWithException(pullRequest.getMessageQueue());
            if (offset < 0) {
                throw new MQClientException(ResponseCode.SYSTEM_ERROR, "Unexpected offset " + offset);
            }
        } catch (Exception e) {
            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
            log.error("Failed to compute pull offset, pullResult: {}", pullRequest, e);
            return;
        }
        boolean brokerBusy = offset < pullRequest.getNextOffset();
        log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
            pullRequest, offset, brokerBusy);
        if (brokerBusy) {
            log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
                pullRequest, offset);
        }

        pullRequest.setPreviouslyLocked(true);
        pullRequest.setNextOffset(offset);
    }
} else {
    //如果当前消息队列再Broker端没有被锁定,说明已经有拉取增在进行,当前拉去请求稍后执行(默认3秒)
    this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
    log.info("pull message later because not locked in broker, {}", pullRequest);
    return;
}
复制代码

如果当前消息队列在Broker端没有被锁定,说明已经有拉取正在进行,当前拉去请求稍后执行(默认3秒)。
如果之前没有被锁定过,也就是说第一次拉取,那么就需要计算最新的offset修正本地最新的待拉取信息,再执行拉取。

4.2.4 订阅关系校验

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;
}
复制代码

这段代码的意思是如果待拉取的Topic在本地缓存中的订阅关系为空,则稍后执行拉取请求。(默认3S)

下篇文章继续讲解。。。

猜你喜欢

转载自juejin.im/post/7128772924508471310