携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情
1 RocketMQ的消费方式包含Pull和Push两种。
1.1 Pull方式
用户主动Pull消息,自主管理位点,可以灵活的掌握消费进度和速度。需要从代码层精准的控制消费。在新版本中 DefaultLitePullConsumer
代替DefaultMQPullConsumer
成为了默认的pull消费者实现类。
1.2 Push方式
代码接入简单,适合大部分业务场景,DefaultMQPushConsumer
是默认的Push消费者类
2 Pull消费流程
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消费流程
3.1 具体消费步骤是:
3.1.1 初始化Push消费者实例
业务代码初始化DefaultMQPushConsumer
,启动pull服务PullMessageService
。这个服务是个线程服务,不断执行run()
方法拉取已经订阅的Topic的全部消息,并将消息缓存在本地队列中。
3.1.2 消费消息
由消费服务将本地缓存队列中的消息不断放入消费线程池。并异步回调业务消费代码。
3.1.3 保存消费进度
业务代码消费消息后,将消费结果返回给消费服务,再由消费服务将消费进度保存在本地,并由消费进度管理服务定时持久化到本地或者Broker。对于消费失败的消息,客户端处理后发回Broker并告知消费失败。
下边详细说说,消费流程中的消费消息
4 消费消息
4.1 PullMessageService拉取消息
messageRequestQueue
中保存着待拉取的Topic
和Queue
信息,程序会不停的从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)
下篇文章继续讲解。。。