ConsumerGroup
本篇主要解释下RocketMQ中ConsumerGroup的设计的思想
在RocketMQ中Consumer都是以一组ConsumerGroup的方式出现的,一般来说同一个消费者组中的消费者不会超过整个Topic下的队列数量,对应关系如下图
一个消费者可以消费多个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 | 消费者组名称 |
| subscriptionTable | Topic的订阅信息 |
| channelInfoTable | Netty 连接的channel |
| consumeType | 消费方式 Pull或者Push |
| consumerFromWhere | 获取消息的机制 比如从头获取或从最新的消息开始获取以及时间获取 |
| lastUpdateTimestamp | 最新的一个更新时间 |
从这些属性我们就可以看出来几个点
- ConsumerGroup就是用来管理我们Consumer的一个分类
- subscriptionTable可以看到 我们同一个ConsumerGroup对应Topic的订阅方式要保持一致,不然会出现混乱 因为subscriptionTable是以Topic为Key存储的
- consumeType可以看到同一个ConsumerGroup对应的消费方式也必须一样
- 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。
我们可以通过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}格式存储的,也就是代表我们同一个消费组下面所有队列的消费信息 那么得出一个结论如下图
GroupA的消费组和GroupB的消费组是完全隔离的,同一条消息是可以被多个消费组消费的。 那么第三个问题和第四个问题也就解决了