RocketMQ源码解析:长轮询是如何实现的?_长轮询

消费者消费很快

在文章一开始,先抛出一个问题,如果消费端的消费进度赶上生产端的消费进度,那么RocketMQ是怎么处理的?

前面的文章我们聊过RocketMQ是基于拉模式来实现消息消费的,消费端会不断的创建拉取任务(即使没有拉取到消息),这样就会造成Broker端的压力很大

RocketMQ源码解析:长轮询是如何实现的?_kafka_02

为了解决这个问题,RocketMQ采用了长轮询的策略,即Consumer发送拉取请求到Broker端,如果Broker有数据则返回,Consumer端再次拉取。如果Broker端没有数据,不立即返回,而是等待一段时间(默认5s)

  1. 如果在等待的这段时间,有要拉取的消息,则将消息返回,Consumer端再次拉取。
  2. 如果等待超时,也会直接返回,不会将这个请求一直hold住,Consumer端再次拉取

源码解析

拉取消息的请求会交给PullMessageProcessor来处理,当没有拉取到消息时,会将对应的请求封装成PullRequest并放到PullRequestHoldService的pullRequestTable中,并response设置为null,此时broker端不会给consumer端返回任何消息,consumer端就不会重新发起拉取请求

// PullMessageProcessor#processRequest
switch (response.getCode()) {
case ResponseCode.SUCCESS:
// 成功拉取到消息的处理逻辑
break;
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;
}

// 省略部分逻辑
}

PullRequestHoldService会不断查看pullRequestTable中的请求是否需要结束挂起

当开启长轮询的时候,先等待5s,然后再去看是否有新消息

PullRequestHoldService#run

RocketMQ源码解析:长轮询是如何实现的?_请求超时_03

RocketMQ源码解析:长轮询是如何实现的?_kafka_04

PullRequestHoldService会不断轮询每个PullRequest对应的队列来新消息没

  1. 如果有新消息则重新拉取消息,返回给客户端
  2. 当前时间 >= 请求hold时间 + 请求超时时间,也会执行重新拉取的逻辑(brokerAllowSuspend设为false,如果这次还没有消息,也会直接返回哈,避免hold的时间太长)
  3. 如果都不满足,再将PullRequest放入请求队列
public void notifyMessageArriving(final String topic, final int queueId, final long maxOffset, final Long tagsCode,
long msgStoreTime, byte[] filterBitMap, Map<String, String> properties) {
String key = this.buildKey(topic, queueId);
// 拿到topic + queueId 对应的 PullRequest
ManyPullRequest mpr = this.pullRequestTable.get(key);
if (mpr != null) {
List<PullRequest> requestList = mpr.cloneListAndClear();
if (requestList != null) {
List<PullRequest> replayList = new ArrayList<PullRequest>();

for (PullRequest request : requestList) {
long newestOffset = maxOffset;
if (newestOffset <= request.getPullFromThisOffset()) {
newestOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId);
}

// 当前最新的offset大于请求的offset,也就是有消息到来
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 {
// 消息匹配,重新拉取消息,并且将brokerAllowSuspend设置为false,没拉到消息也不会挂起了
this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
request.getRequestCommand());
} catch (Throwable e) {
log.error("execute request when wakeup failed.", e);
}
continue;
}
}

// 当前时间 >= 请求hold时间 + 请求超时时间,也重新拉取
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;
}

// 待拉取的偏移量大于消息消费队列最大偏移量,并且未超时,则将拉取任务重新放入,等待下一次检测
replayList.add(request);
}

if (!replayList.isEmpty()) {
mpr.addPullRequest(replayList);
}
}
}
}

如果定时执行重新拉取的逻辑,可能会造成消息不能及时消费。

为了解决这个问题,当有新的消息到达的时候(ReputMessageService构建ConsumeQueue和IndexFile的时候),也会执行PullRequestHoldService#notifyMessageArriving方法,看是否需要结束挂起

DefaultMessageStore.ReputMessageService#doReput

RocketMQ源码解析:长轮询是如何实现的?_请求超时_05


RocketMQ源码解析:长轮询是如何实现的?_rabbitmq_06

参考博客