RocketMQ源码解析:长轮询是如何实现的?

在这里插入图片描述

消费者消费很快

在文章一开始,先抛出一个问题,如果消费端的消费进度赶上生产端的消费进度,那么RocketMQ是怎么处理的?

前面的文章我们聊过RocketMQ是基于拉模式来实现消息消费的,消费端会不断的创建拉取任务(即使没有拉取到消息),这样就会造成Broker端的压力很大

在这里插入图片描述

为了解决这个问题,RocketMQ采用了长轮询的策略,即Consumer发送拉取请求到Broker端,如果Broker有数据则返回,Consumer端再次拉取。如果Broker端没有数据,不立即返回,而是等待一段时间(默认5s)

  1. 如果在等待的这段时间,有要拉取的消息,则将消息返回,Consumer端再次拉取。
  2. 如果等待超时,也会直接返回,不会将这个请求一直hold住,Consumer端再次拉取

源码解析

拉取消息的请求会交给PullMessageProcessor来处理,当没有拉取到消息时,会将对应的请求封装成PullRequest并放到PullRequestHoldService的pullRequestTable中,并response设置为null,此时broker端不会给consumer端返回任何消息,consumer端就不会重新发起拉取请求

// PullMessageProcessor#processRequest
switch (response.getCode()) {
    case ResponseCode.SUCCESS:
        // 成功拉取到消息的处理逻辑
        break;
    case ResponseCode.PULL_NOT_FOUND:

        // 没有拉取到消息时,通过长轮询方式拉取消息
        if (brokerAllowSuspend && hasSuspendFlag) {
            long pollingTimeMills = suspendTimeoutMillisLong;
            if (!this.brokerController.getBrokerConfig().isLongPollingEnable()) {
                pollingTimeMills = this.brokerController.getBrokerConfig().getShortPollingTimeMills();
            }

            String topic = requestHeader.getTopic();
            long offset = requestHeader.getQueueOffset();
            int queueId = requestHeader.getQueueId();
            PullRequest pullRequest = new PullRequest(request, channel, pollingTimeMills,
                this.brokerController.getMessageStore().now(), offset, subscriptionData, messageFilter);
            this.brokerController.getPullRequestHoldService().suspendPullRequest(topic, queueId, pullRequest);
            response = null;
            break;
        }

    // 省略部分逻辑
}

PullRequestHoldService会不断查看pullRequestTable中的请求是否需要结束挂起

当开启长轮询的时候,先等待5s,然后再去看是否有新消息
PullRequestHoldService#run
在这里插入图片描述

在这里插入图片描述

PullRequestHoldService会不断轮询每个PullRequest对应的队列来新消息没

  1. 如果有新消息则重新拉取消息,返回给客户端
  2. 当前时间 >= 请求hold时间 + 请求超时时间,也会执行重新拉取的逻辑(brokerAllowSuspend设为false,如果这次还没有消息,也会直接返回哈,避免hold的时间太长)
  3. 如果都不满足,再将PullRequest放入请求队列
public void notifyMessageArriving(final String topic, final int queueId, final long maxOffset, final Long tagsCode,
    long msgStoreTime, byte[] filterBitMap, Map<String, String> properties) {
    String key = this.buildKey(topic, queueId);
    // 拿到topic + queueId 对应的 PullRequest
    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;
                if (newestOffset <= request.getPullFromThisOffset()) {
                    newestOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId);
                }

                // 当前最新的offset大于请求的offset,也就是有消息到来
                if (newestOffset > request.getPullFromThisOffset()) {
                    // 进行消息过滤
                    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) {
                        match = request.getMessageFilter().isMatchedByCommitLog(null, properties);
                    }

                    if (match) {
                        try {
                            // 消息匹配,重新拉取消息,并且将brokerAllowSuspend设置为false,没拉到消息也不会挂起了
                            this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
                                request.getRequestCommand());
                        } catch (Throwable e) {
                            log.error("execute request when wakeup failed.", e);
                        }
                        continue;
                    }
                }

                // 当前时间 >= 请求hold时间 + 请求超时时间,也重新拉取
                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);
            }
        }
    }
}

如果定时执行重新拉取的逻辑,可能会造成消息不能及时消费。

为了解决这个问题,当有新的消息到达的时候(ReputMessageService构建ConsumeQueue和IndexFile的时候),也会执行PullRequestHoldService#notifyMessageArriving方法,看是否需要结束挂起

DefaultMessageStore.ReputMessageService#doReput

在这里插入图片描述
在这里插入图片描述

参考博客

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java识堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值