使用rocketmq的大体消息发送过程如下:

在前面已经分析过MQ的broker接收生产者客户端发过来的消息的过程,此文主要讲述订阅者获取消息的过程,或者说broker是怎样将消息传递给消费者客户端的,即上面时序图中拉取消息(pull message)动作。。

1. 如何找到入口(MQ-broker端)

分析一个机制或者功能时,我们首先希望的是找到入口,前一篇我们是通过端口号方式顺藤摸瓜的方式找到了入口。但是此篇略微不同,涉及到consumer客户端与broker的两边分析,最终发现逻辑还是比较绕的,主要有很多异步动作,还有循环调用(当然不是一个线程上,而且中间有阻塞队列缓冲),这对调试式分析代码造成了一些不方便。
回到正题,怎么找到这里入口?在具备上篇分析的基础上,我直接分析broker的代码,broker接收消息的时候是靠SendMessageProcessor,那么在消息传递给消费端的时候是不是也是靠某个processor完成的?据这些processor的命名观察,猜测PullMessageProcessor比较像。
为了验证这一想法,注释掉BrokerController中使用这个processor的地方,再重新测试,发现consumer就收不到producer发过来的消息了。想法初步正确。

2. 调试PullMessageProcessor(MQ-broker端)

RemotingServer在注册processor的时候,是根据RequestCode进行注册的。
PullMessageProcessor 对应的RequestCode的PULL_MESSAGE,即11。猜测:consumer客户端不断(或定时轮询,或循环调用,或其他方式)发起pull message请求给broker,broker会处理这些请求,后面会验证这个猜测。
PullMessageProcessor在注册的时候对应的线程池是pullMessageExecutor,线程池的corePoolSize以及maxPoolSize都可以在broker中进行config,字段名是pullMessageThreadPoolNums。默认值16+处理器个数*2。

3. 哪里发送了RequestCode为PULL_MESSAGE的请求(consumer客户端)

通过全局搜索,很容易发现是MQClientAPIImpl.pullMessage422行,发送了PULL_MESSAGE类型的请求。
加断点(consumer客户端需要debug方式启动),看调用堆栈。

很容易发现 是 PullMessageService.run()发出了PULL_MESSAGE的request。 run的代码如下:

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

在线程没有停止的情况下,一直循环发拉取消息的请求,过程中被pullRequestQueue阻塞队列阻塞。
分析谁向pullRequestQueue put了元素?是PullMessageService.executePullRequestImmediately(PullRequest)方法。
谁调了上面的方法,同样断点分析,调用堆栈如下图:

划蓝色线的ResponseFuture地方,是阿里对这种通过发送网络请求调用后还能回调回来的一个特性封装,值得学习。 划红色线的 MQClientAPIImpl$2地方是在处理业务逻辑,位于方法pullMessageAsync(String, RemotingCommand, long, PullCallback)内。此处又是一个异步。

private void pullMessageAsync(//
final String addr,// 1
final RemotingCommand request,//
final long timeoutMillis,//
final PullCallback pullCallback//
) throws RemotingException, InterruptedException {
this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
@Override
public void operationComplete(ResponseFuture responseFuture) {
RemotingCommand response = responseFuture.getResponseCommand();
if (response != null) {
try {
PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response);
assert pullResult != null;
pullCallback.onSuccess(pullResult);// 457行对应此处
}
catch (Exception e) {
pullCallback.onException(e);
}
}
else {
//... 省略
}
}
});
}

4. 调试MQClientAPIImpl.pullMessageAsync(consumer客户端)

谁调用了MQClientAPIImpl.pullMessageAsync?
449行打断点,堆栈如下:

发现又回到上面的PullMessageService的run中。:-(
回头去看看3段落中pullCallback.onSuccess(pullResult);// 457行对应此处
这一行代码,跟进去就会发现玄机,在这里面又调用了PullMessageService.executePullRequestImmediately(PullRequest)方法。 是一个匿名内部类,位于DefaultMQPushConsumerImpl.pullMessage(PullRequest)中。这个方法太长,贴一些简略的,

final long beginTimestamp = System.currentTimeMillis();

PullCallback pullCallback = new PullCallback() {
@Override
public void onSuccess(PullResult pullResult) {
if (pullResult != null) {
pullResult =
DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(
pullRequest.getMessageQueue(), pullResult, subscriptionData); switch (pullResult.getPullStatus()) {
case FOUND:
//...省略 long firstMsgOffset = Long.MAX_VALUE;
if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
else {
//...省略 if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
}
else {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
} //...省略
break;
case NO_NEW_MSG:
//...省略 DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
break;
case NO_MATCHED_MSG:
//...省略 DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
break;
case OFFSET_ILLEGAL:
//...省略

上述代码基本上很清楚地看出来拉取代码的发起逻辑了。在case FOUND分支打个断点,当你从producer的客户端发一条消息过来的时候,能看到断点被命中。当没有producer没有发消息的时候,一直走的就是case NO_NEW_MSG分支。

5. 上面分析的对吗?

不全对。为什么?回去看段落3的[1]标注出,关于是谁调用了PullMessageService.executePullRequestImmediately(PullRequest)方法。

其实不止是段落3分析的那样,还有RebalanceImpl.updateProcessQueueTableInRebalance(String, Set)417行发起的调用。
为什么会想到这个问题?因为按段落3 的分析,调用executePullRequestImmediately方法的入参是 PullRequest,但是上面分析是"循环"调用,那么最初始的这个PullRequest是哪里构造的?

带着这个疑问就不难发现肯定还有其他调用了executePullRequestImmediately方法。于是搜索,加断点,发现RebalanceImpl.updateProcessQueueTableInRebalance会调用。
其实通过搜索 new PullRequest 关键字也是很容易找到上述调用的地方。

谁对RebalanceImpl.updateProcessQueueTableInRebalance发起了调用?
观察调用栈:

Thread [RebalanceService] (Suspended (breakpoint at line 417 in RebalanceImpl))
RebalancePushImpl(RebalanceImpl).updateProcessQueueTableInRebalance(String, Set) line: 417
RebalancePushImpl(RebalanceImpl).rebalanceByTopic(String) line: 321 RebalancePushImpl(RebalanceImpl).doRebalance() line: 248
DefaultMQPushConsumerImpl.doRebalance() line: 250
MQClientInstance.doRebalance() line: 925
RebalanceService.run() line: 49 Thread.run() line: 695

RebalancePushImpl(RebalanceImpl).updateProcessQueueTableInRebalance(String, Set) line: 417

this.dispatchPullRequest(pullRequestList); 该方法对push形式会调用PullMessageService.executePullRequestImmediately。至此,疑问基本解决。

这个堆栈要在consumer启动时会发现,整个堆栈发生在线程 Thread [RebalanceService]

// --RebalanceService  run --
@Override
public void run() {
log.info(this.getServiceName() + " service started"); while (!this.isStoped()) {
this.waitForRunning(WaitInterval);// WaitInterval = 10s
this.mqClientFactory.doRebalance();// MQClientInstance.doRebalance()
} log.info(this.getServiceName() + " service end");
} //-- MQClientInstance.doRebalance() --
public void doRebalance() {
for (String group : this.consumerTable.keySet()) {
MQConsumerInner impl = this.consumerTable.get(group);
if (impl != null) {
try {
impl.doRebalance();// DefaultMQPushConsumerImpl.doRebalance()
} catch (Exception e) {
log.error("doRebalance exception", e);
}
}
}
} // -- DefaultMQPushConsumerImpl.doRebalance --
@Override
public void doRebalance() {
if (this.rebalanceImpl != null) {
this.rebalanceImpl.doRebalance();
}
}
// ......

此处RebalanceImpl的实现是RebalancePushImpl
根据上述线程名可以发现是RebalanceService这个类拉起了上面的RebalanceImpl.updateProcessQueueTableInRebalance

谁拉起了RebalanceService
看下面调用堆栈一目了然

Thread [main] (Suspended (breakpoint at line 185 in com.alibaba.rocketmq.client.impl.factory.MQClientInstance))
owns: com.alibaba.rocketmq.client.impl.factory.MQClientInstance (id=40)
com.alibaba.rocketmq.client.impl.factory.MQClientInstance.start() line: 185
com.alibaba.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl.start() line: 720
com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer.start() line: 365
org.simonme.rocketmq.demo.ConsumerDemo.main(java.lang.String[]) line: 55 // DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("testgroup");

因为我们针对该group设置的consumer就是Push的,即DefaultMQPushConsumer。官方提供的example工程中com.alibaba.rocketmq.example.quickstart.Consumer也是用的push。
引发问题:
1. 应该是可以针对不同的group设置不同形式的(pull or push)的consumer? 2. 不同的client实例订阅同一个group的同一个tag/不同tag会出现怎样的情况?是否还可以设置不同的获取消息方式(push or pull)

那么疑问又来了

如果此处我们设置的不是DefaultMQPushConsumer,而是DefaultMQPullConsumer,那么刚本节(第5节)开头那个问题怎么解决

尝试一下
先看Pull形式的Consumer的例子

在PullMessageService.executePullRequestImmediately(PullRequest)方法处加上断点并调试该demo,就会发现该方法根本不会被调用。因为dispatchPullRequest方法中针对pull模式的实现为空方法体,根本没有发起PullMessageService.executePullRequestImmediately调用。那么上面的疑问就解决了。

6. 分析PullMessageProcessor(MQ-broker端)

入口方法是processRequest
先做一系列的检查,然后获取消息,检查涉及的要点如下图:

获取消息分析
1. 根据offset查询到SelectMapedBufferResult实例。

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

这个GetMessageResult中有SelectMapedBufferResult的List实例。

这个地方体现了零拷贝。消息落地磁盘的时候,是从file map出来的内存buffer,此时消费消息的时候无需再读取文件,而是直接读取map出来的buffer!当然如果堆积消息过多,内存中已经放不下的时候,就需要从磁盘上读取了。 这是rocketmq性能比较好的原因之一。 通常说的零拷贝是指系统态无需拷贝到用户态,即针对大文件拷贝常用的sendfile操作。此处不同于这个概念。

根据GetMessageResult取消息,没太多复杂的,如果发现有消息则走如下分支

switch (getMessageResult.getStatus()) {
case FOUND:
response.setCode(ResponseCode.SUCCESS); // 消息轨迹:记录客户端拉取的消息记录(不表示消费成功)
if (this.hasConsumeMessageHook()) {
// 执行hook
ConsumeMessageContext context = new ConsumeMessageContext();
context.setConsumerGroup(requestHeader.getConsumerGroup());
context.setTopic(requestHeader.getTopic());

rocketmq源码分析3-consumer消息获取的更多相关文章

  1. rocketmq源码分析2-broker的消息接收

    broker消息接收,假设接收的是一个普通消息(即没有事务),此处分析也只分析master上动作逻辑,不涉及ha. 1. 如何找到消息接收处理入口 可以通过broker的监听端口10911顺藤摸瓜式的 ...

  2. rocketmq源码分析4-事务消息实现原理

    为什么消息要具备事务能力 参见还是比较清晰的.简单的说 就是在你业务逻辑过程中,需要发送一条消息给订阅消息的人,但是期望是 此逻辑过程完全成功完成之后才能使订阅者收到消息.业务逻辑过程 假设是这样的: ...

  3. RocketMQ源码分析之从官方示例窥探:RocketMQ事务消息实现基本思想

    摘要: RocketMQ源码分析之从官方示例窥探RocketMQ事务消息实现基本思想. 在阅读本文前,若您对RocketMQ技术感兴趣,请加入RocketMQ技术交流群 RocketMQ4.3.0版本 ...

  4. RocketMQ 源码分析 —— Message 发送与接收

    1.概述 Producer 发送消息.主要是同步发送消息源码,涉及到 异步/Oneway发送消息,事务消息会跳过. Broker 接收消息.(存储消息在<RocketMQ 源码分析 —— Mes ...

  5. 【RocketMQ源码分析】深入消息存储(2)

    前文回顾 CommitLog篇 --[RocketMQ源码分析]深入消息存储(1) MappedFile篇 --[RocketMQ源码分析]深入消息存储(3) 前文说完了一条消息如何被持久化到本地磁盘 ...

  6. 【RocketMQ源码分析】深入消息存储(3)

    前文回顾 CommitLog篇 --[RocketMQ源码分析]深入消息存储(1) ConsumeQueue篇 --[RocketMQ源码分析]深入消息存储(2) 前面两篇已经说过了消息如何存储到Co ...

  7. RocketMQ源码详解 | Consumer篇 · 其一:消息的 Pull 和 Push

    概述 当消息被存储后,消费者就会将其消费. 这句话简要的概述了一条消息的最总去向,也引出了本文将讨论的问题: 消息什么时候才对被消费者可见? 是在 page cache 中吗?还是在落盘后?还是像 K ...

  8. RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)

    在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...

  9. 【RocketMQ源码分析】深入消息存储(1)

    最近在学习RocketMQ相关的东西,在学习之余沉淀几篇笔记. RocketMQ有很多值得关注的设计点,消息发送.消息消费.路由中心NameServer.消息过滤.消息存储.主从同步.事务消息等等. ...

  10. Spring AMQP 源码分析 06 - 手动消息确认

    ### 准备 ## 目标 了解 Spring AMQP 如何手动确认消息已成功消费 ## 前置知识 <Spring AMQP 源码分析 04 - MessageListener> ## 相 ...

随机推荐

  1. div嵌套时,子元素设置margin-top失效问题

    这是因为父元素的padding设置为0时所产生的bug,它自动将margin-top提升到了父元素上,所以此时我们所设置的margin-top自动就到父元素上了,解决方案: 1.给父元素添加一个pad ...

  2. 初识SeekBar

    SeekBar拖动条,是Progress的间接子类 <SeekBar android:id="@+id/seekBar1" android:layout_width=&quo ...

  3. $'\r': command not found 或者 syntax error: unexpected end of file 或者 syntax error near unexpected token `$'\r''

    执行shell脚本如果报如下错误: syntax error near unexpected token `$'\r'' syntax error: unexpected end of file $' ...

  4. Outlook Web App 客户端超时设置

    这篇文章我们讨论一下,OWA 2013在公共和私人的电脑是如何启用和配置. Exchange 2013 Outlook Web App (OWA) 登录页不再允许用户选择无论他们正在使用公共的或私人的 ...

  5. WinForm 公共控件和属性

    Button  按钮 布局 AutoSize 内容超出部分是否扩展到适应尺寸大小 Location  位置坐标 Size   控件大小 行为 Enabled   控件是否启用 visible   控件 ...

  6. java面试题(杨晓峰)---第五讲String、StringBuffer、StringBuilder有什么区别?

    线程 字符 操作频繁度 1 String (1)String的创建机制 由于String在java世界中使用过于频繁,java为了避免在一个系统中产生大量重复的String对象,引入了字符串常量池,其 ...

  7. Android(java)学习笔记106:Android设置文本颜色的4种方法

    1. Android设置文本颜色的4种方法: (1)利用系统自带的颜色类: tv.setTextColor(android.graphics.Color.RED); (2)数字颜色表示: tv.set ...

  8. 【转】 iOS学习之NSBundle介绍和使用

    bundle是一个目录,其中包含了程序会使用到的资源.这些资源包含了如图像,声音,编译好的代码,nib文件(用户也会把bundle称为plug-in).对应bundle, cocoa提供了类NSBun ...

  9. hd - MFM/IDE 硬盘设备

    描述 DESCRIPTION hd* 开头的设备是以裸模式(raw mode)访问MFM/IDE类型硬盘的块设备. 第一个IDE驱动控制器上的主盘(主设备号3)是 hda ;从盘是 hdb. 第二个I ...

  10. vue 文件流下载xlsx 功能实现

    downLoadFile (url, name) { this.xhr = new XMLHttpRequest() this.xhr.open('GET', url, true) this.xhr. ...