一、引言
咱们书接上文,前面我们了解了消息发到到Broker后是如何存储在CommitLog里面的,那么接下来看看消费端是如何让拉取消费消息的,Broker端又是如何管理的,让我们带着这个两个问题来看看RocketMQ是如何解决这两个问题的。
二、消息拉取
2.1 PullMessageProcessor拉取消息处理组件
说到消息的拉取,我们就不得不提到PullMessageProcessor拉取消息处理组件,了解过RocketMQ源码的小伙伴都知道,它里面是由多个组件去共同管理的,每个组件都承担了相应的功能,因此我们只要把每个组件的功能搞清楚然后把他们串起来,这样对我们了解和学习源码帮助都是很大的,为了更清楚的了解消息拉取的过程,我们就从PullMessageProcessor这个组件为突破口,先来看一下这个类:
// 拉取消息处理组件
public class PullMessageProcessor extends AsyncNettyRequestProcessor implements NettyRequestProcessor
现在大家应该就一目了然了,也就明白了为什么要以这个类为突破口去看了,因为它继承我们netty相关的类,而RocketMQ底层的通信就使用的是netty。
消费端要拉取消息,会将消息通过netty发送给拉取消息处理组件,这个组件接收到请求之后就会处理我们的请求,接下来我们看看具体处理拉取消息请求过程:
- 查询当前这个请求是要拉取哪个topic的数据
- 然后根据消费组名称去消费者管理组件(ConsumerManager)里面去获取到你的消费组的信息
-
从消费组订阅的topic里的订阅数据获取出来
-
找消息存储组件查询到这一次我要拉取的消息
具体消息存储组件获取消息,查看RocketMQ消息存储源码刨析第五个小节
- 根据消息存储组件返回的结果,大致可以分为三种结果:成功获取到消息状态为success,返回值不为空状态码为OFFSET_FOUND_NULL,第三种情况就是返回值为空
- 如果成功查询到消息状态码为success,则将消息返回给客户端
- 返回值不为空状态码为OFFSET_FOUND_NULL,则将我们的请求挂起
- 返回值为空程序抛出异常
接下来我们主要看看第二种状态请求挂起这种状态。
2.2 PullRequestHoldService 消费拉取hold服务
要搞清楚如何将我们的请求挂起,就不得不说PullRequestHoldService 这个服务组件,接下来我们就来揭开这个组件的神秘面纱。
说起PullRequestHoldService 这个组件它的本质其实就是一个线程,它继承了ServiceThread:
// 消费拉取hold服务
public class PullRequestHoldService extends ServiceThread{
}
既然这个组件它本质是一个线程,那我们就不得不看一下它最重要的一个方法run(),到底干了一些什么事情:
-
当前是否启用long polling (默认情况下是开启的),长轮询,一个请求过来了挂起,不要返回
-
检查当前挂起的请求,把拉取的topic+queue的最大offset查一下,通知一下新消息来了,你不用挂起了,可以拿到消息走了
在这里大家思考一个问题,RocketMQ是如何感知有新的消息到来呢?
public void notifyMessageArriving(
final String topic,
final int queueId,
final long maxOffset, // 最大的offset已经到了哪儿去了
final Long tagsCode,
long msgStoreTime,
byte[] filterBitMap,
Map<String, String> properties) {
String key = this.buildKey(topic, queueId);
// 拿到当前挂起等待topic@queue里的消息
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; // topic@queue里最新到达的消息最大的offset,新消息的offset
if (newestOffset <= request.getPullFromThisOffset()) { // 如果说最新的消息是小于等于拉取请求里要拉取的起始offset
// 查询消息存储组件里的topic@queue最大的offset,查询出来了以后设置为topic@queue里的最大offset
newestOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(
topic, queueId
);
}
// 但凡说最新的消息大于了拉取请求里起始消息位置
if (newestOffset > request.getPullFromThisOffset()) {
// 根据我们的消费队列判断一下是否匹配,新到达的消息tags是否跟拉取请求里的tags是匹配的
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) {
// 继续判断一下跟commitlog相比是否匹配
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;
}
}
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);
}
}
}
}
上面这段代码就成功为我们揭秘了RocketMQ是如何感知有新的消息到来,我们来看一下它具体的实现流程:
-
到当前挂起等待topic@queue里的消息
-
topic@queue里最新到达的消息最大的offset,新消息的offset
-
如果说最新的消息是小于等于拉取请求里要拉取的起始offset,查询消息存储组件里的topic@queue最大的offset,查询出来了以后设置为topic@queue里的最大offset
-
如果最新的消息大于了拉取请求里起始消息位置,继续判断一下跟commitlog相比是否匹配
-
接着去PullMessageProcessor拉取消息处理组件唤起请求拉取消息,返回给客户端
三、小结
最后用一张流程图来更加清楚的看一下消息拉取的过程