2.从源码角度看RocketMQ普通消息消费的全过程

736 阅读7分钟

本文已参与[新人创作礼]活动,一路开启掘金创作之路。

1.消费端消费消息的代码举例

基于rocketmq-4.9.0 版本分析rocketmq 代码举例:

public class Consumer {

   public static void main(String[] args) throws InterruptedException, MQClientException {

       // 实例化消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("c1-group");

       // 设置NameServer的地址
        consumer.setNamesrvAddr("127.0.0.1:9876");

       //TODO:订阅一个或者多个Topic,以及Tag来过滤需要消费的消息
        consumer.subscribe("test_topic", "*");
       // TODO: 注册消息监听器,用来消费消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                try {

                    System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);

                    boolean consume = new Random().nextBoolean();
                    if(!consume) {
                        //TODO:消费失败,等待重试
                        return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }

                //TODO: 消费成功
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //TODO: 启动消费者实例
        consumer.start();

        System.out.printf("Consumer Started.%n");
   }
}

2.客户端start()方法都干了什么?

首先,暴露给开发者的 DefaultMQPushConsumer是一个外观类,真正工作的是其内部的DefaultMQPushConsumerImpl,所以我们看下DefaultMQPushConsumerImpl#start()的逻辑

内容非常的多,我会在代码上添加一些注释

public synchronized void start() throws MQClientException {
    switch (this.serviceState) {
        case CREATE_JUST:
            log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(),
                this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
            this.serviceState = ServiceState.START_FAILED;
            //TODO:检查配置,比如消费者组是否为空,消费模式是否为空,订阅信息是否为空等等
            this.checkConfig();

            //TODO: 拷贝订阅关系,大概就是将订阅关系设置到重平衡服务类中
            this.copySubscription();

            if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
                this.defaultMQPushConsumer.changeInstanceNameToPID();
            }

            //TODO:创建客户端实例对象
            this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);

            this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
            this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
            this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
            this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);

            //TODO: 创建拉取对象的核心类,后面去broker拉取消息时会看到
            this.pullAPIWrapper = new PullAPIWrapper(
                mQClientFactory,
                this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
            this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);

            if (this.defaultMQPushConsumer.getOffsetStore() != null) {
                this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
            } else {
                switch (this.defaultMQPushConsumer.getMessageModel()) {
                    case BROADCASTING:
                        //TODO: 广播模式创建LocalFileOffsetStore,保存offset到本地文件中
                        this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                        break;
                    case CLUSTERING:
                        //TODO:集群模式创建 RemoteBrokerOffsetStore,保存offset到broker文件中
                        this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                        break;
                    default:
                        break;
                }
                this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
            }
            this.offsetStore.load();

            if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
                this.consumeOrderly = true;
                //TODO:创建顺序消费消息服务类
                this.consumeMessageService =
                    new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
            } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
                this.consumeOrderly = false;
                //TODO:创建其他消费消息服务类,其内部维护了一个线程池,后面会用到
                this.consumeMessageService =
                    new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
            }

            this.consumeMessageService.start();
            //TODO:注册consumer到本地
            boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
            if (!registerOK) {
                this.serviceState = ServiceState.CREATE_JUST;
                this.consumeMessageService.shutdown(defaultMQPushConsumer.getAwaitTerminationMillisWhenShutdown());
                throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
                    + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                    null);
            }

            //启动客户端实例,这个方法非常重要,内部做了很多事情,后面会说
            mQClientFactory.start();
            log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
            this.serviceState = ServiceState.RUNNING;
            break;
        case RUNNING:
        case START_FAILED:
        case SHUTDOWN_ALREADY:
            throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
                + this.serviceState
                + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                null);
        default:
            break;
    }
    
    this.updateTopicSubscribeInfoWhenSubscriptionChanged();
    this.mQClientFactory.checkClientInBroker();
    //TODO:发送消息到broker
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
    //TODO:立即重平衡
    this.mQClientFactory.rebalanceImmediately();
}

2.1 检查配置

  1. 校验GroupName是否为空;校验GroupName是否等于DEFAULT_CONSUMER(等于的话直接抛出异常)
  2. 校验消费模式:集群/广播                            
  3. 校验ConsumeFromWhere                         
  4. 校验开始消费的指定时间                            
  5. 校验AllocateMessageQueueStrategy         
  6. 校验订阅关系                                          
  7. 校验是否注册消息监听                               
  8. 校验消费线程数,consumeThreadMinconsumeThreadMax 默认值都是20,取值区间都是 [1, 1000]   
  9. 校验本地队列缓存消息的最大数,默认是1000,取值范围是[1, 1024], 主要是做流控用的
  10. 校验拉取消息的时间间隔,pullInterval参数,默认是不存在间隔,取值范围是[0, 65535]。当消费速度比生产速度快,可以设置这个参数,避免花费大概率从broker拉取空消息
  11. 校验单次拉取的最大消息数,consumeMessageBatchMaxSize 参数,默认是1,取值范围是[1, 1024] 
  12. 校验单次消费的最大消息数, pullBatchSize 参数,默认是32,取值范围是[1, 1024]。

2.2 拷贝订阅关系

将订阅关系设置到重平衡服务类RebalanceImpl

//TODO:key=topic, value=订阅数据(就是tag信息)
protected final ConcurrentMap<String /* topic */, SubscriptionData> subscriptionInner =
    new ConcurrentHashMap<String, SubscriptionData>();

2.3 创建客户端实例 MQClientInstance

//TODO: 当后面启动的时候会做很多事情,请继续往后看
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);

2.4 创建拉取消息的核心类 PullAPIWrapper

this.pullAPIWrapper = new PullAPIWrapper(
    mQClientFactory,
    this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());

2.5 创建 offset存储服务 OffsetStore

  • 如果是广播模式(BROADCASTING), 则创建 LocalFileOffsetStore 对象,将消费者的offset存储到本地的,默认文件路径为当前用户主目录下的 .rocketmq_offsets/clientId/{clientId}/{group}/Offsets.json。其中clientId为当前消费者id,默认为ip@default,{clientId}为当前消费者id,默认为ip@default, {group}为消费者组名称
  • 如果是集群模式(CLUSTERING), 则创建 RemoteBrokerOffsetStore对象,将消费者的offset存储到broker中,文件路径为当前用户主目录下的store/config/consumerOffset.json

2.6 创建消费消息的服务类 ConsumeMessageService

  • 如果是顺序消费,则创建 ConsumeMessageOrderlyService 对象
  • 如果是其他消费,则创建 ConsumeMessageConcurrentlyService 对象,同时内部也会创建一个ThreadPoolExecutor线程池,这个线程池非常的重要,拉取到消息后会将消息提交到这个线程池中给消费者消费

2.7 将consumer注册到本地

boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);

将消费者组信息添加到本地客户端实例MQClientInstanceconsumerTable Map中,key=groupName; value=DefaultMQPushConsumerImpl,就是消费者对象

2.8 启动客户端实例(重要)

mQClientFactory.start();

其内部做了很多事情,主要如下:

2.8.1 启动远程netty客户端

// Start request-response channel
this.mQClientAPIImpl.start();

这个主要就是用来通信的

2.8.2 启动各种定时任务

// Start various schedule tasks
this.startScheduledTask();

那么有哪些定时任务呢?继续往里看,罗列几个特别关注的

2.8.2.1 发送心跳到Broker

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        try {
            MQClientInstance.this.cleanOfflineBroker();
            MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
        } catch (Exception e) {
            log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
        }
    }
}, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);

延迟1s执行,每隔30s发送一次心跳包

2.8.2.2 持久化消费者的 offset

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        try {
            MQClientInstance.this.persistAllConsumerOffset();
        } catch (Exception e) {
            log.error("ScheduledTask persistAllConsumerOffset exception", e);
        }
    }
}, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

延迟10s执行,每隔5s持久化一次offset
这里的持久化是将本地Map中offset发送到broker中,然后broker中的定时任务写到文件中,完成真正的持久化,后面会看到。

2.8.3 启动拉取消息的服务PullMessageService(重要)

// Start pull service
this.pullMessageService.start();

他是一个异步线程,其核心逻辑是

@Override
public void run() {
    log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        try {
            PullRequest pullRequest = this.pullRequestQueue.take();
            this.pullMessage(pullRequest);
        } catch (InterruptedException ignored) {
        } catch (Exception e) {
            log.error("Pull Message Service Run Method exception", e);
        }
    }

    log.info(this.getServiceName() + " service end");
}

他会监听阻塞队列pullRequestQueue,当队列是空的时候,他会一直阻塞,如果不为空,则获取PullRequest对象,去拉取消息,这个逻辑后面再说。

刚开始肯定是阻塞的,我们要看什么时候往队列中放入值,以及放入值之后做什么

2.8.4 启动重平衡服务 RebalanceService

// Start rebalance service
this.rebalanceService.start();

它也是一个异步线程,其核心逻辑是

@Override
public void run() {
    log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        //TODO:内部使用了juc的CountDownLatch, 使得这里启动后仍然是阻塞的
        this.waitForRunning(waitInterval);
        this.mqClientFactory.doRebalance();
    }

    log.info(this.getServiceName() + " service end");
}

他是重平衡的核心逻辑,但是在启动时,由于使用了JUC的 CountDownLatch锁,使其不会立即重平衡,而是阻塞,什么时候出发重平衡呢?我们还是继续往后看

2.9 发送心跳到broker

this.updateTopicSubscribeInfoWhenSubscriptionChanged();
this.mQClientFactory.checkClientInBroker();
//TODO: 发送心跳到broker
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
this.mQClientFactory.rebalanceImmediately();

是不是有些疑惑?前面的定时任务中不是已经启动了心跳服务吗,为什么这里还要启动呢?我也不清楚,猜测是因为担心网络问题导致没有及时发送给broker吧

2.10 立即启动重平衡服务RebalanceService

this.updateTopicSubscribeInfoWhenSubscriptionChanged();
this.mQClientFactory.checkClientInBroker();
//TODO: 发送心跳到broker
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
//TODO:立即启动重平衡服务
this.mQClientFactory.rebalanceImmediately();

前面在 2.8.4的时候,启动了重平衡服务,但是因为 CountDownLatch 导致阻塞了,这里就是唤醒,可以执行重平衡的逻辑。

这里先不关注它的内部逻辑,请继续往后看

以上客户端算是启动完成了,接下来看服务端相关的过程

3.客户端发送心跳到Broker

前面的2.9 以及 2.8.2.1 都会往broker发送心跳,我们看下发送心跳都做了什么,主要是看服务端
首先客户端发送netty指令

public int sendHearbeat(final String addr,final HeartbeatData heartbeatData,final long timeoutMillis ) throws RemotingException, MQBrokerException, InterruptedException {
    //TODO:心跳指令
    RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, null);
    request.setLanguage(clientConfig.getLanguage());
    request.setBody(heartbeatData.encode());
    //TODO:发送到服务端(addr 就是broker的地址)
    RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
    assert response != null;
    switch (response.getCode()) {
        case ResponseCode.SUCCESS: {
            return response.getVersion();
        }
        default:
            break;
    }

    throw new MQBrokerException(response.getCode(), response.getRemark(), addr);
}

3.1 服务端接收到心跳请求

接收心跳请求服务的类是:ClientManageProcessor

@Override
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request)
    throws RemotingCommandException {
    switch (request.getCode()) {
        //TODO:心跳指令
        case RequestCode.HEART_BEAT:
            return this.heartBeat(ctx, request);
        case RequestCode.UNREGISTER_CLIENT:
            return this.unregisterClient(ctx, request);
        case RequestCode.CHECK_CLIENT_CONFIG:
            return this.checkClientConfig(ctx, request);
        default:
            break;
    }
    return null;
}

说明:当broker启动时(BrokerController), 会调用 registerProcessor()方法,注册处理器,其中就有处理客户端相关请求的

/**
 * ClientManageProcessor
 */
ClientManageProcessor clientProcessor = new ClientManageProcessor(this);
this.remotingServer.registerProcessor(RequestCode.HEART_BEAT, clientProcessor, this.heartbeatExecutor);
this.remotingServer.registerProcessor(RequestCode.UNREGISTER_CLIENT, clientProcessor, this.clientManageExecutor);
this.remotingServer.registerProcessor(RequestCode.CHECK_CLIENT_CONFIG, clientProcessor, this.clientManageExecutor);

3.2 处理心跳请求都做了什么?

主要做3件事

3.2.1 保存订阅组配置

  1. 将订阅组配置保存到本地表 subscriptionGroupTable
  2. 将订阅组配置持久化
public SubscriptionGroupConfig findSubscriptionGroupConfig(final String group) {
    //TODO:从本地订阅组表中获取
    SubscriptionGroupConfig subscriptionGroupConfig = this.subscriptionGroupTable.get(group);
    if (null == subscriptionGroupConfig) {
        if (brokerController.getBrokerConfig().isAutoCreateSubscriptionGroup() || MixAll.isSysConsumerGroup(group)) {
            //TODO:创建订阅组配置,除了订阅组名称,其他的都是默认值
            subscriptionGroupConfig = new SubscriptionGroupConfig();
            subscriptionGroupConfig.setGroupName(group);
            //TODO:将订阅组配置添加到Map中
            SubscriptionGroupConfig preConfig = this.subscriptionGroupTable.putIfAbsent(group, subscriptionGroupConfig);
            if (null == preConfig) {
                log.info("auto create a subscription group, {}", subscriptionGroupConfig.toString());
            }
            this.dataVersion.nextVersion();
            //TODO:持久化订阅组配置
            this.persist();
        }
    }

    return subscriptionGroupConfig;
}

这个订阅组配置是啥?

  • 在broker的 $home/store/config/ 目录下,有很多文件,其中subscriptionGroup.json 就是保存了订阅组配置

image.png

  • 内容如下:

image.png

3.2.2 注册消费者

public boolean registerConsumer(final String group, final ClientChannelInfo clientChannelInfo,
    ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere,
    final Set<SubscriptionData> subList, boolean isNotifyConsumerIdsChangedEnable) {
    //TODO:key=groupName, value=消费者组信息(其内部维护了各个消费者)
    ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group);
    if (null == consumerGroupInfo) {
        //TODO:构建消费者组信息
        ConsumerGroupInfo tmp = new ConsumerGroupInfo(group, consumeType, messageModel, consumeFromWhere);
        //TODO:保存到消费者组表中
        ConsumerGroupInfo prev = this.consumerTable.putIfAbsent(group, tmp);
        consumerGroupInfo = prev != null ? prev : tmp;
    }
    
    //TODO:更新channel,实际上就是保存消费者组下的消费者,第一次发起心跳的候肯定返回的是true
    boolean r1 =
        consumerGroupInfo.updateChannel(clientChannelInfo, consumeType, messageModel,
            consumeFromWhere);
    //TODO:更新订阅信息        
    boolean r2 = consumerGroupInfo.updateSubscription(subList);

    //TODO:有任何一个返回true,则通知客户端消费组有变动,发起重平衡通知
    if (r1 || r2) {
        if (isNotifyConsumerIdsChangedEnable) {
            this.consumerIdsChangeListener.handle(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel());
        }
    }

 //TODO:注册
this.consumerIdsChangeListener.handle(ConsumerGroupEvent.REGISTER, group, subList);

    return r1 || r2;
}

注册消费者简单总结都做了什么?

  1. 从消费者组表consumerTable中,根据key(=groupName)获取消费者组信息;如果返回null,则new ConsumerGroupInfo,设置groupName,ConsumeType,MessageModel,ConsumeFromWhere相关参数,所以说一个消费者组具有共性;不过也从侧面说明,同一个消费者组下的消费者以第一个注册的消费者为准。
  2. 更新channel,实际上就是保存消费者组下的消费者,保存到消费者组对象ConsumerGroupInfo中的channel表channelInfoTable
//TODO:消费者组
public class ConsumerGroupInfo {
    private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME);
    private final String groupName;
    private final ConcurrentMap<String/* Topic */, SubscriptionData> subscriptionTable =
        new ConcurrentHashMap<String, SubscriptionData>();
    //TODO:value 就是消费者组下的消费者    
    private final ConcurrentMap<Channel, ClientChannelInfo> channelInfoTable =
        new ConcurrentHashMap<Channel, ClientChannelInfo>(16);
        
    private volatile ConsumeType consumeType;
    private volatile MessageModel messageModel;
    private volatile ConsumeFromWhere consumeFromWhere;
}
  1. 更新订阅者,将订阅者信息更新到订阅表subscriptionTable
  2. 是否通知客户端进行重平衡,在消费者第一次发起心跳的时候,注册方法中的r1,r2肯定返回true, 此时就要给客户端发起重平衡指令
public void notifyConsumerIdsChanged(
    final Channel channel,
    final String consumerGroup) {
    if (null == consumerGroup) {
        log.error("notifyConsumerIdsChanged consumerGroup is null");
        return;
    }

    NotifyConsumerIdsChangedRequestHeader requestHeader = new NotifyConsumerIdsChangedRequestHeader();
    requestHeader.setConsumerGroup(consumerGroup);
    RemotingCommand request =
 //TODO: 构建 通知客户端id改变的指令,就是有新的消费者注册或者注销
RemotingCommand.createRequestCommand(RequestCode.NOTIFY_CONSUMER_IDS_CHANGED, requestHeader);

    try {
 //TODO: 给客户端发送指令,客户端自己发起重平衡
this.brokerController.getRemotingServer().invokeOneway(channel, request, 10);
    } catch (Exception e) {
        log.error("notifyConsumerIdsChanged exception. group={}, error={}", consumerGroup, e.toString());
    }
}
  1. 心跳是每隔30s发起一次,在第一次发起心跳后,如果也没有任何变动,则注册方法中的r1,r2返回fasle,则执行注册

重点关注下第4步,它会保存客户端消费者组以及消费者组下的消费者的信息;以及给客户端发起重平衡指令,请继续往下看

4.客户端立即重平衡

我们沿着代码顺序该看客户端立即重平衡了,也是步骤2.10;不过在上面消费者启动后第一次向broker发送心跳时,broker会给客户端发起RequestCode.NOTIFY_CONSUMER_IDS_CHANGED指令,客户端ClientRemotingProcessor接受到请求后,则立即执行重平衡。

//TODO: 客户端接收到 RequestCode.NOTIFY_CONSUMER_IDS_CHANGED 指令,则立即发起重平衡
public RemotingCommand notifyConsumerIdsChanged(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    try {
        final NotifyConsumerIdsChangedRequestHeader requestHeader =
            (NotifyConsumerIdsChangedRequestHeader) request.decodeCommandCustomHeader(NotifyConsumerIdsChangedRequestHeader.class);
        log.info("receive broker's notification[{}], the consumer group: {} changed, rebalance immediately",
            RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
            requestHeader.getConsumerGroup());
        this.mqClientFactory.rebalanceImmediately();
    } catch (Exception e) {
        log.error("notifyConsumerIdsChanged exception", RemotingHelper.exceptionSimpleDesc(e));
    }
    return null;
}

至于是上面RequestCode.NOTIFY_CONSUMER_IDS_CHANGED 触发的重平衡,还是客户端启动时步骤2.10触发的重平衡,他们的逻辑都是一样的,我们则看重平衡都做了什么?

4.1 重平衡都做了什么?

什么是重平衡?
Rebalance(重平衡)机制指的是:将一个Topic下的多个队列,在同一个消费者组(consumer group)下的多个消费者实例(consumer instance)之间进行重新分配。
注意: 重平衡讨论的前提是集群消费

重平衡的启动代码:RebalanceService

@Override
public void run() {
    log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        //TODO: 客户端每隔20s触发一次重平衡
        this.waitForRunning(waitInterval);
        this.mqClientFactory.doRebalance();
    }

    log.info(this.getServiceName() + " service end");
}

一步一步走,进入核心逻辑代码中RebalanceImpl

private void rebalanceByTopic(final String topic, final boolean isOrder) {
    switch (messageModel) {
        case BROADCASTING: {
            //TODO: 忽略广播模式的代码
            break;
        }
        case CLUSTERING: {
            //TODO: 获取这个topic下的所有队列(默认是4个)
            Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
            //TODO: 获取集群下所有客户端的id
            List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
           
           //TODO: ....忽略一些判断代码

            if (mqSet != null && cidAll != null) {
                List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
                mqAll.addAll(mqSet);

                Collections.sort(mqAll);
                Collections.sort(cidAll);

                //TODO: 默认是平均分配策略
                AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;

                //TODO: 分配结果
                List<MessageQueue> allocateResult = null;
                try {
                    //TODO: 第一个参数是消费者组
                    //TODO: 第二个参数是当前的客户端id
                    //TODO: 第三个参数是所有的queue(默认4个)
                    //TODO: 第四个参数是所有的客户端id
                    allocateResult = strategy.allocate(
                        this.consumerGroup,
                        this.mQClientFactory.getClientId(),
                        mqAll,
                        cidAll);
                } catch (Throwable e) {
                    log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),
                        e);
                    return;
                }

                Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
                if (allocateResult != null) {
                    allocateResultSet.addAll(allocateResult);
                }

                //TODO:这个方法内部要做的内容很多,我在下面进行阐述
                boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
                if (changed) {
                    log.info(
                        "rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}",
                        strategy.getName(), consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(),
1.                         allocateResultSet.size(), allocateResultSet);
                    this.messageQueueChanged(topic, mqSet, allocateResultSet);
                }
            }
            break;
        }
        default:
            break;
    }
}

总结重平衡都做了什么?

  1. 获取这个topic下的所有队列(默认是4个)
  2. 根据groupName向broker发起获取消费者列表信息的指令,返回消费者组下的所有消费者的id
//TODO: 向broker发起获取消费者列表信息的指令,返回消费者组下的所有消费者的id
RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_LIST_BY_GROUP, requestHeader);

在前面3.2.2步骤注册消费者的时候,会将同一个消费者组的信息保存到ConsumerGroupInfo对象中,将同一个消费者组下的消费者保存到 ConsumerGroupInfo对象中的 ClientChannelInfo对象中(每个消费者客户端对应一个ClientChannelInfo,其中就有clientId。 (比如:192.168.0.102@20400#92954641838250;192.168.0.102@14302#92953532837230)

  1. 根据分配策略重新分配queue,默认是平均分配策略,参数说明请看代码注释

主要策略有:平均分配策略,环形平均策略,一致性hash策略,同机房策略

  1. 处理分配结果,这个请往下看

4.2 处理重平衡的分配结果

假如我启动一个消费者客户端,那么这个消费者到底消费哪几个队列(MessageQueue)呢?他就是通过重平衡来确定的,此时只有一个客户端,那么它肯定就要消费所有队列,也就是queueid=0,1,2,3; 假如我再启动一个消费者客户端,那么现在有两个客户端,此时怎么分配呢?按照平均分配策略,那么一个消费queueid=0,1; 另一个消费queueid=2,3 。

由于我现在是刚启动一个客户端,所以这4个queue对于它来说,就是新的队列,此时他就要做一个非常重要的一步,创建 PullRequest 对象。

//TODO: ....省略部分代码

List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
//TODO: 遍历queue
for (MessageQueue mq : mqSet) {
        //TODO: ...... 省略诸多代码
        
        PullRequest pullRequest = new PullRequest();
        pullRequest.setConsumerGroup(consumerGroup);
        //TODO:这个参数非常重要,用来标识逻辑队列的消费偏移量
        pullRequest.setNextOffset(nextOffset);
        //TODO: 这个就是我需要新消费的队列
        pullRequest.setMessageQueue(mq);
        pullRequest.setProcessQueue(pq);
        
        //TODO: 放到集合中,因为我可能要消费多个队列,比如一个消费者的情况下,他要消费4个queue
        pullRequestList.add(pullRequest);
}

//TODO: 分发PullReqeust,请看4.3
this.dispatchPullRequest(pullRequestList);

对于新启动的客户端,它必然是要去分配queue的,从而确定我要消费哪几个队列(集群模式下,同一个queue只能被同一个消费者组下的一个消费者去消费).当消费端都启动完成后,没有意外情况下,虽然重平衡服务每隔20s执行一次,但是因为没有消费者组下的消费者没有变化以及queue也没有变化,所以他是不会真正触发重平衡逻辑的。

一旦重平衡分配好,拉取消息时可能最关注的是对象PullRequestnextOffset属性,后面拉取消息会多次看到

4.3 分发PullRequest 对象(重要)

分发逻辑代码:

@Override
public void dispatchPullRequest(List<PullRequest> pullRequestList) {
    //TODO: 当前消费者客户端遍历它需要消费的queue
    for (PullRequest pullRequest : pullRequestList) {
       
//TODO: 这一步非常重要,请继续往后看
this.defaultMQPushConsumerImpl.executePullRequestImmediately(pullRequest);
        log.info("doRebalance, {}, add a new pull request {}", consumerGroup, pullRequest);
    }
}

首先是 当前消费者客户端遍历它需要消费的queue,然后立刻executePullRequest, 它做了什么? 继续点进去看,发现它的核心代码是:

public void executePullRequestImmediately(final PullRequest pullRequest) {
    try {
        //TODO: 放入阻塞队列中
        this.pullRequestQueue.put(pullRequest);
    } catch (InterruptedException e) {
        log.error("executePullRequestImmediately pullRequestQueue.put", e);
    }
}

这一步不知道有没有印象,在前面2.8.3步骤中,拉取消息对象PullMessageService从阻塞队列pullRequestQueue中获取 PullRequest对象,如果没有,则一直阻塞;在此之前,他都是阻塞的,但是刚刚的重平衡逻辑执行完成后,将 PullRequest对象放入阻塞队列中,这样拉取消息对象PullMessageService就可以从阻塞队列中获取到值,从而执行拉取消息的服务。

那么接下来就是开始拉取消息了

5.PullMessageService拉取消息

PullMessageService终于可以队列中获取到PullRequest对象了,那么接下来就是从broker拉取消息了

@Override
public void run() {
    log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        try {
            PullRequest pullRequest = this.pullRequestQueue.take();
            //TODO:开始拉取消息
            this.pullMessage(pullRequest);
        } catch (InterruptedException ignored) {
        } catch (Exception e) {
            log.error("Pull Message Service Run Method exception", e);
        }
    }

    log.info(this.getServiceName() + " service end");
}

一步一步走,然后来到 DefaultMQPushConsumerImpl#pullMessage(PullRequest request)中. 我们看具体都干了什么?

5.1 判断是否触发流控

流控主要是保护消费者。当消费者消费能力不够时,拉取速度太快会导致大量消息积压,很可能内存溢出

  1. 判断queue缓存的消息数量是否超过1000(可以根据pullThresholdForQueue参数配置),如果超过1000,则先不去broker拉取消息,而是先暂停50ms,然后重新将对象放入队列中(this.pullRequestQueue.put(pullRequest)),然后重新拉取(就是上面代码中的 this.pullReuqestQueue.take())
  2. 判断queue缓存的消息大小是否超过100M(可以根据 pullThresholdSizeForQueue参数配置),如果超过100M,则先不去broker拉取消息,而是先暂停50ms,然后重新将对象放入队列中(this.pullRequestQueue.put(pullRequest)),然后重新拉取(就是上面代码中的 this.pullReuqestQueue.take())

5.2 构建消息处理的回调对象 PullCallback

它是非常重要的,但是我这里先不说它,等从broker拉取到消息后,会交给它来处理,到时候我们在返回来看它,这里先跳过。

5.3PullAPIWrapper拉取消息

这个对象熟悉吗? 他就是2.4步骤中创建的拉取消息的核心对象

5.3.1 客户端构建拉取消息的请求

//TODO:简单看下参数
this.pullAPIWrapper.pullKernelImpl(
    //TODO: 指定去哪个queue拉取消息
    pullRequest.getMessageQueue(),
    //TODO:表达式,就是tag/sql
    subExpression,
    //TODO: 表达式类型,TAG/SQL
    subscriptionData.getExpressionType(),
    subscriptionData.getSubVersion(),
    //TODO: 这个非常重要的,第一次拉取它的值是 0 
    pullRequest.getNextOffset(),
    //TODO: 这个参数值默认是32
    this.defaultMQPushConsumer.getPullBatchSize(),
    sysFlag,
    commitOffsetValue,
    BROKER_SUSPEND_MAX_TIME_MILLIS,
    CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
    //TODO: 异步
    CommunicationMode.ASYNC,
    //TODO: 它就是5.2中的回调对象
    pullCallback
);

将上面的参数信息封装到PullMessageRequestHeader对象中,然后拉取消息

    PullMessageRequestHeader requestHeader = new PullMessageRequestHeader();
    requestHeader.setConsumerGroup(this.consumerGroup);
    requestHeader.setTopic(mq.getTopic());
    //TODO:消费哪个queue
    requestHeader.setQueueId(mq.getQueueId());
    //TODO:从哪个queue的offset开始消费
    requestHeader.setQueueOffset(offset);
    //TODO: pullBatchSize, 默认是32
    requestHeader.setMaxMsgNums(maxNums);
    requestHeader.setSysFlag(sysFlagInner);
    requestHeader.setCommitOffset(commitOffset);
    requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis);
    requestHeader.setSubscription(subExpression);
    requestHeader.setSubVersion(subVersion);
    requestHeader.setExpressionType(expressionType);

    //TODO: 拉取消息
    PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage(
        brokerAddr,
        requestHeader,
        timeoutMillis,
        communicationMode,
        pullCallback);

    return pullResult;

创建拉取消息的netty指令,发送到broker

public PullResult pullMessage(
    final String addr,
    final PullMessageRequestHeader requestHeader,
    final long timeoutMillis,
    final CommunicationMode communicationMode,
    final PullCallback pullCallback
) throws RemotingException, MQBrokerException, InterruptedException {
    //TODO: 构建拉取消息的netty指令:PULL_MESSAGE
    RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader);

    switch (communicationMode) {
        case ONEWAY:
            assert false;
            return null;
        case ASYNC:
            //TODO: 异步拉取,将拉取消息的结果交给 PullCallback 处理
            this.pullMessageAsync(addr, request, timeoutMillis, pullCallback);
            return null;
        case SYNC:
            return this.pullMessageSync(addr, request, timeoutMillis);
        default:
            assert false;
            break;
    }

    return null;
}

异步拉取消息,将拉取到的消息交给 PullCallback 进行处理,后面我们在回来看PullCallback

5.3.2 服务端接收拉取消息的请求

服务端接收拉取消息请求的处理器是:PullMessageProcessor

说明:当broker启动时候(BrokerController),会注册很多处理器(registerProcessor()方法),其中就有拉取消息的处理器 PullMessageProcessor

/**
 * PullMessageProcessor
 */
this.remotingServer.registerProcessor(RequestCode.PULL_MESSAGE, this.pullMessageProcessor, this.pullMessageExecutor);
this.pullMessageProcessor.registerConsumeMessageHook(consumeMessageHookList);

首先是一系列的参数,权限判断,我们直接跳过,来到拉取消息的核心代码

//TODO: 从broker 拉取消息,只需要关注这一行即可
final GetMessageResult getMessageResult =
    this.brokerController.getMessageStore().getMessage(requestHeader.getConsumerGroup(), requestHeader.getTopic(),
        requestHeader.getQueueId(), requestHeader.getQueueOffset(), requestHeader.getMaxMsgNums(), messageFilter);

然后我们继续点进去看,它会来到DefaultMessageStore对象的 getMessage(final String group, final String topic, final int queueId, final long offset,final int maxMsgNums, final MessageFilter messageFilter)方法

getMessge()方法参数简单说明一下

  1. 第一个参数是消费者组
  2. 第二个参数是topic
  3. 第三个参数是queueid,表示消费哪个队列
  4. 第四个参数是offset,表示消费的起始偏移量,这个参数比较重要的
  5. 第五个参数是pullBatchSize的值,默认是32
  6. 第六个参数是消息过滤器(消费端可以根据TAG/SQL过滤消息,但是SQL过滤要在broker端完成过滤)

接下来就是拉取消息的核心逻辑了:

//TODO: maxMsgNums 默认是32,取的是 pullBatchSize的值
public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset,
    final int maxMsgNums,
    final MessageFilter messageFilter) {
    //TODO: ......省略一些不关注的代码......
    long beginTime = this.getSystemClock().now();

    GetMessageStatus status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
    //TODO: 下一次消费的起始偏移量,这里先将客户端传递过来的offset赋值给它,继续往后看
    long nextBeginOffset = offset;
    long minOffset = 0;
    long maxOffset = 0;

    //TODO: 创建保存消息的容器
    GetMessageResult getResult = new GetMessageResult();
    //TODO:commmitlog的最大物理偏移量
    final long maxOffsetPy = this.commitLog.getMaxOffset();


    //TODO: 根据 topic 和 queueId 获取 ConsumeQueue
    // 一个 ConsumeQueue 对应一个 MappedFileQueue
    // 一个 MappedFileQueue 对应多个 MappedFile
    ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);
    if (consumeQueue != null) {
        //TODO: 队列中保存了最大,最小 offset, 会设置到保存消息的容器GetMessageResult中
        minOffset = consumeQueue.getMinOffsetInQueue();
        maxOffset = consumeQueue.getMaxOffsetInQueue();
        //TODO: ...... 省略offset的边缘检测.......
        } else {

            //TODO: 从 consumequeue 中读取索引数据
            //TODO: 这个和消息分发 ReputMessageService 从 commitlog 中读取消息是一样的
            //TODO: 第一次 offset=0, 从 consumequeue中读取多少消息呢?
            //TODO: 在数据分发后,MappedFile wrotePosition 会记录写入的位置(就是记录写到哪里了)
            SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(offset);
            if (bufferConsumeQueue != null) {
                try {
                    status = GetMessageStatus.NO_MATCHED_MESSAGE;

                    long nextPhyFileStartOffset = Long.MIN_VALUE;
                    long maxPhyOffsetPulling = 0;

                    int i = 0;

                    //TODO: pullBatchSize(32) 好像并没有用, 只有当 pullBatchSize > 800 时才有用?
                    final int maxFilterMessageCount = Math.max(16000, maxMsgNums * ConsumeQueue.CQ_STORE_UNIT_SIZE);
                    final boolean diskFallRecorded = this.messageStoreConfig.isDiskFallRecorded();
                    ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();


                    //TODO: bufferConsumeQueue.getSize()  就是consumequeue 中的消息索引单元的总size(size/20 = 索引个数)
                    //TODO: 每20个字节往前推
                    for (; i < bufferConsumeQueue.getSize() && i < maxFilterMessageCount; i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {                       
                       //TODO: 一个索引单元包含三个元素:消息偏移量,消息大小,消息tag的hashcode
                        long offsetPy = bufferConsumeQueue.getByteBuffer().getLong();                      
                        int sizePy = bufferConsumeQueue.getByteBuffer().getInt();
                        long tagsCode = bufferConsumeQueue.getByteBuffer().getLong();

                        //TODO: offsetPy + sizePy = 确定一条消息

                        maxPhyOffsetPulling = offsetPy;

                        if (nextPhyFileStartOffset != Long.MIN_VALUE) {
                            if (offsetPy < nextPhyFileStartOffset)
                                continue;
                        }

                        boolean isInDisk = checkInDiskByCommitOffset(offsetPy, maxOffsetPy);

                        //TODO: pullBatchSize在这里会工作,当超过默认的32条后,就会跳出循环
                        if (this.isTheBatchFull(sizePy, maxMsgNums, getResult.getBufferTotalSize(), getResult.getMessageCount(),
                            isInDisk)) {
                            break;
                        }
                        
                        //TODO: ..... 忽略判断代码.......

                        //TODO: 从commitlog 读取消息,一次读取一条消息
                        SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy);
                        
                        //TODO: .....忽略判断代码,比如没有读取到消息就continue....
                         
                        //TODO: 将读到的消息放入容器中,然后继续循环 
                        getResult.addMessage(selectResult);
                        status = GetMessageStatus.FOUND;
                        nextPhyFileStartOffset = Long.MIN_VALUE;
                    }

                    if (diskFallRecorded) {
                        long fallBehind = maxOffsetPy - maxPhyOffsetPulling;
                        brokerStatsManager.recordDiskFallBehindSize(group, topic, queueId, fallBehind);
                    }


                    //TODO: 下一次的 queue offset
                    //TODO: 假如第一次读取,并且只有一条,那么 nextBeginOffset = 0 + 20 / 20 = 1;
                    nextBeginOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);

                    long diff = maxOffsetPy - maxPhyOffsetPulling;
                    long memory = (long) (StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE
                        * (this.messageStoreConfig.getAccessMessageInMemoryMaxRatio() / 100.0));
                    getResult.setSuggestPullingFromSlave(diff > memory);
                } finally {

                    bufferConsumeQueue.release();
                }
            } else {
                status = GetMessageStatus.OFFSET_FOUND_NULL;
                nextBeginOffset = nextOffsetCorrection(offset, consumeQueue.rollNextFile(offset));
                log.warn("consumer request topic: " + topic + "offset: " + offset + " minOffset: " + minOffset + " maxOffset: "
                    + maxOffset + ", but access logic queue failed.");
            }
        }
    } 
    //TODO: 忽略else

    getResult.setStatus(status);

    //TODO: 设置 nextBeginOffset ,消费者拿到 nextBeginOffset 后会设置到 nextOffset
    //TODO: 然后消费者下次传过来,他就是这个方法的参数的  offset
    getResult.setNextBeginOffset(nextBeginOffset);
    getResult.setMaxOffset(maxOffset);
    getResult.setMinOffset(minOffset);
    return getResult;
}

接下来我们对拉取逻辑做个总结:

  1. 创建保存消息的容器对象GetMessageResult,它重要保存4部分内容
  • 1)它会保存拉取到的信息
  • 2)逻辑消费队列的nextBeginOffset,这个参数非常非常的重要,它就表示消费者下次消费时从哪开始读取消息(指的是消息索引,根据索引读取真正的消息),后面我们会看到这个参数;
  • 3)逻辑消费队列的 minOffset,最小消费偏移量
  • 4)逻辑消费队列的 maxOffset, 最大消费偏移量 image.png
  1. 根据topic和queueid获取ConsumeQueue,它就是逻辑消费队列,保存着索引单元数据,以及最大offset, 最小offset,以及最大物理偏移量maxPhysicOffset
  2. ConsumeQueue中读取索引数据,从offset位置开始读取,那么这个offset是多少?它取的值是从消费端传过来的nextOffset(请看5.3.1);而这个nextOffset,就是从broker返回的nextBeginOffset,后面还会看到它。我假设是第一次读取消息,那么它肯定是0,那么读取多少消息索引数据呢?在我的消息生产章节中,有提到,当消息索引写入后,会有一个wrotePosition参数,记录已经写到的位置; 所以,我这里就读取从 offset到wrotePositon间的索引数据。
//TODO: 这个offset是消费端传过来的 nextOffset,而这个nextOffst 是broker返回的nextBeginOffset
SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(offset);

假设我在queueid=0的队列写入了33条消息,那么这里返回的SelectMappedBufferResult对象中,其内部的size=660,因为每个消息索引单元是固定20字节,所以20*33=660

  1. 遍历消息的索引单元(我假设消息生产者向queueid=0队列写入了33条数据,而我读取的也是queueid=0的队列,而且是第一次消费)
//TODO: bufferConsumeQueue.getSize()就是consumequeue 中的消息索引单元的总size(size/20 = 索引个数),如果是写入了33条数据,则size=660
//TODO: 循环条件是每次递增20byte(因为每个索引单元固定20byte)
int i = 0;
for (; i < bufferConsumeQueue.getSize() && i < maxFilterMessageCount; i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
  //TODO:......
}

i = 0, 遍历第1条消息索引单元,读取这个索引单元存储的3个数据,分别是消息偏移量offsetPy,消息大小sizePy,消息tag的hashcode,然后根据消息的物理偏移量offsetPy消息大小sizePy,从commitlog中读取一条消息

SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy);

然后将消息保存到容器对象GetMessageResult中.
i = 20 .....遍历第2条消息索引单元.......
i = 40 .....遍历第3条消息索引单元.......
........
i = 620 .....遍历第32条消息索引单元,将消息保存到容器中,此时容器中已经有了32条消息
i = 640 .....遍历第33条消息索引单元,但是此时(pullBatchSize的值<=容器中消息的总数)的结果是true, 如果是true,则跳出循环,不在遍历索引数据。

  1. 计算下一次从队列的哪个位置开始消费,也就是计算队列新的起始offset
//TODO: 下一次的 queue offset
nextBeginOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);

前面我读到 i=640 时,也就是读到第32条消息后退出了循环。由于是第一次消费,所以offset=0, i = 640, 640/20 =32, 所以nextBeginOffset=32(当我下次读取时,这个nextBeginOffset 就变成了上面的offset)

那么本次共计读取了32条消息(还有1条消息没有读取消费,等待下一次读取)

  1. 给容器对象GetMessageResult设置offset值(重要)
//TODO: 设置 nextBeginOffset ,消费者拿到 nextBeginOffset 后会设置到 nextOffset
//TODO: 然后消费者下次传过来,他就是这个方法的参数的  offset
getResult.setNextBeginOffset(nextBeginOffset);
getResult.setMaxOffset(maxOffset);
getResult.setMinOffset(minOffset);

nextBeginOffset=32,maxOffset=33,minOffset=0

  1. 将拉取结果返回给消费者客户端

至此,从broker单次拉取消息就算是结束了

5.3.3 客户端获取broker的响应结果

就是将broker响应的PullMessageResponseHeader对象转换成客户端本地对象PullResult,然后将PullResult对象交给回调函数PullCallback处理(就是 5.2 步骤)

5.4 回调函数PullCallback处理拉取到消息(参考5.2步骤)

//TODO: 拉取消息回调,这里非常重要,不过这里是从broker拉取消息成功后才执行的,继续往后看
PullCallback pullCallback = new PullCallback() {
    @Override
    public void onSuccess(PullResult pullResult) {
        if (pullResult != null) {

            /**
             * 处理从broker读取到的消息
             * 将二进制内容抓换成 MessageExt 对象 并根据tag进行过滤
             */
            pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
                subscriptionData);

            switch (pullResult.getPullStatus()) {

                //TODO: 发现了消息
                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;

                    //TODO: 如果没有消息则立即执行,立即拉取的意思是继续将PullRequest 放入队列中,这样
                    // take()方法将不会在阻塞,然后继续从broker拉取消息,从而达到持续从broker拉取消息
                    if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
                        DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                    } else {
                        firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();

                        DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(),
                            pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size());

                        boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());

                        //TODO: 将消息提交到线程池中,由ConsumeMessageService 进行消费
                        DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
                            pullResult.getMsgFoundList(),
                            processQueue,
                            pullRequest.getMessageQueue(),
                            dispatchToConsume);


                        //TODO: 上面是异步消费,然后这里是将PullRequest放入 队列中,这样take()方法将不会
                        //TODO: 阻塞,然后继续从broker拉取消息,从而达到持续从broker拉取消息
                        //延迟 pullInterval 时间再去拉取消息
                        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:
                    pullRequest.setNextOffset(pullResult.getNextBeginOffset());

                    DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);

                    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
                    break;
                case OFFSET_ILLEGAL:
                    log.warn("the pull request offset illegal, {} {}",
                        pullRequest.toString(), pullResult.toString());
                    pullRequest.setNextOffset(pullResult.getNextBeginOffset());

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

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

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

                                DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue());

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

5.4.1 如果拉取出现异常

则延迟3s钟,将PullRequest对象再次放入队列pullRequestQueue中,等待再次take(),然后还是按照步骤4.3往后执行

public void executePullRequestImmediately(final PullRequest pullRequest) {
    try {
        this.pullRequestQueue.put(pullRequest);
    } catch (InterruptedException e) {
        log.error("executePullRequestImmediately pullRequestQueue.put", e);
    }
}
private final LinkedBlockingQueue<PullRequest> pullRequestQueue = new LinkedBlockingQueue<>();

5.4.2 拉取成功,开始处理消息

5.4.2.1 消息转换和过滤

将二进制内容转换成 MessageExt 对象;并根据tag进行过滤

public PullResult processPullResult(final MessageQueue mq, final PullResult pullResult,
    final SubscriptionData subscriptionData) {
    PullResultExt pullResultExt = (PullResultExt) pullResult;

    this.updatePullFromWhichNode(mq, pullResultExt.getSuggestWhichBrokerId());
    if (PullStatus.FOUND == pullResult.getPullStatus()) {
        ByteBuffer byteBuffer = ByteBuffer.wrap(pullResultExt.getMessageBinary());
        //TODO:将二进制消息转换成MessageExt对象
        List<MessageExt> msgList = MessageDecoder.decodes(byteBuffer);

        List<MessageExt> msgListFilterAgain = msgList;
        //TODO:根据TAG过滤消息
        if (!subscriptionData.getTagsSet().isEmpty() && !subscriptionData.isClassFilterMode()) {
            msgListFilterAgain = new ArrayList<MessageExt>(msgList.size());
            for (MessageExt msg : msgList) {
                if (msg.getTags() != null) {
                    if (subscriptionData.getTagsSet().contains(msg.getTags())) {
                        msgListFilterAgain.add(msg);
                    }
                }
            }
        }
        
        //TODO:....省略......
}

5.4.2.2 更新PullRequest对象的nextOffset属性值

//TODO: 发现了消息
case FOUND:
    long prevRequestOffset = pullRequest.getNextOffset();
    //TODO:更新nextOffset的值
    pullRequest.setNextOffset(pullResult.getNextBeginOffset());
    long pullRT = System.currentTimeMillis() - beginTimestamp;
    DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(),
        pullRequest.getMessageQueue().getTopic(), pullRT);

在前面5.3步骤中,我们举例读取了32条消息后,nextBegingOffset经过计算是32,然后将消息和offset值一并返回给消费者。所以这里PullRequestnextOffset值是32.

5.4.2.3 将读取到的消息保存到本地缓存队列ProcessQueue

//TODO:将本次读取到的所有消息(经过了TAG/sql过滤了)保存到队列中
boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());

5.4.2.4 将消息提交到线程池中进行消费(重要)

这里我先不展开说,我放到第6大步骤中详细展开

5.4.2.5 再次将 PullRequest 放到阻塞队列

//TODO: 上面是异步消费(5.4.2.4),然后这里是将PullRequest放入 队列中,这样take()方法将不会
//TODO: 阻塞,然后继续从broker拉取消息,从而达到持续从broker拉取消息
//延迟 pullInterval 时间再去拉取消息
if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
        DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
} else {
    //立即拉取消息
    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}

这里有一个 pullInterval参数,表示间隔多长时间在放入队列中(实际上就是间隔多长时间再去broker拉取消息)。当消费者消费速度比生产者快的时候,可以考虑设置这个值,这样可以避免大概率拉取到空消息。 上面将新的对象PullRequest放入队列中(这个新仅仅是因为nextOffset值变了),然后还是执行第5大步骤,当broker接收到拉取请求后,然后将根据nextOffset(值=32)读取逻辑索引的值,我当初举例的时候,是总共写入了33条消息(那么就有33条索引数据),所以,他会读取出[32,33]区间的索引数据,也就是最后一条消息索引,然后读取出真正的消息,再次计算 nextBeginOffset的值,然后返回给消费者。如此反复,从broker读取消息消费。

6.ConsumeMessageService消费消息

就是5.4.2.4步骤的逻辑

//TODO: 将消息提交到线程池中,由ConsumeMessageService 进行消费
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
    pullResult.getMsgFoundList(),
    processQueue,
    pullRequest.getMessageQueue(),
    dispatchToConsume);

由于我们的是普通消息(不是顺序消息),所以由ConsumeMessageConcurrentlyService类来消费消息。

在2.6步骤中,我们提到其内部会创建一个线程池ThreadPoolExecutor,这个线程池非常重要,消息最终将提交到这个线程池中。

但是在提交到线程池之前,还要做一件事 ---》 分割消息

6.1 分割消息

@Override
public void submitConsumeRequest(
    final List<MessageExt> msgs,
    final ProcessQueue processQueue,
    final MessageQueue messageQueue,
    final boolean dispatchToConsume) {
    //TODO:默认值是1
    final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
    //TODO: msg.size()是从broker拉取到的经过TAG/SQL过滤后的消息总和
    if (msgs.size() <= consumeBatchSize) {
        ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);
        try {
            this.consumeExecutor.submit(consumeRequest);
        } catch (RejectedExecutionException e) {
            this.submitConsumeRequestLater(consumeRequest);
        }
    } else {
        //TODO:分割消息
        for (int total = 0; total < msgs.size(); ) {
            List<MessageExt> msgThis = new ArrayList<MessageExt>(consumeBatchSize);
            for (int i = 0; i < consumeBatchSize; i++, total++) {
                if (total < msgs.size()) {
                    msgThis.add(msgs.get(total));
                } else {
                    break;
                }
            }

            ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue);
            try {
                this.consumeExecutor.submit(consumeRequest);
            } catch (RejectedExecutionException e) {
                for (; total < msgs.size(); total++) {
                    msgThis.add(msgs.get(total));
                }

                this.submitConsumeRequestLater(consumeRequest);
            }
        }
    }
}

就是比较本次拉取到的消息总数size与consumeMessageBatchMaxSize(默认=1)值的大小.如果size > consumeMessageBatchMaxSize,则按照consumeMessageBatchMaxSize将消息分割,然后分批次将消息submit到线程池中。

6.2 将消息submit到线程池中开始消费

提交到线程池中的是 ConsumeRequest对象,他是一个Runnable, 所以我们就看ConsumeRequestrun()方法就好。

获取消息监听器然后开始消费

@Override
public void run() {
    //TODO: 省略部分代码.....
    //TODO: 获取消息监听器MessageListenerConcurrently
    MessageListenerConcurrently listener = ConsumeMessageConcurrentlyService.this.messageListener;
   
    //TODO:省略部分代码.......
    try {
        //TODO:开始消费
        status = listener.consumeMessage(Collections.unmodifiableList(msgs), context);
    } catch (Throwable e) {
        log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}",
            RemotingHelper.exceptionSimpleDesc(e),
            ConsumeMessageConcurrentlyService.this.consumerGroup,
            msgs,
            messageQueue);
        hasException = true;
    }
    //TOOD:省略部分代码
}

这个监听器以及消费方法熟悉吗? 没错,他就是我们消费代码中指定的回调监听器

image.png

到这里,消费者就真正开始消费消息了。。。。。

6.3 处理消费结果

ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this);
switch (this.defaultMQPushConsumer.getMessageModel()) {
    case BROADCASTING:
        //TODO: 广播模式下,如果消费失败,则直接丢弃消息
        //消费失败才会进入循环
        for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
            MessageExt msg = consumeRequest.getMsgs().get(i);
            log.warn("BROADCASTING, the message consume failed, drop it, {}", msg.toString());
        }
        break;
    case CLUSTERING:
        List<MessageExt> msgBackFailed = new ArrayList<MessageExt>(consumeRequest.getMsgs().size());
        //消费失败,才会进入循环
        for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
            MessageExt msg = consumeRequest.getMsgs().get(i);
            //TODO:将消息发送到broker,继续看这个方法内部
            boolean result = this.sendMessageBack(msg, context);
            if (!result) {
                msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
                msgBackFailed.add(msg);
            }
        }

        //TODO: 如果消息发送都broker失败,也不能丢弃,延迟5s后再次放入线程池中
        if (!msgBackFailed.isEmpty()) {
            consumeRequest.getMsgs().removeAll(msgBackFailed);

            this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
        }
        break;
    default:
        break;
}

6.3.1 消费失败or成功

6.3.1.1 广播模式消费

广播模式消费成功,然后执行6.3.2步骤
广播模式消费失败,直接丢弃消息,什么也不做

6.3.1.2 集群模式消费

集群模式消费成功,然后执行6.3.2步骤
集群模式消费失败,则遍历消息,将每条消息重新发回到broker;如果消息发回到broker失败,也不能丢弃,则将消息重新放到ConsumeMessageConcurrentlyService内部的线程池中,等待再次消费。

这里简单说下,发回broker都做了什么?

  1. 根据消费者组构建重试topic"%RETRY%GroupName"

  2. 从commitlog再次读取出这条消息,在其properties中标记为retry。读取这条消息的目的是为了使用它的一些消息内容

  3. 设置延迟等级(再次说明消息的重试是利用延迟消息机制),第一次delayLevel默认是3,对应的延迟时间是10s,每次重试延迟等级+1;超过默认的16次后,则放入死信队列。 image.png

  4. 构建消息体对象MessageExtBrokerInner,设置topic为重试topic,设置重试次数+1

  5. 然后将消息写入到commitlog中参考消息写入过程,然后消息分发创建索引。

  6. 然后等待10s后,读取消息重试

这也说明,重试的消息虽然和原来的消息一模一样,但本质已经是新的消息了(原来的消息实际上已经被消费过了)

这里有一个疑问?
假如我提交到线程池中的消息总数是10条,我前面9条都消费成功了,但是最后一条消费失败了,那么前面9条也要重试吗?
答案是:是的。这也说明重试可能会导致重复消费。这一点还是要注意的。

6.3.2 从本地缓存队列中移除消息并持久化offset

long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());
if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
    this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
}

无论是否消费成功,都会将队列缓存的消息remove掉,然后更新offset到offset表中(offsetTable)

@Override
public void updateOffset(MessageQueue mq, long offset, boolean increaseOnly) {
    if (mq != null) {
        AtomicLong offsetOld = this.offsetTable.get(mq);
        if (null == offsetOld) {
            offsetOld = this.offsetTable.putIfAbsent(mq, new AtomicLong(offset));
        }

        if (null != offsetOld) {
            if (increaseOnly) {
                MixAll.compareAndIncreaseOnly(offsetOld, offset);
            } else {
                offsetOld.set(offset);
            }
        }
    }
}

在客户端启动的时候,会启动很多定时任务,其中在2.8.2.2步骤中启动了持久化offset的定时任务

//TODO: 延迟10s之后,每隔5s执行一次持久化任务
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        try {
            MQClientInstance.this.persistAllConsumerOffset();
        } catch (Exception e) {
            log.error("ScheduledTask persistAllConsumerOffset exception", e);
        }
    }
}, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

遍历所有queue,然后将其offset持久化到文件中。
总结下步骤:

  1. 构建UpdateConsumerOffsetRequestHeader对象,设置topic,queueid,offset
  2. 构建netty指令(RequestCode.UPDATE_CONSUMER_OFFSET),发送到broker端
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, requestHeader);
  1. broker接收到指令后,将信息保存到ConsumerOffsetManager对象的offsetTable属性中

注意:这个offsetTable是服务端的,前面那个是消费者客户端的

  1. 服务端持久化offset,在broker(BrokerController)启动的时候,也会启动很多定时任务,其中就有持久化offset的,就是将上面的offsetTable内容写到文件中;代码如下:
//TODO:延迟10s,每隔5s持久化一次offset
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        try {
            BrokerController.this.consumerOffsetManager.persist();
        } catch (Throwable e) {
            log.error("schedule persist consumerOffset error.", e);
        }
    }
}, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

持久化的默认文件路径是:$home/store/config/consumerOffset.json
文件内容如下:

image.png

至此,消费者的消费就结束了。

7.总结

消费的过程要比消息的生产复杂的多,介于作者水平有限,就总结这些,希望对大家有帮助。
限于作者水平,文中难免有错误之处,欢迎指正,勿喷,感谢感谢