Rocketmq5.0源码阅读(一)pull模式

1,342 阅读3分钟

最近在给社区提PR和补充文档的时候跟踪了不少源码,调整了下状态,记录在此一些心得。本人阅读的是rocketmq5.0的源码,目前Rocketmq的Consumer支持三种模式Pop,Push,Pull模式,Producer支持两种模式Push和Pull

ps: 不过最大的动力是为了近期RIP-46 生产者幂等发送的编写做准备

  • POP Consumer 作为 5.0 的一大特性,POP 消费模式展现了一种全新的消费模式。其具备的轻量级,无状态,无队列独占等特点,对于消息积压场景,Streaming 消费场景等都非常友好
  • Pull Consumer 需要手动调用consumer拉消息
  • Push Consumer 只需要提供一个listener即可实现对消息的监听

下面将对Pull模式下的Produder和Consumer进行分析

在源码中找到NamesrvStartup和BrokerStartup 启动main函数 让我们写一个demo

消费者

用默认生产者生成数据 org.apache.rocketmq.example.simple.TestProducer

import org.apache.rocketmq.client.QueryResult;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.remoting.common.RemotingHelper;

public class TestProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
        producer.start();

        for (int i = 0; i < 1; i++)
            try {
                {
                    Message msg = new Message("TopicTest1",
                        "TagA",
                        "key113",
                        "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                    SendResult sendResult = producer.send(msg);
                    System.out.printf("%s%n", sendResult);

                    QueryResult queryMessage =
                        producer.queryMessage("TopicTest1", "key113", 10, 0, System.currentTimeMillis());
                    for (MessageExt m : queryMessage.getMessageList()) {
                        System.out.printf("%s%n", m);
                    }
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        producer.shutdown();
    }
}

类路径 org.apache.rocketmq.example.simple.PullConsumer

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageQueue;

public class PullConsumer {
    // 记录每个队列的消费进度
    private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();

    public static void main(String[] args) throws MQClientException {
        // 1. 创建DefaultMQPullConsumer实例
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("test_consumer");
        // 2. 设置NameServer
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.start();

        // 3. 获取Topic的所有队列
        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest");

        // 4. 遍历所有队列
        for (MessageQueue mq : mqs) {
            System.out.printf("Consume from the queue: %s%n", mq);
            SINGLE_MQ:
            while (true) {
                try {
                    // 5. 拉取消息 输入参数依次为(消息队列,tag消息过滤,消息队列,一次最大拉去消息数量)
                    PullResult pullResult =
                            consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
                    System.out.printf("%s%n", pullResult);
                    // 6. 将消息放入hash表中,存储该队列的消费进度
                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
                    switch (pullResult.getPullStatus()) {
                        case FOUND:  // 找到消息,输出
                            System.out.println(pullResult.getMsgFoundList().get(0));
                            break;
                        case NO_MATCHED_MSG:  // 没有匹配tag的消息
                            System.out.println("无匹配消息");
                            break;
                        case NO_NEW_MSG:  // 该队列没有新消息,消费offset=最大offset
                            System.out.println("没有新消息");
                            break SINGLE_MQ;  // 跳出该队列遍历
                        case OFFSET_ILLEGAL:  // offset不合法
                            System.out.println("Offset不合法");
                            break;
                        default:
                            break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        // 7. 关闭Consumer
        consumer.shutdown();
    }

    /**
     * 从Hash表中获取当前队列的消费offset
     * @param mq 消息队列
     * @return long类型 offset
     */
    private static long getMessageQueueOffset(MessageQueue mq) {
        Long offset = OFFSE_TABLE.get(mq);
        if (offset != null)
            return offset;

        return 0;
    }

    /**
     * 将消费进度更新到Hash表
     * @param mq 消息队列
     * @param offset offset
     */
    private static void putMessageQueueOffset(MessageQueue mq, long offset) {
        OFFSE_TABLE.put(mq, offset);
    }
}

debug参考前一篇的配置方式 545454.PNG

43543543.PNG

6456546645.PNG

  • 实现方式

名词解释: MessageQueue: RocketMQ的所有主题都是由多个队列组成,以此实现队列数量的水平拆分和队列内部的流式存储。

生产者: 发送消息的进程

消费者:消费消息的进程

订阅关系: 订阅关系一致指的是同一个消费者Group ID下所有Consumer实例所订阅的Topic、Tag必须完全一致。如果订阅关系不一致,消息消费的逻辑就会混乱,甚至导致消息丢失。

image.png

  • 获取消息队列 public Set<MessageQueue> fetchSubscribeMessageQueues(String topic)

  • 同步拉取消息 public PullResult pull(MessageQueue mq, String subExpression, long offset, int maxNums, long timeout)

  • 同步阻塞拉取消息 public PullResult pullBlockIfNotFound(MessageQueue mq, String subExpression, long offset, int maxNums)

  • 获取队列的消费Offset public long fetchConsumeOffset(MessageQueue mq, boolean fromStore)

  • 更新消费组Offset public void updateConsumeOffset(MessageQueue mq, long offset)

时序图: www.processon.com/view/link/6…

Rocketmq- DefaultMQPullConsumerImpl的实现逻辑.jpg

看起来 fetchConsumeOffset和updateConsumeOffset的细节还没有完全展开 可能需要在netty的ProcessRemotingRequest逻辑当中 对代码做进一步展开

Continue...QwQ