消息拉取流程

1.消息拉取流程

1.PullMessageService 实现机制
@Override
public void run() {
    log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        try {
            PullRequest pullRequest = this.pullRequestQueue.take();
            this.pullMessage(pullRequest);
        } catch (InterruptedException ignored) {
        } catch (Exception e) {
            log.error("Pull Message Service Run Method exception", e);
        }
    }

    log.info(this.getServiceName() + " service end");
}

PullMessageService ,消息拉取服务线程,run 方法是其核心逻辑, run 方法的几个核心要点如下

1 ) while ( !this isStopped ())这是一种通用 设计技巧, isStopped 声明为volatile , 每执行一次业务逻辑检测一下其运行状态,可以通过其他线程将 stopped 设置为 true 从而停止该线程

)从 pullRequestQueue 中获取 PullRequest 消息拉取任务,如果 pullRequestQueue为空,则线程将阻塞,直到有拉取任务被放入

3 )调用 pullMessage 法进行消息拉取

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

一个是在 RocketMQ 根据PullRequest 拉取任务执行完一次消息拉取任务后,又将 PullRequest 对象放入到 pullRequestQueue ,第二个是Rebalanccelmpl 中创建消息拉取

private void pullMessage(final PullRequest pullRequest) {
    final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
    if (consumer != null) {
        DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
        impl.pullMessage(pullRequest);
    } else {
        log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
    }
}

根据消费组名 MQClientlnstance 中获取消费者内部实现 MQConsumerlnner ,令人意外的 这里将 consumer 强制转换为 DefaultMQPushConsumerlmpl ,也就是 PullMessageService ,该线程只为 PUSH 模式服务, 那拉模式如何拉取消息呢?其实细想也不难理解,PULL 模式 RocketMQ 需要提供拉取消息 API 即可, 具体由应用程序显示调用拉取 API

2.ProcessQueue 实现机制

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

中移除

3.消息拉取基本流程

1 )消息拉取客户端消息拉取请求封装

  1. 消息服务器 找并返回消息

3 )消息拉取客户端处理返回的消息

3.1消息拉取客户端消息拉取请求封装
final ProcessQueue processQueue = pullRequest.getProcessQueue();
if (processQueue.isDropped()) {
    log.info("the pull request[{}] is dropped.", pullRequest.toString());
    return;
}

pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());

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

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;
}
  1. 从 PullRequest中获取ProcessQueue,如果处理队列当前状态未被丢弃,则更新ProcessQueue 的lastPullTimestamp 为当前时间戳

如果当前消费者被挂起,则将拉取任务延迟 ls 再次放入到 PullMessageService 的拉取任务队列中,结束本次消息拉取

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 (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;
            }
  1. 进行消息拉取流控 , 从消息消费数量与消费间隔两个维度进行控制

1 )消息处理总数,如果 processQueue 当前处理的消息条数超过了 PullThresholdForQueue=l000 将触发流控,放弃本次拉取任务,并且该队列的下 次拉取任务将在 50 毫秒后才加入到拉取任务队列中, 触发 1000 次流控后输出提示语: the consumer message buffer is

full, so do flow control, minOffset= {队列最小偏移 }, maxOffset= {队列最大偏移 }, size={消息总条数}, pullRequest= {拉取任务}, flowControlTimes= {流控触发次数}

2 ) ProcessQueue 中队列最大偏移量与最小偏离量的间距, 不能超 consumeConcurrentlyMaxSpan ,否则触发流控, 每触发 1000 次输出提示语: the queue’s messages, span too long, so do flow control, minOffset= {队列 小偏移量}, maxOffs t= {队列最大偏移量}, maxSpan={间隔}pullRequest= {拉取任务信息}, flowControlTimes={流控触发次数} 这里主要的考量是担心一条消息堵塞,消息进度无法向前推进,可能造成大量消息重复消费

RocketMQ 消息消费端会从 3 个维度进行限流:

  1. 消息消费端队列中积压的消息超过 1000 条
  2. 消息处理队列中尽管积压没有超过 1000 条,但最大偏移量与最小偏移量的差值超过 2000
  3. 消息处理队列中积压的消息总大小超过 100M
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;
}

拉取该主题订阅信息,如果为空,结束本次消息拉取,关于该队列的下一次拉取延迟3秒

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

构建消息拉取系统标记

this.pullAPIWrapper.pullKernelImpl(
    pullRequest.getMessageQueue(),
    subExpression,
    subscriptionData.getExpressionType(),
    subscriptionData.getSubVersion(),
    pullRequest.getNextOffset(),
    this.defaultMQPushConsumer.getPullBatchSize(),
    sysFlag,
    commitOffsetValue,
    BROKER_SUSPEND_MAX_TIME_MILLIS,
    CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
    CommunicationMode.ASYNC,
    pullCallback
);

调用 PullAPIWrapper illKernellmpl 法后与服务端交互

FindBrokerResult findBrokerResult =
    this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
        this.recalculatePullFromWhichNode(mq), false);
if (null == findBrokerResult) {
    this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic());
    findBrokerResult =
        this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(),
            this.recalculatePullFromWhichNode(mq), false);
}

根据 brokerName,BrokerId从 MQClientlnstance 中获取 Broker 地址,在整个RocketMQ的 Broker 的部署结构中,相同名称的 Broker 构成主从结构,其 BrokerId 会不一样,在每次拉取消息后,会给出 个建议,下次拉取从主节点还是从节点拉取

String brokerAddr = findBrokerResult.getBrokerAddr();
if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) {
    brokerAddr = computPullFromWhichFilterServer(mq.getTopic(), brokerAddr);
}

如果消息 过滤模式为类过滤, 则需要根据主题名称、 broker 地址找到注册在Broke 上的 FilterServer 地址,从 FilterServer 上拉取消息,否则从 Broker 上拉取消息

3.2消息服务器 找并返回消息

根据消息拉取命令 Co de : RequestCode.PULL MESSAGE

final GetMessageResult getMessageResult =
    this.brokerController.getMessageStore().getMessage(requestHeader.getConsumerGroup(), requestHeader.getTopic(),
        requestHeader.getQueueId(), requestHeader.getQueueOffset(), requestHeader.getMaxMsgNums(), messageFilter);

调用 MessageStore.getMessage 查找消息

GetMessageStatus status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
long nextBeginOffset = offset;
long minOffset = 0;
long maxOffset = 0;
GetMessageResult getResult = new GetMessageResult();
final long maxOffsetPy = this.commitLog.getMaxOffset();
ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);

根据主题名称与队列编号获取消息消费队列

getResult.setStatus(status);
getResult.setNextBeginOffset(nextBeginOffset);
getResult.setMaxOffset(maxOffset);
getResult.setMinOffset(minOffset);

根据主从同 延迟,如果从节点数据包 次拉取的偏移 ,设置下一次拉取任务的 brokerId

3,3.消息拉取害户端处理消息

消息拉取客户端调用人口: MQClientAPIImpl#pullMessageAsync, NettyRemoting-Client 在收到服务器端响应结构后会回到 Pull Callback 的onSuccess或 onException, PullCall-Back 对象在DefaultMQPushConsumerlmpl#pullMessage 中创建

jabaprivate 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. 根据响应结果解码成 PullResultExt 对象,此时只是从网络中读取消息列表到byte[] messageBinary
pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,subscriptionData);

调用 pullAPIWrapper的processPullResult 将消息字节数组解码成消息列表填充msgFoundList ,井对消息进行消息过滤(TAG )模式

processPullResult参数

l ) pullStatus :拉取结果

2 ) nextBeginOffset :下次拉取偏移量

3 ) minOffset :消息队列最小偏移量。

4 ) maxOffset :消息队列最大偏移量。

5 ) msgFoundList 具体拉取的消息列表

3.1正常情况下

拉取结果为 pullStatus.FOUND (找到对应的消息)

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()) {
    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
} 

更新 pullStatus 的下一次拉取偏移量,如果 msgFoundList 为空,PullReqeuest 放入到 PullMessageService的pullRequestQueue ,以便 PullMessag Serivce能唤醒并再次执行消息拉取. 为什么 pullStatus.FOUND, msgFoundList 会为空呢?因为RocketMQ 根据 TAG 消息过滤,在服务端只是验证了 TAG 的has code ,在客户端再次对消息进行过滤 故可能会出现 msgFoundList 为空的情况

boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
    pullResult.getMsgFoundList(),
    processQueue,
    pullRequest.getMessageQueue(),
    dispatchToConsume);

如果msgFoundList不为空, 首先将拉取到的消息存入 ProcessQueue ,然后将拉取到的消息提交到 ConsumeMessageService 中供消费者消费,该方法是一个异步方法,也就是 PullCallBack 将消息提交ConsumeMessageService 中就会立即返回,至于这些消息如何消费, PullCallBack 不关注

if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
        DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
} else {
    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}

将消息提交给消费者线程之后 PullCallBack 将立即返回,可以说本次消息拉取顺利完成,然后根据 PullInterval参数,如果PullInterval>0 ,则等待 pulllnterval 毫秒后将PullRequest 对象放入到 PullMessageServicepull的RequestQueue ,该消息队列的下次拉取即将被激活,达到持续消息拉取,实现准 时拉取消息的效果

3.2异常情况
case NO_NEW_MSG:
case NO_MATCHED_MSG:
    pullRequest.setNextOffset(pullResult.getNextBeginOffset());

    DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);

    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
    break;

如果返回 NO_NEW_MSG没有新消息,NO_MATCHED_MSG(没有匹配消息),则直接使用服务器端校正的偏移 进行下一次消息的拉取 再来 看看服务端是如何校正 Offset.

NO_NEW_MSG对应 GetMessageResult.OFFSET_FOUND _ NULL, GetMessageResult.OFFSET_OVERFLOW_ONE

OFFSET_OVERFLOW_ONE:待拉取 offset 等于消息队列最大的偏移量,如果有新的消息到达, 此时会创建一个新的 ConsumeQueue 文件,按照上一个 ConsumeQueue 的最大偏移量就是下一个文件的起始偏移 ,所以如果按照该 offset 第二次拉取消息时能成功

OFFSET _FOUND _NULL : 根据ConsumeQueue的偏移量没有找到内容,将偏移定位到下一个ConsumeQueue ,其实就是 offse +( 一个ConsumeQueue 含多少个条目=MappedFileSize I 20)

case OFFSET_ILLEGAL:
    log.warn("the pull request offset illegal, {} {}",
        pullRequest.toString(), pullResult.toString());
    pullRequest.setNextOffset(pullResult.getNextBeginOffset());

    pullRequest.getProcessQueue().setDropped(true);
    DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {

        @Override
        public void run() {
            try {
                DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(),
                    pullRequest.getNextOffset(), false);

                DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());

                DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());

                log.warn("fix the pull request offset, {}", pullRequest);
            } catch (Throwable e) {
                log.error("executeTaskLater Exception", e);
            }
        }
    }, 10000);
    break;

如果拉取结果显示偏移量非法,首先将 ProcessQueue的dropped为ture 表示丢弃该消费 队列, ProcessQueue 中拉取的消息将停止消 费,然后根据服务端 下一次校对的偏移量尝试更新消息消费进度( 内存中 ),然后尝试持久化消息消费进度,并将该消息队列从 Rebala cnlmpl 的处理队列中移除,意味着暂停该消息队列的消息拉取,等待下一次消息队列 重新负载

4. 消息拉取长轮询机制分析

RocketMQ 并没有真正实现推模式,而是消费者主动向消息服务器拉取消息, RocketMQ推模式是循环向消息服务端发送消息拉取请求,如果消息消费者向 RocketMQ 发送消息拉取时,消息并未到达消费队列,如果不启用长轮询机制,则会在服务端等待 shortPolling

TimeMills 时间后(挂起)再去判断消息是否已到达消息队列,如果消息未到达则提示消息拉取客户端 PULL_NOT_FOUND (消息不存在),如果开启长轮询模式, RocketMQ 一方面会每5s 轮询检查 一次消息是否可达 ,同时一有新消息到达后立马通知挂起线程再次验证新消息是否是感兴趣的消息,如果是则从 commitlog 文件提取消息返回给消息拉取客户端,否直到挂起超时,超时时间由消息拉取方在消息拉取时封装在请求参数中, PUSH 模式默认为15s, PULL 模式通过 DefaultMQPullConsum r#setBrokerSuspendMaxTim Millis 设置 RocketMQ 通过在 Broker 端配置 longPollingEnable 为true开启长轮询模式

消息拉取时服务端从Commitlog 未找到消息时的处理逻辑如下

private RernotingCommand processRequest(final Channel channel, 
			RernotingCommand request, boolean brokerAllowSuspend) {
		case ResponseCode.PULL_NOT_FOUND:
    if (brokerAllowSuspend && hasSuspendFlag) {
        long pollingTimeMills = suspendTimeoutMillisLong;
        if (!this.brokerController.getBrokerConfig().isLongPollingEnable()) {
            pollingTimeMills = this.brokerController.getBrokerConfig().getShortPollingTimeMills();
        }

        String topic = requestHeader.getTopic();
        long offset = requestHeader.getQueueOffset();
        int queueId = requestHeader.getQueueId();
        PullRequest pullRequest = new PullRequest(request, channel, pollingTimeMills,
            this.brokerController.getMessageStore().now(), offset, subscriptionData, messageFilter);
        this.brokerController.getPullRequestHoldService().suspendPullRequest(topic, queueId, pullRequest);
        response = null;
        break;
    }

I )参数释义

Channel channel :网络通道,通过该通道向消息拉取客户端发送响应结果

RemotingCommand request 消息、拉取请求

boolean brokerAllowSuspend : Broker 端是否支持挂起,处理消息拉取时默认传入 true,表示支持如果未找到消息则挂起,如果该参数为 false ,未找到消息时直接返回客户端消息未找到

  1. 如果brokerAllowSuspend为true,表示支持挂起,则将响应对象response 设置为null,将不会立即向 客户端写入响应, hasSuspendFlag 参数在拉取消息时构建的拉取标记,默认为 true

3 )默认支持挂起,根据是否开启长轮询来决定挂起方式, 如果支持长轮询模式,挂起时时间来源于请求参数, PUSH 模式默 15s, PULL 模式通过DefaultMQPulIConsumer#brokerSuspenMaxTimeMilli 设置,默认20s, 然后创建拉取任务 PullRequest 并提交PulIRequestHoldServic 线程中

RocketMQ 轮询机制由 两个线程共 来完成

1 ) PullRequestHoldService 每隔 5s 重试一次

2 ) DefaultMessageStore#ReputMessageService 每处理一次重新拉取, Thread.sleep ( 1 ), 继续下一次检查

5. PullRequestHoldService 线程详解
public void suspendPullRequest(final String topic, final int queueId, final PullRequest pullRequest) {
    String key = this.buildKey(topic, queueId);
    ManyPullRequest mpr = this.pullRequestTable.get(key);
    if (null == mpr) {
        mpr = new ManyPullRequest();
        ManyPullRequest prev = this.pullRequestTable.putIfAbsent(key, mpr);
        if (prev != null) {
            mpr = prev;
        }
    }

    mpr.addPullRequest(pullRequest);
}

根据消息主题与消息队列构建为key, ConcurrentMap <String /topic@queueld / ManyPullRequest> pullRequestTable 中获取该主题@ 队列对应的 ManyPullRequest ,通过ConcurrentMap 并发并发特 ,维护主题@ 队列的 ManyPullRequest ,然后将 PullRequest放入

ManyPullRequest. ManyPullRequest 对象内部持有一个 PullRequest 列表,表示同一主题队列的累积拉取消息任务

@Override
public void run() {
    log.info("{} service started", this.getServiceName());
    while (!this.isStopped()) {
        try {
            if (this.brokerController.getBrokerConfig().isLongPollingEnable()) {
                this.waitForRunning(5 * 1000);
            } else {
                this.waitForRunning(this.brokerController.getBrokerConfig().getShortPollingTimeMills());
            }

            long beginLockTimestamp = this.systemClock.now();
            this.checkHoldRequest();
            long costTime = this.systemClock.now() - beginLockTimestamp;
            if (costTime > 5 * 1000) {
                log.info("[NOTIFYME] check hold request cost {} ms.", costTime);
            }
        } catch (Throwable e) {
            log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }

    log.info("{} service end", this.getServiceName());
}

如果开启长轮询,每 5s 尝试一次,判断新消息是否到达, 如果未开启长轮询,则默认等待 ls 再次尝试,可以通过BrokerConfig#shortPollingTimeMills 改变等待时间

private void checkHoldRequest() {
    for (String key : this.pullRequestTable.keySet()) {
        String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR);
        if (2 == kArray.length) {
            String topic = kArray[0];
            int queueId = Integer.parseInt(kArray[1]);
            final long offset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId);
            try {
                this.notifyMessageArriving(topic, queueId, offset);
            } catch (Throwable e) {
                log.error("check hold request failed. topic={}, queueId={}", topic, queueId, e);
            }
        }
    }
}

遍历拉取任务表,根据主题与队列获取消息消费队列最大偏移量,如果该偏移量大于待拉取偏移量, 说明有新的消息到达,调用 notifyMessageArriving 触发消息拉取

if (newestOffset > request.getPullFromThisOffset()) {
    boolean match = request.getMessageFilter().isMatchedByConsumeQueue(tagsCode,
        new ConsumeQueueExt.CqExtUnit(tagsCode, msgStoreTime, filterBitMap));
    // match by bit map, need eval again when properties is not null.
    if (match && properties != null) {
        match = request.getMessageFilter().isMatchedByCommitLog(null, properties);
    }

    if (match) {
        try {
            this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
                request.getRequestCommand());
        } catch (Throwable e) {
            log.error("execute request when wakeup failed.", e);
        }
        continue;
    }
}

如果消息队列的最大偏移量大于待拉取偏移量 ,如果消息匹配 调用 executeRequest When Wakeup 将消息返回给消息拉取客户端,否则等待 一次尝试。

if (System.currentTimeMillis() >= (request.getSuspendTimestamp() + request.getTimeoutMillis())) {
    try {
        this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
            request.getRequestCommand());
    } catch (Throwable e) {
        log.error("execute request when wakeup failed.", e);
    }
    continue;
}

如果挂起超时时间超时,则不继续等待直接返回客户消息未找到

final RemotingCommand response = PullMessageProcessor.this.processRequest(channel, request, false);

这里 的核心又回到长轮询的入口代码了 ,其核心brokerAllowSuspend为false ,表示不支持拉取线程挂起,即 根据偏移量无法获取消息时将 不挂起线程等待新消息到来,直接返回未找到消息

回想一下,如果当开启了长轮询机制 PullRequestHoldService线程会每隔 5s 被唤醒去尝试检查是否有新消息到来直到超时,如果被挂起, 需要等待5s,消息拉取实时性比较差,为了避免 RocketMQ 另外一种机制: 当消息到达时唤醒挂起线程触发一次检查

6. DefaultMessageStore$ReputMessageService 详解

ReputMessageService 线程主要是根据 Commitlog 将消息转发到 ConsumQueue 和Index等文件

if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
    && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
    DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
        dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
        dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
        dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
}

新消息到达 CommitLog 时, ReputMessageService 线程 将消息转发给 ConsumeQueue和IndexFile ,如 Broker 端开启了长轮询 且角色主节点,则最终将调用 Pull-RequestHoldService 线程 notifyMessageArriving 方法挂起线程,判断当前消息队列最大偏移量是否大于待拉取 便移量, 大于则 拉取消息 ,长轮询模式使得消息拉取能实现准时

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值