本文已参与[新人创作礼]活动,一路开启掘金创作之路。
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 检查配置
- 校验GroupName是否为空;校验GroupName是否等于DEFAULT_CONSUMER(等于的话直接抛出异常)
- 校验消费模式:集群/广播
- 校验ConsumeFromWhere
- 校验开始消费的指定时间
- 校验AllocateMessageQueueStrategy
- 校验订阅关系
- 校验是否注册消息监听
- 校验消费线程数,
consumeThreadMin
和consumeThreadMax
默认值都是20,取值区间都是 [1, 1000] - 校验本地队列缓存消息的最大数,默认是1000,取值范围是[1, 1024], 主要是做流控用的
- 校验拉取消息的时间间隔,
pullInterval
参数,默认是不存在间隔,取值范围是[0, 65535]。当消费速度比生产速度快,可以设置这个参数,避免花费大概率从broker拉取空消息 - 校验单次拉取的最大消息数,
consumeMessageBatchMaxSize
参数,默认是1,取值范围是[1, 1024] - 校验单次消费的最大消息数,
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/{group}/Offsets.json。其中{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);
将消费者组信息添加到本地客户端实例MQClientInstance
的 consumerTable
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 保存订阅组配置
- 将订阅组配置保存到本地表
subscriptionGroupTable
中 - 将订阅组配置持久化
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
就是保存了订阅组配置
- 内容如下:
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;
}
注册消费者简单总结都做了什么?
- 从消费者组表
consumerTable
中,根据key(=groupName)获取消费者组信息;如果返回null,则new ConsumerGroupInfo
,设置groupName
,ConsumeType
,MessageModel
,ConsumeFromWhere
相关参数,所以说一个消费者组具有共性;不过也从侧面说明,同一个消费者组下的消费者以第一个注册的消费者为准。 - 更新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;
}
- 更新订阅者,将订阅者信息更新到订阅表
subscriptionTable
中 - 是否通知客户端进行重平衡,在消费者第一次发起心跳的时候,注册方法中的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());
}
}
- 心跳是每隔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;
}
}
总结重平衡都做了什么?
- 获取这个topic下的所有队列(默认是4个)
- 根据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)
- 根据分配策略重新分配queue,默认是平均分配策略,参数说明请看代码注释
主要策略有:平均分配策略,环形平均策略,一致性hash策略,同机房策略
- 处理分配结果,这个请往下看
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也没有变化,所以他是不会真正触发重平衡逻辑的。
一旦重平衡分配好,拉取消息时可能最关注的是对象
PullRequest
的nextOffset
属性,后面拉取消息会多次看到
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 判断是否触发流控
流控主要是保护消费者。当消费者消费能力不够时,拉取速度太快会导致大量消息积压,很可能内存溢出
- 判断queue缓存的消息数量是否超过1000(可以根据
pullThresholdForQueue
参数配置),如果超过1000,则先不去broker拉取消息,而是先暂停50ms,然后重新将对象放入队列中(this.pullRequestQueue.put(pullRequest)
),然后重新拉取(就是上面代码中的this.pullReuqestQueue.take()
) - 判断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()方法参数简单说明一下
- 第一个参数是消费者组
- 第二个参数是topic
- 第三个参数是queueid,表示消费哪个队列
- 第四个参数是offset,表示消费的起始偏移量,这个参数比较重要的
- 第五个参数是pullBatchSize的值,默认是32
- 第六个参数是消息过滤器(消费端可以根据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;
}
接下来我们对拉取逻辑做个总结:
- 创建保存消息的容器对象GetMessageResult,它重要保存4部分内容
- 1)它会保存拉取到的信息
- 2)逻辑消费队列的
nextBeginOffset
,这个参数非常非常的重要,它就表示消费者下次消费时从哪开始读取消息(指的是消息索引,根据索引读取真正的消息),后面我们会看到这个参数; - 3)逻辑消费队列的
minOffset
,最小消费偏移量 - 4)逻辑消费队列的
maxOffset
, 最大消费偏移量
- 根据topic和queueid获取
ConsumeQueue
,它就是逻辑消费队列,保存着索引单元数据,以及最大offset, 最小offset,以及最大物理偏移量maxPhysicOffset
- 从
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
- 遍历消息的索引单元(我假设消息生产者向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,则跳出循环,不在遍历索引数据。
- 计算下一次从队列的哪个位置开始消费,也就是计算队列新的起始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条消息没有读取消费,等待下一次读取)
- 给容器对象
GetMessageResult
设置offset值(重要)
//TODO: 设置 nextBeginOffset ,消费者拿到 nextBeginOffset 后会设置到 nextOffset
//TODO: 然后消费者下次传过来,他就是这个方法的参数的 offset
getResult.setNextBeginOffset(nextBeginOffset);
getResult.setMaxOffset(maxOffset);
getResult.setMinOffset(minOffset);
nextBeginOffset=32,maxOffset=33,minOffset=0
- 将拉取结果返回给消费者客户端
至此,从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值一并返回给消费者。所以这里PullRequest
的nextOffset
值是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
, 所以我们就看ConsumeRequest
的 run()
方法就好。
获取消息监听器然后开始消费
@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:省略部分代码
}
这个监听器以及消费方法熟悉吗? 没错,他就是我们消费代码中指定的回调监听器
到这里,消费者就真正开始消费消息了。。。。。
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都做了什么?
-
根据消费者组构建重试topic
"%RETRY%GroupName"
-
从commitlog再次读取出这条消息,在其properties中标记为retry。读取这条消息的目的是为了使用它的一些消息内容
-
设置延迟等级(再次说明消息的重试是利用延迟消息机制),第一次delayLevel默认是3,对应的延迟时间是10s,每次重试延迟等级+1;超过默认的16次后,则放入死信队列。
-
构建消息体对象
MessageExtBrokerInner
,设置topic为重试topic,设置重试次数+1 -
然后将消息写入到commitlog中参考消息写入过程,然后消息分发创建索引。
-
然后等待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持久化到文件中。
总结下步骤:
- 构建
UpdateConsumerOffsetRequestHeader
对象,设置topic,queueid,offset
值 - 构建netty指令(
RequestCode.UPDATE_CONSUMER_OFFSET
),发送到broker端
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, requestHeader);
- broker接收到指令后,将信息保存到
ConsumerOffsetManager
对象的offsetTable
属性中
注意:这个offsetTable是服务端的,前面那个是消费者客户端的
- 服务端持久化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
文件内容如下:
至此,消费者的消费就结束了。
7.总结
消费的过程要比消息的生产复杂的多,介于作者水平有限,就总结这些,希望对大家有帮助。
限于作者水平,文中难免有错误之处,欢迎指正,勿喷,感谢感谢