使用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. Java实现的断点续传功能

    代码中已经加入了注释,需要的朋友可以直接参考代码中的注释.下面直接上功能实现的主要代码: import java.io.File; import java.io.FileNotFoundExcepti ...

  2. 洛谷P3959 宝藏(模拟退火乱搞)

    题意 题目链接 题面好长啊...自己看吧.. Sol 自己想了一个退火的思路,没想到第一次交85,多退了几次就A了哈哈哈 首先把没用的边去掉,然后剩下的边从小到大排序 这样我们就得到了一个选边的序列, ...

  3. PC端和手机端页面的一丢丢区别

    <!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8 ...

  4. 深入理解Java虚拟机--个人总结

    JVM内存区域 我们在编写程序时,经常会遇到OOM(out of Memory)以及内存泄漏等问题.为了避免出现这些问题,我们首先必须对JVM的内存划分有个具体的认识.JVM将内存主要划分为:方法区. ...

  5. 学习Linux的好网站

    http://www.linuxcommand.org/ 很不错的学习shell和script的教程.包含:Learning the shell 和 writing shell scripts 两个内 ...

  6. Lua与游戏的不解之缘

    本文转载自秦元培博客:blog.csdn.net/qinyuanpei 一.什么是Lua? Lua 是一个小巧的脚本语言,巴西里约热内卢天主教大学里的一个研究小组于1993年开发,其设计目的是为了嵌入 ...

  7. JavaScript 获取对象中第一个属性

    使用 Object.keys(object) 可以取出属性名为数组,但会打乱顺序 严格意义上对象中是只有映射关系而没有顺序的,但是在存储结构里是有顺序的,如果想获取存储结构里的第一个属性可以使用for ...

  8. Java中的后台线程和join方法

    /*守护线程(后台线程):在一个进程中如果只剩下 了守护线程,那么守护线程也会死亡. 需求: 模拟QQ下载更新包. 一个线程默认都不是守护线程. */ public class Demo extend ...

  9. Bootstrap历练实例:默认的列表组

    Bootstrap 列表组 本章我们将讲解列表组.列表组件用于以列表形式呈现复杂的和自定义的内容.创建一个基本的列表组的步骤如下: 向元素 <ul> 添加 class .list-grou ...

  10. FTPClient:enterLocalPassiveMode()方法简单说明

    问题:在Java程序中,使用FTPClient下载FTP文件的时候,可以下载到FTP服务器上的文件夹,但是里面的文件没有下载到本地. 分析:这个涉及到FTP在使用的过程中,客户端和服务端连接过程中,端 ...