RocketMQ源码解析之ConsumerGroup (三)

2,868 阅读4分钟

ConsumerGroup

本篇主要解释下RocketMQ中ConsumerGroup的设计的思想

在RocketMQ中Consumer都是以一组ConsumerGroup的方式出现的,一般来说同一个消费者组中的消费者不会超过整个Topic下的队列数量,对应关系如下图

image.png

一个消费者可以消费多个queue,但是一个queue只能被一个消费者所消费 这点和RabbitMQ的方式就完全不一样了。

相信你也会有以下疑问

1.ConsumerGroup到底是个什么东西?
2.Broker在什么时候创建ConsumerGroup
3.RocketMQ又是如何处理ConsumerGroup的?
4.不同的ConsumerGroup又是如何处理的?

带着这些疑问 我们去看看RocketMQ的源码具体是如何实现ConsumerGroup的对应Consumer的管理源码中对应在ConsumerManager

ConsumerManager

ConsumerManager是用来管理所有Consumer

private final ConcurrentMap<String, ConsumerGroupInfo> consumerTable =
    new ConcurrentHashMap<String, ConsumerGroupInfo>(1024);

我们可以看到 在ConsumerManager中存着一个consumerTable的消费者表 key为GroupName value就是对应ConsumerGroup的一些信息。

ConsumerGroup是什么

private final String groupName;
private final ConcurrentMap<String/* Topic */, SubscriptionData> subscriptionTable =
    new ConcurrentHashMap<String, SubscriptionData>();
private final ConcurrentMap<Channel, ClientChannelInfo> channelInfoTable =
    new ConcurrentHashMap<Channel, ClientChannelInfo>(16);
private volatile ConsumeType consumeType;
private volatile MessageModel messageModel;
private volatile ConsumeFromWhere consumeFromWhere;
private volatile long lastUpdateTimestamp = System.currentTimeMillis();
属性描述
groupName消费者组名称
subscriptionTableTopic的订阅信息
channelInfoTableNetty 连接的channel
consumeType消费方式 Pull或者Push
consumerFromWhere获取消息的机制 比如从头获取或从最新的消息开始获取以及时间获取
lastUpdateTimestamp最新的一个更新时间

从这些属性我们就可以看出来几个点

  1. ConsumerGroup就是用来管理我们Consumer的一个分类
  2. subscriptionTable可以看到 我们同一个ConsumerGroup对应Topic的订阅方式要保持一致,不然会出现混乱 因为subscriptionTable是以Topic为Key存储的
  3. consumeType可以看到同一个ConsumerGroup对应的消费方式也必须一样
  4. consumeFromWhere 可以看到消费机制也必须一样

那么我们第一条疑问就可以解决了,ConsumerGroup就是用来维护相同类型的消费者的一个消费集合,这里的相同类型可以看上面的2,3,4点。

创建时机

创建时机我们就需要通过Client到Broker的连接消息去求证了,通过ClientManagerProcessor去查看。

@Override
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request)
  throws RemotingCommandException {
  switch (request.getCode()) {
      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;
}

对应RequestCode有一种消息叫做HEART_BEAT也就是心跳消息。

RemotingCommand response = RemotingCommand.createResponseCommand(null);
HeartbeatData heartbeatData = HeartbeatData.decode(request.getBody(), HeartbeatData.class);
//创建对应的ClientChannelInfo
ClientChannelInfo clientChannelInfo = new ClientChannelInfo(
    ctx.channel(),
    heartbeatData.getClientID(),
    request.getLanguage(),
    request.getVersion()
);
//解析心跳数据中的消费数据
for (ConsumerData data : heartbeatData.getConsumerDataSet()) {
    ...//省略Topic订阅配置的一些信息
    //注册对应的消费组
    boolean changed = this.brokerController.getConsumerManager().registerConsumer(
        data.getGroupName(),
        clientChannelInfo,
        data.getConsumeType(),
        data.getMessageModel(),
        data.getConsumeFromWhere(),
        data.getSubscriptionDataSet(),
        isNotifyConsumerIdsChangedEnable
    );

}

从上面代码得知我们的ConsumerGroup是在客户端连接上来之后的心跳消息中解析出来 注册到ConsumerManager中的。

这样我们就可以通过源码解释我们的第二个问题 在客户端连接上来之后通过心跳数据创建的ConsumerGroup

ConsumerGroup消费机制

接下来我们需要知道ConsumerGroup是怎么消费消息的,这里的查看源码的思路应该采用最简单的思路去按,我们消费的方式有两种,第一种Pull的方式第二种是Push的方式 显然第一种的方式更容易追中源码 通过Client端发来的消息去跟踪消费过程。

简单分享我的源码查看逻辑,比如我们需要查看消费消息的机制 直接找到RequestCode,因为消费必然有通讯 通讯都是通过RequestCode区分意图那我们直接查看对应的ReqeustCode。

image.png

我们可以通过PULL_MESSAGE的Code找到Broker中的处理机制PullMessageProcessor, PullMessageProcessor中的处理逻辑代码实在太长我这里就贴出一部分跟ConsumerGroup相关的东西。

//获取消息
final GetMessageResult getMessageResult =
   this.brokerController.getMessageStore().getMessage(requestHeader.getConsumerGroup(), requestHeader.getTopic(),
       requestHeader.getQueueId(), requestHeader.getQueueOffset(), requestHeader.getMaxMsgNums(), messageFilter);
       
     
       
 ....//省略部分逻辑
boolean storeOffsetEnable = brokerAllowSuspend;
storeOffsetEnable = storeOffsetEnable && hasCommitOffsetFlag;
storeOffsetEnable = storeOffsetEnable
   && this.brokerController.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE;
if (storeOffsetEnable) {
//保存消费的commitoffset
this.brokerController.getConsumerOffsetManager().commitOffset(RemotingHelper.parseChannelRemoteAddr(channel),
       requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getCommitOffset());
}
    
       

关于消息获取其实之前文件存储哪里已经简答描述过ConsumeQueue获取消息,这里我们看下ConsumerOffsetManager如何处理ConsumerGroup的消费。

public void commitOffset(final String clientHost, final String group, final String topic, final int queueId,
    final long offset) {
    // topic@group
    String key = topic + TOPIC_GROUP_SEPARATOR + group;
    this.commitOffset(clientHost, key, queueId, offset);
}

private void commitOffset(final String clientHost, final String key, final int queueId, final long offset) {
    ConcurrentMap<Integer, Long> map = this.offsetTable.get(key);
    if (null == map) {
        map = new ConcurrentHashMap<Integer, Long>(32);
        map.put(queueId, offset);
        this.offsetTable.put(key, map);
    } else {
        Long storeOffset = map.put(queueId, offset);
        if (storeOffset != null && offset < storeOffset) {
            log.warn("[NOTIFYME]update consumer offset less than store. clientHost={}, key={}, queueId={}, requestOffset={}, storeOffset={}", clientHost, key, queueId, offset, storeOffset);
        }
    }
}

这里的ConsumerOffsetManager是继承于ConfigManager文件存储在config目录下的consumerOffset.json,存储的都是消费的进度。

{
        "offsetTable":{
                "TopicTest@GroupTest":{0:365,1:399,2:480,3:480}
        }

也就是存储消息的消费的进度是topic@group:{queueId:offset}格式存储的,也就是代表我们同一个消费组下面所有队列的消费信息 那么得出一个结论如下图

image.png

GroupA的消费组和GroupB的消费组是完全隔离的,同一条消息是可以被多个消费组消费的。 那么第三个问题和第四个问题也就解决了