1、RocketMQ之消费者实例源码分析原理

RocketMQ的主要角色有,轻量级注册中心NameServer、消息存储即流程控制器Broker、消息生产者Producer、消息消费者Consumer。整体架构图如下:
在这里插入图片描述
整体的工作流程是Broker在启动时会向所有NameServer发起注册,NameServer本地利用二级缓存对Broker的路由信息进行存储,两者之间利用心跳机制对注册的信息进行维护。Producer在发送消息时,会先向NameServer获取Broker的路由信息再进行消息的发送。Consumer在消费时同样会先向NameServer获取Broker的路由信息后找到对应的消费队列进行消息的消费,这里的消费方式有推、拉两种模式。

DefaultMQPushConsumer是DefaultMQPushConsumerImpl的包装类,开放给开发人员使用,DefaultMQPushConsumer中的几乎所有的方法内部都是由DefaultMQPushConsumerImpl实现的。这个是典型的门面模式设计模式

核心类之任务类ServiceThread的子类包含:FileWatchService、PullMessageService、RebalanceService。
核心抽象类之RebalanceImpl的子类包含:RebalancePullImpl、RebalancePushImpl。
核心类MQClientInstance类核心属性变量包含:PullMessageService、RebalanceService。
核心类DefaultMQPushConsumerImpl类核心属性变量包含:RebalanceImpl、MQClientInstance、ConsumeMessageService。
核心接口ConsumeMessageService的实现子类:ConsumeMessageConcurrentlyService、ConsumeMessageOrderlyService。

1、DefaultMQPushConsumerImpl

  1. 创建MQClientInstance实例。
  2. 集群模式下RemoteBrokerOffsetStore实现远程维护offset,广播模式LocalFileOffsetStore本地维护offset。
  3. ConsumeMessageService持有自定义消费者监听类。
  4. 注册消费者组ConsumerGroup。
  5. MQClientInstance#start。
public class DefaultMQPushConsumerImpl implements MQConsumerInner {
    
    

    public synchronized void start() throws MQClientException {
    
    
    	this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(defaultMQPushConsumer, 
    																								this.rpcHook);
    	this.consumeMessageService.start();//第三章 队列的定期锁定
    	//Map集合consumerTable注册当前消费者实例
    	boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
    	mQClientFactory.start();
		...
        this.updateTopicSubscribeInfoWhenSubscriptionChanged();//#1
        this.mQClientFactory.checkClientInBroker();//#2
        this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();//#3
        this.mQClientFactory.rebalanceImmediately();//#4
    }
}
  • 步骤1判断broker集群信息是否发生变化。首先通过NameServer地址获取注册在其上的broker地址信息、broker设置的队列个数。其次,根据TopicRouteData分别更新TopicPublishInfo、subscribeInfo中broker地址以及初始化对应数量的队列MessageQueue等信息。
  • 步骤3发送心跳信息。将全部消费者实例、生产者实例通过心跳方式与broker之间进行通信。broker地址是利用步骤1获取得到的。
  • 步骤4立即唤醒RebalanceService中的任务。
public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
    DefaultMQProducer defaultMQProducer) {
    
    
    TopicRouteData topicRouteData;
    if (isDefault && defaultMQProducer != null) {
    
    
        ...
    } else {
    
    
        topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);//#1
    }
    if (topicRouteData != null) {
    
    
        TopicRouteData old = this.topicRouteTable.get(topic);
        boolean changed = topicRouteDataIsChange(old, topicRouteData);
        if (!changed) {
    
    
            changed = this.isNeedUpdateTopicRouteInfo(topic);
        }

        if (changed) {
    
    
            TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
            for (BrokerData bd : topicRouteData.getBrokerDatas()) {
    
    
                this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
            }
            // Update Pub info
            {
    
    
                TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);//#2
                publishInfo.setHaveTopicRouterInfo(true);
                Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
                while (it.hasNext()) {
    
    
                    Entry<String, MQProducerInner> entry = it.next();
                    MQProducerInner impl = entry.getValue();
                    if (impl != null) {
    
    
                        impl.updateTopicPublishInfo(topic, publishInfo);//MQProducerInner#topicPublishInfoTable
                    }
                }
            }
            // Update sub info
            {
    
    
                Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);//#3
                Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
                while (it.hasNext()) {
    
    
                    Entry<String, MQConsumerInner> entry = it.next();
                    MQConsumerInner impl = entry.getValue();
                    if (impl != null) {
    
    
                        impl.updateTopicSubscribeInfo(topic, subscribeInfo);// 更新rebalanceImpl#topicSubscribeInfoTable
                    }
                }
            }
            this.topicRouteTable.put(topic, cloneTopicRouteData);
            return true;
        }
    }
    return false;
}

2、MQClientInstance

public void start() throws MQClientException {
    
    
    synchronized (this) {
    
    
        switch (this.serviceState) {
    
    
            case CREATE_JUST:
                this.serviceState = ServiceState.START_FAILED;
                // If not specified,looking address from name server
                if (null == this.clientConfig.getNamesrvAddr()) {
    
    
                    this.mQClientAPIImpl.fetchNameServerAddr();
                }
                // Start request-response channel 
                this.mQClientAPIImpl.start();//#1 启动netty RPC功能
                // Start various schedule tasks
                this.startScheduledTask();//#2 开启各种定时任务
                // Start pull service
                this.pullMessageService.start();//#3 开启从broker端拉取消息的任务
                // Start rebalance service
                //#4 开启消费者实例分配队列的任务,从 consumerTable 中获取全部消费者实例,根据指定的分配策略调整消费者实例监听的队列
                this.rebalanceService.start();
                // Start push service
                this.defaultMQProducer.getDefaultMQProducerImpl().start(false);// 调用DefaultMQProducerImpl#start
                this.serviceState = ServiceState.RUNNING;
                break;
            case START_FAILED:
            default:
                break;
        }
    }
}

3、ConsumeMessageOrderlyService定期锁定队列

定期锁定当前实例被分配的队列信息。

针对MessageListenerConcurrently如果通过设置消费线程池数量为1的方式控制顺序消费,可能导致往后消息消费的阻塞。

public void start() {
    
    
    if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) {
    
    
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                ConsumeMessageOrderlyService.this.lockMQPeriodically();
            }
        }, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS);//延迟一秒,间隔20秒
    }
}

RebalanceImpl.lockAll()方法对所有broker上的当前topic的MessageQueue进行锁定(所谓锁定,就是把MessageQueue对应的ProcessQueue的成员变量locked设置为true),如果锁定失败,将暂停当前MessageQueue的拉取,直到下个定时任务重新锁定。

public void lockAll() {
    
    
    HashMap<String, Set<MessageQueue>> brokerMqs = this.buildProcessQueueTableByBrokerName();//#1
    Iterator<Entry<String, Set<MessageQueue>>> it = brokerMqs.entrySet().iterator();
    while (it.hasNext()) {
    
    
        Entry<String, Set<MessageQueue>> entry = it.next();
        final String brokerName = entry.getKey();
        final Set<MessageQueue> mqs = entry.getValue();
        if (mqs.isEmpty())
            continue;
        FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, true);
        if (findBrokerResult != null) {
    
    
            LockBatchRequestBody requestBody = new LockBatchRequestBody();
            requestBody.setConsumerGroup(this.consumerGroup);
            requestBody.setClientId(this.mQClientFactory.getClientId());
            requestBody.setMqSet(mqs);

            try {
    
    
                Set<MessageQueue> lockOKMQSet =
                    this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), 
                    																	requestBody, 1000);//#2
                for (MessageQueue mq : lockOKMQSet) {
    
    //#3
                    ProcessQueue processQueue = this.processQueueTable.get(mq);
                    if (processQueue != null) {
    
    
                        if (!processQueue.isLocked()) {
    
    
                        }
                        processQueue.setLocked(true);
                        processQueue.setLastLockTimestamp(System.currentTimeMillis());
                    }
                }
                for (MessageQueue mq : mqs) {
    
    //#4
                    if (!lockOKMQSet.contains(mq)) {
    
    
                        ProcessQueue processQueue = this.processQueueTable.get(mq);
                        if (processQueue != null) {
    
    
                            processQueue.setLocked(false);
                        }
                    }
                }
            } 
        }
    }
}
  1. 步骤1从RebalanceImpl#processQueueTable获取当前group分配到的全部MessageQueue。
  2. 步骤2向Broker发送锁定消息队列请求,该方法会返回本次成功锁定的消息消费队列。
  3. 步骤3中遍历步骤2中返回的锁定队列,并且将该MessageQueue对应的processQueue锁定。

为什么会有这个锁定呢 ? 顺序消费的时候使用,消费之前会判断一下ProcessQueue锁定时间是否超过阈值(默认30000ms),如果没有超时,代表还是持有锁。

4、RebalanceImpl

public void doRebalance(final boolean isOrder) {
    
    
   Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
   if (subTable != null) {
    
    
       for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
    
    
           final String topic = entry.getKey();
           this.rebalanceByTopic(topic, isOrder);//#1
       }
   }
   this.truncateMessageQueueNotMyTopic();
}

subTable:所有consumerGroup 的订阅信息,包含订阅的topic、tag等信息。

  • 步骤1首先通过RPC获取broker集群中某个broker实例。通过broker实例得到其上的队列信息。在#3.1中给每个消费者实例根据指定的分配算法分配其所属的队列。
private void rebalanceByTopic(final String topic, final boolean isOrder) {
    
    
    switch (messageModel) {
    
    
        case BROADCASTING: {
    
    
            ...
            break;
        }
        case CLUSTERING: {
    
    
            Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
            List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
            if (mqSet != null && cidAll != null) {
    
    
                List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
                mqAll.addAll(mqSet);
                Collections.sort(mqAll);
                Collections.sort(cidAll);
                AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
                List<MessageQueue> allocateResult = null;
                // #1
                allocateResult = strategy.allocate(this.consumerGroup,this.mQClientFactory.getClientId(),mqAll,cidAll);
                ...
				// #2
                boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
                ...
            }
        }  
    }
}
  1. 步骤1通过队列分配策略初始化当前group监听的对应队列。
  2. 步骤2更新Map集合类型的ProcessQueueTable中队列信息。
private boolean updateProcessQueueTableInRebalance(final String topic, final Set<MessageQueue> mqSet,
    final boolean isOrder) {
    
    
    boolean changed = false;
	...
	Iterator<Entry<MessageQueue, ProcessQueue>> it = this.processQueueTable.entrySet().iterator();
    while (it.hasNext()) {
    
    
    	...
    }
    List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
    for (MessageQueue mq : mqSet) {
    
    
        if (!this.processQueueTable.containsKey(mq)) {
    
    
            ...
            this.removeDirtyOffset(mq);
            ProcessQueue pq = new ProcessQueue();
            long nextOffset = this.computePullFromWhere(mq);
            ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
            if (pre != null) {
    
    
             } else {
    
    
                 PullRequest pullRequest = new PullRequest();
                 pullRequest.setConsumerGroup(consumerGroup);
                 pullRequest.setNextOffset(nextOffset);
                 pullRequest.setMessageQueue(mq);
                 pullRequest.setProcessQueue(pq);
                 pullRequestList.add(pullRequest);
                 changed = true;
             }
            ...
        }
    }
    this.dispatchPullRequest(pullRequestList);//#1
    return changed;
}

每个Message Queue 有个对应的 Proces Queue 对象。

两个核心类:ProcessQueue、PullRequest。一个核心属性之processQueueTable。

  1. 步骤1中核心功能:PullMessageService任务开始从broker队列中拉取数据的前提是阻塞队列pullRequestQueue中必须存在PullRequest元素,而该处功能就是给PullMessageService中队列属性中初始化元素。

4.1、流量控制

Pull 获得的消息,如果直接提交到线程里执行,很难监控和控制,比如,如何得知当前消息堆积的数 ?如何重复处理某些消息? 如何延迟处某些消息?RocketMQ定义了一个快照类 Process Queue 来解决这些问题,在Push Consumer 运行的时候, 每个Message Queue 有个对应的 Proces Queue 对象,保存了这个 Message Queue 消息处理状态的快照。

扫描二维码关注公众号,回复: 14728783 查看本文章

ProcessQueue 对象里主要的内容是一个 TreeMap 和一个读写锁。TreeMap里以 Message Queue Offset 作为 Key ,以消息内容的引用为 Value ,保存了所有从 MessageQueue 获取到,但是还未被处理的消息。读写锁控制着多个线程对 TreeMap 对象的并发访问。

5、任务之RebalanceService

RebalanceService任务的调用是通过MQClientInstance#start触发的。即生产者、消费者启动过程中都有机会触发当前任务。即使用户应用中同时存在生产者、消费者该任务的触发理论上应该被触发一次。参考原因

public void run() {
    
    
    while (!this.isStopped()) {
    
    
    	this.waitForRunning(waitInterval);
        this.mqClientFactory.doRebalance();
    }
}

waitForRunning存在暂停阻塞的原因:

  • 如果应用同时存在生产者、消费者前提下,先启动生产者导致的任务触发使其执行没有任何意义,这种情况下消费者实例启动过程中会立即唤醒当前线程完成队列分配任务。
  • 如果应用中只有生产者则由于MQClientInstance属性consumerTable没有【当前也不会】注册消费者实例则也不会触发队列任务分配。此时也不会不断频繁空轮询。

private static long waitInterval = Long.parseLong(System.getProperty(“rocketmq.client.rebalance.waitInterval”, “20000”));

public void doRebalance() {
    
    
    for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
    
    
        MQConsumerInner impl = entry.getValue();
        impl.doRebalance();//DefaultMQPushConsumerImpl#doRebalance --> RebalanceImpl#doRebalance
    }
}

consumerTable:遍历全部的consumerGroup。真正实现消费者实例分配broker队列的功能是由RebalanceImpl完成。

5.1、AllocateMessageQueueAveragely

public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
   					List<String> cidAll) {
    
    
    ...
    List<MessageQueue> result = new ArrayList<MessageQueue>();
    int index = cidAll.indexOf(currentCID);//#1 有序列表cidAll
    int mod = mqAll.size() % cidAll.size();//#2 有序列表mqAll
    int averageSize =
        mqAll.size() <= cidAll.size() ? 1 : 
        (mod > 0 && index < mod ? mqAll.size() / cidAll.size() + 1 : mqAll.size() / cidAll.size());//#3
    int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;//#4
    int range = Math.min(averageSize, mqAll.size() - startIndex);//#5
    for (int i = 0; i < range; i++) {
    
    
        result.add(mqAll.get((startIndex + i) % mqAll.size()));//#6
    }
    return result;
}

currentCID:当前消费者实例ID。mqAll:创建topic时指定的队列数。cidAll:消费组下全部消费实例集合。
当mqAll.size() == cidAll.size(),则mod = 0,averageSize = 1:

  • 如果 index = 0,则 startIndex = 0,range = 1。则当前消费者实例分配得到的队列为列表mqAll下标为0的队列。
  • 如果 index = 1,则 startIndex = 1,range = 1。则当前消费者实例分配得到的队列为列表mqAll下标为1的队列。
    … 以此类推得知,该种情况下每个消费者实例分别分配到一个互不相同的队列。并且分配到的队列其在列表mqAll中的下标就是当前消费实例在列表cidAll中的下标。

当mqAll.size() > cidAll.size(),则mod > 0: mqAll.size() / cidAll.size() 表示每个消费实例最少分配的队列数

  • 如果 index > mod,averageSize = mqAll.size() / cidAll.size() + 1即对应消费实例得到 leastAvg【(mqAll - mod)/cidAll 】个队列。
    如果index = 0,则 averageSize = leastAvg + 1,startIndex = 0,range = leastAvg + 1。则当前消费者实例优先获取前 leastAvg + 1 队列。
    如果index = 1,则 averageSize = leastAvg + 1,startIndex = averageSize,range = averageSize。则当前消费者实例优先获取averageSize后的共 leastAvg + 1 队列。
    … 以此类推得知,前mod个消费组获取 leastAvg + 1 个队列,剩余消费组实例获取 leastAvg 个队列。
  • 如果 index < mod,averageSize = mqAll.size() / cidAll.size() + 1即将多余的mod个队列平均分配到前mod个消费实例。

小总结:消费组中消费实例个数不变的前提下,每隔20s触发的每次分配任务其结果是每个消费实例得到的队列始终是一样的。
疑问:每个消费者实例都是在自己JVM获取队列信息,如何保证某个broker中某个队列没有被重复监听呢?
答:首先mqAll、cidAll都是有序的。所以只要在消费组运行期间没有新增消费实例,则mqAll、cidAll两个列表中元素顺序在每隔20s触发定时任务的过程中一直是保持一样的。其次,每个消费实例获取队列信息起始位置都是其在cidAll列表中的下标,只要mqAll、cidAll列表中元素顺序不变,则消费者实例获取到的队列信息每次都是一样的。

6、PullMessageService

private void pullMessage(final PullRequest pullRequest) {
    
    
    final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
    DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
    impl.pullMessage(pullRequest);
}
@Override
public void run() {
    
    
    while (!this.isStopped()) {
    
    
        PullRequest pullRequest = this.pullRequestQueue.take();
        this.pullMessage(pullRequest);// --> DefaultMQPushConsumerImpl#pullMessage
    }
}

阻塞队列pullRequestQueue【LinkedBlockingQueue】存在PullRequest表明当前应用中存在消费者实例。而且阻塞队列中的元素只有在消费者实例启动过程中才会被初始化。

PullRequest:主要包含messageQueue、processQueue。当前消费者实例分配得到的每个队列messageQueue最终都会被封装成对应的PullRequest。消费者实例不断通过PullRequest从broker拉取当前队列messageQueue中的消息。如果存在消息并不会同步直接返回,而是通过NettyClientHandler异步返回。

阻塞队列好处:即使应用只有消息生产者,一直阻塞不会频繁空轮询造成CPU压力。

public void pullMessage(final PullRequest pullRequest) {
    
    
    final ProcessQueue processQueue = pullRequest.getProcessQueue();
    ...

    if (!this.consumeOrderly) {
    
    
        ...
    } else {
    
    //如果当前processQueue没有被锁定,则延迟当前队列消费数据
        if (processQueue.isLocked()) {
    
    
            if (!pullRequest.isLockedFirst()) {
    
    
                ...
            }
        } else {
    
    
            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
            return;
        }
    }

    final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
    ...
    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:
                       boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());//#1
                       DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(//#2
                                pullResult.getMsgFoundList(),processQueue,pullRequest.getMessageQueue(),dispatchToConsume);
                        break;
                    case NO_NEW_MSG:
                    case NO_MATCHED_MSG:
                        ...
                    case OFFSET_ILLEGAL:
                        ...
                    default:
                        break;
                }
            }
        }
    };

    ...
    SubscriptionData sd = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
    ...
    this.pullAPIWrapper.pullKernelImpl(
        pullRequest.getMessageQueue(),
        subExpression,
        subscriptionData.getExpressionType(),
        subscriptionData.getSubVersion(),
        pullRequest.getNextOffset(),
        this.defaultMQPushConsumer.getPullBatchSize(),
        sysFlag,
        commitOffsetValue,
        BROKER_SUSPEND_MAX_TIME_MILLIS,
        CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
        CommunicationMode.ASYNC,
        pullCallback
    );
}

7、消费者保证消息的消费顺序之ConsumeMessageOrderlyService

先稍微讲一下消费逻辑,首先RebalancePushImpl.dispatchPullRequest方法把当前消费者实例负责的MessageQueue及对应的ProcessQueue封装成了pullRequest进行分发,会被分发到一个PullMessageService.pullRequestQueue的阻塞链表队列中,然后PullMessageService线程不停从该队列中获取请求(没有就阻塞)拉取消息,在DefaultMQPushConsumerImpl.pullMessage中执行真正的消费逻辑这里会不停的拉取(当然有限流),拉取到消息后被异步处理,但是请放心异步处理也会按顺序提交consumeOffset,不会造成Offset大的消息先消费的情况,这里我们异步处理类及方法。

参考文章

猜你喜欢

转载自blog.csdn.net/qq_36851469/article/details/129031513