RocketMQ消息拉取consumer端实现

1.consume实现

        在Apache RocketMQ中,消费者(Consumer)通过拉取(Pull)或推送(Push)模式来获取消息。但是在传统Push模式下可能会造成consumer端压力过大或处理不过来的场景,所以在RocketMQ统一采取手动Pull模式。

2.PullRequest初始化

        RocketMQ对每个Topic下的每个Queue维护了一个PullRequest,每个PullRequest只有在本次处理完毕后才能进行下一次使用,主要使用到了CallBack机制。初始化是在Rebalance中,由于在重平衡过程中Queue可能出现新增或删除的场景,所以需要在Rebalance中维护。每个Consumer在创建过程中会创建一个MQClientInstance(之前版本中同一个JVM里只会创建一个),每个PullRequest中有processQueue表示结果处理的Queue,consumer消费的消息。MessageQueue表示需要处理的Queue

org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#start
删除无效代码
public synchronized void start() throws MQClientException {
        switch (this.serviceState) {
            case CREATE_JUST:
                
                this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
// 消息拉取的client
                if (this.pullAPIWrapper == null) {
                    this.pullAPIWrapper = new PullAPIWrapper(
                            mQClientFactory,
                            this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
   
// 消费消息的offset,默认Cluster模式,从Broker拉取
                if (this.defaultMQPushConsumer.getOffsetStore() != null) {
                    this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
                } else {
                    switch (this.defaultMQPushConsumer.getMessageModel()) {
                        case BROADCASTING:
                            this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                            break;
                        case CLUSTERING:
                            this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                            break;
                        default:
                            break;
                    }
                   
        }
        this.updateTopicSubscribeInfoWhenSubscriptionChanged();
        this.mQClientFactory.checkClientInBroker();
// 启动后立马进行一次重平衡,运行过程中如果consumer新增减少都会触发重平衡,broker扩容topic队列也会触发
        if (this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()) {
            this.mQClientFactory.rebalanceImmediately();
        }
    }

看一下重平衡做的事,重平衡是由org.apache.rocketmq.client.impl.consumer.RebalanceService线程执行,每20s或收到事件执行都会,事件位置org.apache.rocketmq.client.impl.ClientRemotingProcessor#notifyConsumerIdsChanged

org.apache.rocketmq.client.impl.consumer.RebalanceImpl 相关代码

    public boolean doRebalance(final boolean isOrder) {
        boolean balanced = true;
        // 获取订阅的topic列表
        Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
        if (subTable != null) {
            for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
                final String topic = entry.getKey();
                try {
                    // 判断是不是有需要client重平衡,如果不是说明需要broker帮助重平衡-- 默认不走这里
                    if (!clientRebalance(topic) && tryQueryAssignment(topic)) {
                        boolean result = this.getRebalanceResultFromBroker(topic, isOrder);
                        if (!result) {
                            balanced = false;
                        }
                    } else {

                        // 对每个topic做重平衡,计算出当前consumer分配的Queue
                        boolean result = this.rebalanceByTopic(topic, isOrder);
                        if (!result) {
                            balanced = false;
                        }
                    }
                } catch (Throwable e) {
                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                        log.warn("rebalance Exception", e);
                        balanced = false;
                    }
                }
            }
        }
        // 清理不再属于自己的queue,可能有些Queue被分配到了其他consumer
        this.truncateMessageQueueNotMyTopic();

        return balanced;
    }
 

private boolean rebalanceByTopic(final String topic, final boolean isOrder) {
        boolean balanced = true;
        switch (messageModel) {
            case BROADCASTING:// 删除这个代码,默认不会使用广播模式
            case CLUSTERING: {
                // 获取topic对应的queue
                Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
                // 获取topic对应的consumer
                List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
                if (null == mqSet) {
                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                        this.messageQueueChanged(topic, Collections.<MessageQueue>emptySet(), Collections.<MessageQueue>emptySet());
                        log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic);
                    }
                }

                if (null == cidAll) {
                    log.warn("doRebalance, {} {}, get consumer id list failed", consumerGroup, topic);
                }

                if (mqSet != null && cidAll != null) {
                    List<MessageQueue> mqAll = new ArrayList<>();
                    mqAll.addAll(mqSet);
                    // 为什么可以每个consumer自己计算,由于所有consumer使用相同的计算策略,保证所有consumer计算结果相同
                    Collections.sort(mqAll);
                    Collections.sort(cidAll);

                    AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;

                    List<MessageQueue> allocateResult = null;
                    try {
                        allocateResult = strategy.allocate(
                                this.consumerGroup,
                                this.mQClientFactory.getClientId(),
                                mqAll,
                                cidAll);
                    } catch (Throwable e) {
                        log.error("allocate message queue exception. strategy name: {}, ex: {}", strategy.getName(), e);
                        return false;
                    }

                    Set<MessageQueue> allocateResultSet = new HashSet<>();
                    if (allocateResult != null) {
                        allocateResultSet.addAll(allocateResult);
                    }
                    // 依据结果构建ProcessQueue,每个Queue都会持有一个PullRequest
                    boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
                    if (changed) {
                        log.info(
                                "client rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}",
                                strategy.getName(), consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(),
                                allocateResultSet.size(), allocateResultSet);
                        this.messageQueueChanged(topic, mqSet, allocateResultSet);
                    }

                    balanced = allocateResultSet.equals(getWorkingMessageQueue(topic));
                }
                break;
            }
            default:
                break;
        }

        return balanced;
    }

3.PullRequest重复使用

        RocketMQ是在Netty基础上实现,所以在这里使用了事件驱动机制,在从队列中获取消息后,调用Netty发送消息后,org.apache.rocketmq.remoting.netty.NettyRemotingAbstract本地维护一个responseTable。在获取结果后,从map中获取处理的Reponse,根据回调函数函数来执行逻辑。

class NettyClientHandler extends SimpleChannelInboundHandler<RemotingCommand> {

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
    //处理逻辑
            processMessageReceived(ctx, msg);
        }
    }
}

   public void processResponseCommand(ChannelHandlerContext ctx, RemotingCommand cmd) {
        final int opaque = cmd.getOpaque();
        final ResponseFuture responseFuture = responseTable.get(opaque);
        if (responseFuture != null) {
            responseFuture.setResponseCommand(cmd);

            responseTable.remove(opaque);

            if (responseFuture.getInvokeCallback() != null) {
// 执行response的Callback。更具代码找到每个callback的位置很重要,在拉取消息过程中。callback位于PullMessageService中
                executeInvokeCallback(responseFuture);
            } else {
                responseFuture.putResponse(cmd);
                responseFuture.release();
            }
        } else {
            log.warn("receive response, cmd={}, but not matched any request, address={}", cmd, RemotingHelper.parseChannelRemoteAddr(ctx.channel()));
        }
    }

下面我们看看真正执行拉取消息的逻辑

    @Override
    public void run() {
        while (!this.isStopped()) {
            try {
                // 从阻塞队列中获取请求,初始请求是在Rebalance中生成并放入队列中
                MessageRequest messageRequest = this.messageRequestQueue.take();
                if (messageRequest.getMessageRequestMode() == MessageRequestMode.POP) {
                    this.popMessage((PopRequest) messageRequest);
                } else {
                    this.pullMessage((PullRequest) messageRequest);
                }
            } catch (Exception e) {
                logger.error("Pull Message Service Run Method exception", e);
            }
        }
    }


// 删除了无关代码

   public void pullMessage(final PullRequest pullRequest) {
        // 获取pullRequest对应的processor
        final ProcessQueue processQueue = pullRequest.getProcessQueue();
        if (processQueue.isDropped()) {
            log.info("the pull request[{}] is dropped.", pullRequest.toString());
            return;
        }

        pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());

        final MessageQueue messageQueue = pullRequest.getMessageQueue();
        final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(messageQueue.getTopic());
        if (null == subscriptionData) {
            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
            log.warn("find the consumer's subscription failed, {}", pullRequest);
            return;
        }

        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 prevRequestOffset = pullRequest.getNextOffset();
                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());
                            long pullRT = System.currentTimeMillis() - beginTimestamp;
                            DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
                                    pullRequest.getMessageQueue().getTopic(), pullRT);

                            long firstMsgOffset = Long.MAX_VALUE;
                            if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
                                // 如果消息列表为空,则直接拉取下一次消息
                                DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                            } else {
                                // 获取第一条消息的offset
                                firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();

                                DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
                                        pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());
                                // 将消息列表放入processQueue
                                boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
                                // 提交给对应的listener处理,用ConsumeMessageConcurrentlyService举例
                                DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
                                        pullResult.getMsgFoundList(),
                                        processQueue,
                                        pullRequest.getMessageQueue(),
                                        dispatchToConsume);

                                if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
                                    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
                                            DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
                                } else {
                                    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                                }
                            }

                            if (pullResult.getNextBeginOffset() < prevRequestOffset
                                    || firstMsgOffset < prevRequestOffset) {
                                log.warn(
                                        "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",
                                        pullResult.getNextBeginOffset(),
                                        firstMsgOffset,
                                        prevRequestOffset);
                            }

                            break;
                        case NO_NEW_MSG:
                        case NO_MATCHED_MSG: // 没有新消息,或没有匹配的消息 可能被tag过滤掉了 ,这里需要将offset进行移动
                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());

                            DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
                            DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                            break;
                        case OFFSET_ILLEGAL: // offset非法,说明系统有问题,将PullRequest废弃,执行重平衡
                            log.warn("the pull request offset illegal, {} {}",
                                    pullRequest.toString(), pullResult.toString());
                            pullRequest.setNextOffset(pullResult.getNextBeginOffset());

                            pullRequest.getProcessQueue().setDropped(true);
                            DefaultMQPushConsumerImpl.this.executeTask(new Runnable() {

                                @Override
                                public void run() {
                                    try {
                                        DefaultMQPushConsumerImpl.this.offsetStore.updateAndFreezeOffset(pullRequest.getMessageQueue(),
                                                pullRequest.getNextOffset());

                                        DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue());

                                        // removeProcessQueue will also remove offset to cancel the frozen status.
                                        DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());
                                        DefaultMQPushConsumerImpl.this.rebalanceImpl.getmQClientFactory().rebalanceImmediately();

                                        log.warn("fix the pull request offset, {}", pullRequest);
                                    } catch (Throwable e) {
                                        log.error("executeTaskLater Exception", e);
                                    }
                                }
                            });
                            break;
                        default:
                            break;
                    }
                }
            }

上面代码需要结合源码看,需要了解底层的设计以及如何执行的callback,以及netty底层基础技术,这块还是可以学习到很多东西 

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一段Java代码示例,可以拉取RocketMQ队列中所有消息,并从0开始消费: ```java import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; import org.apache.rocketmq.client.consumer.PullResult; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; import java.util.HashMap; import java.util.Map; import java.util.Set; public class RocketMQPullConsumer { public static void main(String[] args) throws Exception { // 消费者组名 String consumerGroupName = "consumer_group_name"; // NameServer地址列表 String namesrvAddr = "localhost:9876"; // 要消费的topic String topic = "topic_name"; // 创建消息拉取消费者实例 DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(consumerGroupName); consumer.setNamesrvAddr(namesrvAddr); consumer.start(); // 获取topic中所有队列 Set<MessageQueue> mqSet = consumer.fetchSubscribeMessageQueues(topic); // 存储每个队列的消费进度 Map<MessageQueue, Long> offsetMap = new HashMap<>(); // 遍历每个队列,从0开始拉取消息 for (MessageQueue mq : mqSet) { long offset = 0L; PullResult pullResult; do { pullResult = consumer.pull(mq, "*", offset, 32); offsetMap.put(mq, pullResult.getNextBeginOffset()); for (org.apache.rocketmq.common.message.MessageExt msg : pullResult.getMsgFoundList()) { // 处理消息的业务逻辑 System.out.printf("%s%n", new String(msg.getBody())); } offset = pullResult.getNextBeginOffset(); } while (pullResult.getMsgFoundList() != null && pullResult.getMsgFoundList().size() > 0); } // 存储每个队列的消费进度 consumer.updateConsumeOffset(offsetMap); consumer.shutdown(); } } ``` 在这段代码中,我们使用了`DefaultMQPullConsumer`来创建一个消息拉取消费者实例。首先,我们调用`fetchSubscribeMessageQueues()`方法获取指定topic中所有队列,并遍历每个队列。在每个队列中,我们从0开始拉取消息,直到没有消息可以拉取为止。在拉取消息的同时,我们可以将消费进度保存到`offsetMap`中,以便下次从上次消费的位置开始消费。最后,我们将消费进度更新到RocketMQ服务器中,并关闭消费者实例。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值