RocketMQ消息拉取

一、引言

咱们书接上文,前面我们了解了消息发到到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拉取消息处理组件唤起请求拉取消息,返回给客户端

三、小结

最后用一张流程图来更加清楚的看一下消息拉取的过程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值