带着问题去研究中间件,想想自己实现如何实现
前提
通过架构可以知道下面角色之间的对应关系
- 主题:消息队列(MessageQueue)= 1:n
- 主题:消息生产者 = 1:n (n>=1)
- 主题:消息消费者 = 1:n(n>=1)
问题
围绕着上面的关系那么就会存在三类问题
消费者
- 消费组角度:一个消费组中多个消费者是如何对消息队列(1个主题多个消息队列)进行负载消费的。
- 消费者角度:一个消费者中多个线程又是如何协作(并发)的消费分配给该消费者的消息队列中的消息呢?
- 增量消费方面:消息消费进度如何保存,包括MQ是如何知道消息是否正常被消费了。
重要的类
不熟悉的话,可以边看变回来查看具体的功能
- DefaultMQPushConsumerImpl :消息消息者默认实现类,应用程序中直接用该类的实例完成消息的消费,并回调业务方法。
- RebalanceImpl 字面上的意思(重新平衡)也就是消费端消费者与消息队列的重新分布,与消息应该分配给哪个消费者消费息息相关。
- MQClientInstance 消息客户端实例,负载与MQ服务器(Broker,Nameserver)交互的网络实现
- PullAPIWrapper Pull与Push在RocketMQ中,其实就只有Pull模式,所以Push其实就是用pull封装一下
- MessageListenerInner 消费消费回调类,当消息分配给消费者消费时,执行的业务代码入口
- OffsetStore 消息消费进度保存
- ConsumeMessageService 消息消费逻辑
源码
Consumer 启动
入口:DefaultMQPushConsumer 的 start 方法
RebalanceImpl启动
依赖的 MQClientFactory 的初始化
//IMP 决定了后面的 MQInstance 也就是与其他组件的交换逻辑
//如果是集群消费模式,如果instanceName为默认值 "DEFAULT",那么改成 UtilAll.getPid() + "#" + System.nanoTime()
if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
this.defaultMQPushConsumer.changeInstanceNameToPID();
}
/*
* K2 3 获取MQClientManager实例,然后根据clientId获取或者创建CreateMQClientInstance实例,并赋给mQClientFactory变量
*
* MQClientInstance封装了RocketMQ底层网络处理API,Producer、Consumer都会使用到这个类,是Producer、Consumer与NameServer、Broker 打交道的网络通道。
* 因此,同一个clientId对应同一个MQClientInstance实例就可以了,即同一个应用中的多个producer和consumer使用同一个MQClientInstance实例即可。
*/
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
rebalanceImpl初始化,主要其中注入了 MQClientFactory,那岂不是一个 PushConsuemr 实例,必然有一个次对象,这个对象的功能,后续说明
/*
* K2 4 设置负载均衡服务的相关属性
* RebalanceImpl 要解决的问题:对 MessageQueue 资源的重平衡
*/
this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
PullAPIWrapper 对象构建
此对象的构建中,也存在一个 mQClientFactory
/*
* IMP 核心组件,无论是推还是拉都是使用此组件来执行的
* K2 5 创建消息拉取核心对象PullAPIWrapper,封装了消息拉取及结果解析逻辑的API0º
*/
this.pullAPIWrapper = new PullAPIWrapper(
mQClientFactory,
this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
//为PullAPIWrapper注册过滤消息的钩子函数
this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
OffsetStore 对象构建
同样的也 MQClientFactory 注入到对象中,同时此处,针对不同的消费方式,消息存储在不同的地方本地和远端 Broker( 如果 Broker 不进行通信的话,岂不是会丢失进度呢??? )
/* IMP OffsetStore 是用于记录当前消费者消费进度的一个组件
* LocalFileOffsetStore:顾名思义,就是将消费进度存储在 Consumer 本地,Consumer 会在磁盘上生成文件以保存进度。
* RemoteBrokerOffsetStore:将消费进度保存在远端的 Broker。
*
* K2 6 根据消息模式设置不同的OffsetStore,用于实现消费者的消息消费偏移量offset的管理
*/
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
//根据不用的消费模式选择不同的OffsetStore实现
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
//如果是广播消费模式,则是LocalFileOffsetStore,消息消费进度即offset存储在本地磁盘中。
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:
//如果是集群消费模式,则是RemoteBrokerOffsetStore,消息消费进度即offset存储在远程broker中。
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
ConsumeMessageService 消费管理构建
/*
* K2 8 根据消息监听器的类型创建不同的消息消费服务
*/
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
//如果是MessageListenerOrderly类型,则表示顺序消费,创建ConsumeMessageOrderlyService
this.consumeOrderly = true;
this.consumeMessageService =
new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
//如果是MessageListenerConcurrently类型,则表示并发消费,创建ConsumeMessageOrderlyService
this.consumeOrderly = false;
this.consumeMessageService =
new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}
//启动消息消费服务
this.consumeMessageService.start();
MQClientInstance 启动
- 里面干的事情,各种定时任务之类的,同时这个对象里面包含了很多重要对象,包含了,各种消费者对象,路由信息,broker 信息,以及拉取服务等等,
定时任务
- 具体任务如下
PullMessageService 启动
- 是一个现成,那么肯定是一个自旋的任务去执行
@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");
}
private void pullMessage(final PullRequest pullRequest) {
//获取消费者
final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
if (consumer != null) {
DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
impl.pullMessage(pullRequest);
} else {
log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
}
}
public MQConsumerInner selectConsumer(final String group) {
return this.consumerTable.get(group);
}
- 上面说明了啥呢,cnsumerTable 是一个 Map结构,如果一个应用创建了同一个消费者组的多个消费者,此处会怎样呢,必然只会选择一个,所以,同一个应用增加多个消费者并不会 提高消费效率。但是实际上看了,对象而言,这个 consumerTable 是私有变量,根本不会进行重用,应该两个都会消费的,这个地方应该有什么问题,请教一下 大佬问问
-
看到这里,终于知道了,其实内部还是启动的其他线程(PullMessageService)来执行拉取消息。那么 Consumer 和 PullMessageService 的关系是什么样子的呢?
- 一个应用程序(消费端),一个消费组 一个 DefaultMQPushConsumerImpl ,同一个IP:端口,会有一个MQClientInstance ,而每一个MQClientInstance中持有一个PullMessageServive实例,故可以得出如下结论:同一个应用程序中,如果存在多个消费组,那么多个DefaultMQPushConsumerImpl 的消息拉取,都需要依靠一个PullMessageServive
- 简言之就是,一个 Consumer 对应一个 MQClientIntstance,也对应一个 PullMessageService,不同的 Consumer 对应的 PullMessageService 不一样。和大佬沟通之后,发现,之前的版本都是Producer 和 Consumer 都是底层使用同一个 MQClientInstance,但是呢,现在都是一对一的,即一个 Consumer 使用一个 MQClientInstance,也就底层一个 PullMessageService 了。终于解惑了。。。
PullMessageService 是依赖内存队列的请求进行拉取消息的,那么这个请求是什么时候加入到这个对象里面的呢???
PullMessageService 拉取消息
-
上面的循环不断的从队列中获取 PullRequest
private void pullMessage(final PullRequest pullRequest) { final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup()); if (consumer != null) { DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer; impl.pullMessage(pullRequest); } else { log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest); } } -
上面的拉取消息,最终回到 DefaultMQPushConsumerImpl的pullMessage
DefualtMQPushConsumerImpl 的 pullMessage 方法
- 首先获取PullRequest的 处理队列ProcessQueue,然后更新该消息队列最后一次拉取的时间。
// IMP
// ProcessQueue 内部会通过 TreeMap 来存放这些暂时还没有被消费的 Message,TreeMap 是一个用红黑树实现的有序 Map。
// Key 是消息在当前 ProcessQueue 所对应的 MessageQueue 中的偏移量,Value 就是 Message 自身。
final ProcessQueue processQueue = pullRequest.getProcessQueue();
if (processQueue.isDropped()) {
log.info("the pull request[{}] is dropped.", pullRequest.toString());
return;
}
pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
- 如果消费者 服务状态不为ServiceState.RUNNING,或当前处于暂停状态,默认延迟3秒再执行(PullMessageService.executePullRequestLater)。 如何实现延迟执行呢,简单就使用 Sleep 方法。等会看看里面具体如何实现延迟的操作。
try {
this.makeSureStateOK();
} catch (MQClientException e) {
log.warn("pullMessage exception, consumer state not ok", e);
//延迟执行
this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
return;
}
//暂停
if (this.isPause()) {
log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
return;
}
延迟消息实现
-
使用定时任务线程池,然后过了多久之后,重新放回到 PullRequest 中,可见这个延迟也是个大概的时间,具体需要等消费者也就是 PullMessageService 拉取到才去执行
public void executePullRequestLater(final PullRequest pullRequest, final long timeDelay) { if (!isStopped()) { this.scheduledExecutorService.schedule(new Runnable() { @Override public void run() { PullMessageService.this.executePullRequestImmediately(pullRequest); } }, timeDelay, TimeUnit.MILLISECONDS); } else { log.warn("PullMessageServiceScheduledThread has shutdown"); } } //PullMessageService.executePullRequestImmediately /** * PullMessageService的方法 * 下一次消息拉取 * * @param pullRequest 拉取请求 */ public void executePullRequestImmediately(final PullRequest pullRequest) { try { //存入pullRequestQueue集合,等待下次拉取 this.pullRequestQueue.put(pullRequest); } catch (InterruptedException e) { log.error("executePullRequestImmediately pullRequestQueue.put", e); } }
-
拉取消息进行限流限速
- 消息数量达到阔值(默认1000个)
- 消息体总大小(默认100m)
long cachedMessageCount = processQueue.getMsgCount().get();
long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
// 数量
if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
if ((queueFlowControlTimes++ % 1000) == 0) {
log.warn(
"the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
}
return;
}
//消息体大小
if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
if ((queueFlowControlTimes++ % 1000) == 0) {
log.warn(
"the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
}
return;
}
非顺序消息拉取
- 如果我们自己实现的话,肯定是,获取订阅的 broker 地址信息(通过版本进行增量拉取,同时设置最大拉取次数,防止,过多拉取失败,导致消费太慢),然后去 broker 中拉取消息,如果拉取失败了,进行重试处理。看看 MQ 是如何实现的呢。
-
获取主题订阅信息
final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic()); if (null == subscriptionData) { this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); log.warn("find the consumer's subscription failed, {}", pullRequest); return; }-
主题的订购信息,实体类
public class SubscriptionData implements Comparable<SubscriptionData> { public final static String SUB_ALL = "*"; private boolean classFilterMode = false; private String topic; private String subString; private Set<String> tagsSet = new HashSet<String>(); private Set<Integer> codeSet = new HashSet<Integer>(); private long subVersion = System.currentTimeMillis(); private String expressionType = ExpressionType.TAG; }
-
-
构造回调方法,涉及到重试操作
-
获取偏移量,好吧,那上面的当前拉取时间是干嘛用的呢???如果不记得可以返回去看看 setLastPullTimestamp
-
如果是集群模式,就去内存中获取偏移量
long commitOffsetValue = 0L; if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) { //此处会涉及本地获取还是集群获取 commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY); if (commitOffsetValue > 0) { commitOffsetEnable = true; } }
-
-
拉取消息,好嘛,最后还是交给了别人来进行拉取,自己就是构造了请求参数等等信息,不过也是,专业的事儿找专业的人来干,后续可以很好的扩展
//IMP 拉取消息 this.pullAPIWrapper.pullKernelImpl( pullRequest.getMessageQueue(), subExpression, subscriptionData.getExpressionType(), subscriptionData.getSubVersion(), pullRequest.getNextOffset(), this.defaultMQPushConsumer.getPullBatchSize(), sysFlag, commitOffsetValue, BROKER_SUSPEND_MAX_TIME_MILLIS, CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND, CommunicationMode.ASYNC, pullCallback ); -
就是进一步获取 topic 和 broker 的地址信息,然后交由 MQClientAPIImpl 进行拉取消息
//MQClientAPIImpl.pullMessage /** * IMP 拉取消息 * @param addr * @param requestHeader * @param timeoutMillis * @param communicationMode * @param pullCallback * @return * @throws RemotingException * @throws MQBrokerException * @throws InterruptedException */ public PullResult pullMessage( final String addr, final PullMessageRequestHeader requestHeader, final long timeoutMillis, final CommunicationMode communicationMode, final PullCallback pullCallback ) throws RemotingException, MQBrokerException, InterruptedException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader); switch (communicationMode) { case ONEWAY: assert false; return null; case ASYNC: this.pullMessageAsync(addr, request, timeoutMillis, pullCallback); return null; case SYNC: return this.pullMessageSync(addr, request, timeoutMillis); default: assert false; break; } return null; }-
对于异步和同步而言,同步是等待返回结果,而异步,就是在 DefaultConsumerPushImpl 拉取消息的时候,创建的回调函数进行处理,其实这也是异步的常用套路。使用回调来进行结果处理
private void pullMessageAsync( final String addr, final RemotingCommand request, final long timeoutMillis, final PullCallback pullCallback ) throws RemotingException, InterruptedException { /* * 基于netty给broker发送异步消息,设置一个InvokeCallback回调对象 * * InvokeCallback#operationComplete方法将会在得到结果之后进行回调,内部调用pullCallback的回调方法 */ this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { /** * 异步执行的回调方法 */ @Override public void operationComplete(ResponseFuture responseFuture) { //返回命令对象 RemotingCommand response = responseFuture.getResponseCommand(); if (response != null) { try { //解析响应获取结果 PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response, addr); assert pullResult != null; //如果解析到了结果,那么调用pullCallback#onSuccess方法处理 pullCallback.onSuccess(pullResult); } catch (Exception e) { //出现异常则调用pullCallback#onException方法处理异常 pullCallback.onException(e); } } else { //没有结果,都调用onException方法处理异常 if (!responseFuture.isSendRequestOK()) { //发送失败 pullCallback.onException(new MQClientException("send request failed to " + addr + ". Request: " + request, responseFuture.getCause())); } else if (responseFuture.isTimeout()) { //超时 pullCallback.onException(new MQClientException("wait response from " + addr + " timeout :" + responseFuture.getTimeoutMillis() + "ms" + ". Request: " + request, responseFuture.getCause())); } else { pullCallback.onException(new MQClientException("unknown reason. addr: " + addr + ", timeoutMillis: " + timeoutMillis + ". Request: " + request, responseFuture.getCause())); } } } }); }
-
-
回调方法
-
这个时候是不是想到几个点,如果你自己写的时候,
- 不同响应如何处理,成功,失败,异常
- 内存队列进行消费,如果一个拉取请求太久怎么办,或者 broker 超时了,怎么去重试,要求肯定是不能阻塞后续的消费
PullCallback pullCallback = new PullCallback() { @Override public void onSuccess(PullResult pullResult) { if (pullResult != null) { /* * K2 1 处理pullResult,进行消息解码、过滤以及设置其他属性的操作 */ pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult, subscriptionData); switch (pullResult.getPullStatus()) { case FOUND: //拉取的起始offset long prevRequestOffset = pullRequest.getNextOffset(); //设置下一次拉取的起始offset到PullRequest中 pullRequest.setNextOffset(pullResult.getNextBeginOffset()); //增加拉取耗时 long pullRT = System.currentTimeMillis() - beginTimestamp; DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), pullRT); long firstMsgOffset = Long.MAX_VALUE; //如果没有消息 if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) { /* * 立即将拉取请求再次放入PullMessageService的pullRequestQueue中,PullMessageService是一个线程服务 * PullMessageService将会循环的获取pullRequestQueue中的pullRequest然后向broker发起新的拉取消息请求 * 进行下次消息的拉取 */ DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); } else { //获取第一个消息的offset firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset(); //增加拉取tps DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size()); /* * K2 2 将拉取到的所有消息,存入对应的processQueue处理队列内部的msgTreeMap中 */ boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList()); /* * K2 3 通过consumeMessageService将拉取到的消息构建为ConsumeRequest,然后通过内部的consumeExecutor线程池消费消息 * consumeMessageService有ConsumeMessageConcurrentlyService并发消费和ConsumeMessageOrderlyService顺序消费两种实现 */ DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest( pullResult.getMsgFoundList(), processQueue, pullRequest.getMessageQueue(), dispatchToConsume); /* * K2 4 获取配置的消息拉取间隔,默认为0,则等待间隔时间后将拉取请求再次放入pullRequestQueue中,否则立即放入pullRequestQueue中 * 进行下次消息的拉取 */ if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) { /* * 将executePullRequestImmediately的执行放入一个PullMessageService的scheduledExecutorService延迟任务线程池中 * 等待给定的延迟时间到了之后再执行executePullRequestImmediately方法 */ DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval()); } else { /* * 立即将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取 */ 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); //立即将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取 DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); break; //请求offset不合法,过大或者过小 case OFFSET_ILLEGAL: log.warn("the pull request offset illegal, {} {}", pullRequest.toString(), pullResult.toString()); //更新下一次拉取偏移量,这个下一次的开始偏移是broker那边进行返回的 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); //持久化offset 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; } } } /* * 出现异常,延迟3s将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取 */ @Override public void onException(Throwable e) { if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { log.warn("execute the pull request exception", e); } DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); } };- 上面无非就是针对不同的情况,来进行相应的处理,下面主要针对成功和异常的情况看看,成功之后如何处理,异常之后如何重试
-
-
拉取成功之后的处理
//获取第一个消息的offset firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset(); //增加拉取tps DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size()); /* * K2 2 将拉取到的所有消息,存入对应的processQueue处理队列内部的msgTreeMap中 */ boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList()); /* * K2 3 通过consumeMessageService将拉取到的消息构建为ConsumeRequest,然后通过内部的consumeExecutor线程池消费消息 * consumeMessageService有ConsumeMessageConcurrentlyService并发消费和ConsumeMessageOrderlyService顺序消费两种实现 */ DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest( pullResult.getMsgFoundList(), processQueue, pullRequest.getMessageQueue(), dispatchToConsume); /* * K2 4 获取配置的消息拉取间隔,默认为0,则等待间隔时间后将拉取请求再次放入pullRequestQueue中,否则立即放入pullRequestQueue中 * 进行下次消息的拉取 */ if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) { /* * 将executePullRequestImmediately的执行放入一个PullMessageService的scheduledExecutorService延迟任务线程池中 * 等待给定的延迟时间到了之后再执行executePullRequestImmediately方法 */ DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval()); } else { /* * 立即将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取 */ 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;-
拉取到消息之后,放到消费队列 processQueue,这还这么操作,拉取到难道不消费吗,各种内存队列,生产-消费者模式呀,生产者是当前的消费者 PushConsumer,内存队列就是加入的地方,那么消费者是谁呢,如何通知消费者来进行消费消息呢,后续可以看到,此方法中底层代码可以看到将消息加入一个 msgTreeMap 中,同时增加了一些统计数据
/** * IMP putMessage * * 该方法将拉取到的所有消息,存入对应的processQueue处理队列内部的msgTreeMap中。 * * 返回是否需要分发消费dispatchToConsume,当当前processQueue的内部的msgTreeMap中 * 有消息并且consuming=false,即还没有开始消费时,将会返回true。 * * dispatchToConsume对并发消费无影响,只对顺序消费有影响。 * @param msgs 一批消息 * @return 是否需要分发消费,当当前processQueue的内部的msgTreeMap中有消息并且consuming=false,即还没有开始消费时,将会返回true */ public boolean putMessage(final List<MessageExt> msgs) { boolean dispatchToConsume = false; try { //尝试加写锁防止并发 this.treeMapLock.writeLock().lockInterruptibly(); try { int validMsgCnt = 0; for (MessageExt msg : msgs) { //当该消息的偏移量以及该消息存入msgTreeMap // 此处肯定是两个数据结构的关联点 MessageExt old = msgTreeMap.put(msg.getQueueOffset(), msg); if (null == old) { //如果集合没有这个offset的消息,那么增加统计数据 validMsgCnt++; this.queueOffsetMax = msg.getQueueOffset(); msgSize.addAndGet(msg.getBody().length); } } //消息计数 msgCount.addAndGet(validMsgCnt); //当前processQueue的内部的msgTreeMap中有消息并且consuming=false,即还没有开始消费时,dispatchToConsume = true,consuming = true if (!msgTreeMap.isEmpty() && !this.consuming) { dispatchToConsume = true; this.consuming = true; } //计算broker累计消息数量 if (!msgs.isEmpty()) { MessageExt messageExt = msgs.get(msgs.size() - 1); String property = messageExt.getProperty(MessageConst.PROPERTY_MAX_OFFSET); if (property != null) { long accTotal = Long.parseLong(property) - messageExt.getQueueOffset(); if (accTotal > 0) { this.msgAccCnt = accTotal; } } } } finally { this.treeMapLock.writeLock().unlock(); } } catch (InterruptedException e) { log.error("putMessage exception", e); } return dispatchToConsume; } -
消费消息服务提交(只看非顺序的)
- 上面是降拉取到 的消息存储到了 treeMap 中,其实如果这个接口是阻塞队列的话,是可以接受一个线程不断自旋获取,或者阻塞等待获取,如果有消息那么就去消费,但是通过源码分析,是利用 treeMap 的有序性,以及查询速度快的情况来作为存储消息的结构,那么就需要一个阻塞队列,来通知,消费者来进行消费,所以,此处就会有消息服务提交的一个步骤。
- 通过下面源码发现,是直接将 返回的消息,构造成一个 runnable 然后交给了线程池执行,底层也是一种生产-消费模式。有点奇怪了,那上面的 msgTreeMap 存储消息干嘛呢???
@Override public void submitConsumeRequest( final List<MessageExt> msgs, final ProcessQueue processQueue, final MessageQueue messageQueue, final boolean dispatchToConsume) { //单次批量消费的数量,默认1 // consumeMessageBatchMaxSize是什么意思呢?他的字面意思就是单次批量消费的数量,实际上它代表着每次发送给 // 消息监听器MessageListenerOrderly或者MessageListenerConcurrently的consumeMessage方法中的参数List msgs中的最多的消息数量。 // // consumeMessageBatchMaxSize默认值为1,所以说,无论是并发消费还是顺序消费,每次的consumeMessage方法的执行, // msgs集合默认都只有一条消息。同理,如果把它设置为其他值n,无论是并发消费还是顺序消费,每次的consumeMessage的执行,msgs集合默认都最多只有n条消息。 // final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize(); /* * 如果消息数量 <= 单次批量消费的数量,那么直接全量消费 */ if (msgs.size() <= consumeBatchSize) { //构建消费请求,将消息全部放进去 ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue); try { // 重点 //将请求提交到consumeExecutor线程池中进行消费 this.consumeExecutor.submit(consumeRequest); } catch (RejectedExecutionException e) { //提交的任务被线程池拒绝,那么延迟5s进行提交,而不是丢弃 this.submitConsumeRequestLater(consumeRequest); } } /* * 如果消息数量 > 单次批量消费的数量,那么需要分割消息进行分批提交 */ else { //遍历 for (int total = 0; total < msgs.size(); ) { //一批消息集合,每批消息最多consumeBatchSize条,默认1 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 consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue); try { //将请求提交到consumeExecutor线程池中进行消费 this.consumeExecutor.submit(consumeRequest); } catch (RejectedExecutionException e) { //被拒绝之后,把所有的消息都加入到的msgThis这个集合中,整体延迟5s进行执行 for (; total < msgs.size(); total++) { msgThis.add(msgs.get(total)); } //提交的任务被线程池拒绝,那么所有后面的任务都延迟5s进行提交,而不是丢弃 this.submitConsumeRequestLater(consumeRequest); } } } }- 每次超了每次消费的消息个数,那么就对消息进行分批处理,然后依次处理,不过这个地方写的,和我们写的也没啥区别
-
ConsumeRequest
单独拎出来是比较重要,也是每一批消息,消费的任务模型,直接上源码,又臭又长,还是慢慢分析吧,因为是 Runnable,懂得都懂
-
既然消费一批消息, 即 processQueue,那么一开始的一些校验肯定必不可少了,如果是重试消息的话,还原会真正的主题,咦??,所以,重试消息的主题在拉取到之后,broker 端不进行修改主题信息吗,为啥此处还有进行调整呢???后续重试消息的地方讲解
//如果处理队列被丢弃,那么直接返回,不再消费,例如负载均衡时该队列被分配给了其他新上线的消费者,尽量避免重复消费 if (this.processQueue.isDropped()) { log.info("the message queue not be able to consume, because it's dropped. group={} {}", ConsumeMessageConcurrentlyService.this.consumerGroup, this.messageQueue); return; } //重置重试topic defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());public void resetRetryAndNamespace(final List<MessageExt> msgs, String consumerGroup) { //获取重试topic final String groupTopic = MixAll.getRetryTopic(consumerGroup); for (MessageExt msg : msgs) { //尝试通过PROPERTY_RETRY_TOPIC属性获取每个消息的真实topic String retryTopic = msg.getProperty(MessageConst.PROPERTY_RETRY_TOPIC); //如果该属性不为null,并且重试topic和消息的topic相等,则表示当前消息是重试消息 if (retryTopic != null && groupTopic.equals(msg.getTopic())) { //那么设置消息的topic为真实topic,即还原回来 msg.setTopic(retryTopic); } if (StringUtils.isNotEmpty(this.defaultMQPushConsumer.getNamespace())) { msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace())); } } } -
消费消息前的扩展点,钩子函数
/* * K2 2 如果有消费钩子,那么执行钩子函数的前置方法consumeMessageBefore * 我们可以注册钩子ConsumeMessageHook,再消费消息的前后调用 */ ConsumeMessageContext consumeMessageContext = null; if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) { consumeMessageContext = new ConsumeMessageContext(); consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace()); consumeMessageContext.setConsumerGroup(defaultMQPushConsumer.getConsumerGroup()); consumeMessageContext.setProps(new HashMap<String, String>()); consumeMessageContext.setMq(messageQueue); consumeMessageContext.setMsgList(msgs); consumeMessageContext.setSuccess(false); ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext); } -
调用业务的监听器方法来处理消息
try { if (msgs != null && !msgs.isEmpty()) { //循环设置每个消息的起始消费时间 for (MessageExt msg : msgs) { MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis())); } } /* * K2 3 调用listener#consumeMessage方法,进行消息消费,调用实际的业务逻辑,返回执行状态结果 * 有两种状态ConsumeConcurrentlyStatus.CONSUME_SUCCESS 和 ConsumeConcurrentlyStatus.RECONSUME_LATER */ status = listener.consumeMessage(Collections.unmodifiableList(msgs), context); } catch (Throwable e) { log.warn(String.format("consumeMessage exception: %s Group: %s Msgs: %s MQ: %s", RemotingHelper.exceptionSimpleDesc(e), ConsumeMessageConcurrentlyService.this.consumerGroup, msgs, messageQueue), e); //抛出异常之后,设置异常标志位 hasException = true; } -
根据不同的消费状态 status 进行 处理,主要还是去判断,超时了,异常的情况,如何去处理呢
/* * K2 4 对返回的执行状态结果进行判断处理 */ //计算消费时间 long consumeRT = System.currentTimeMillis() - beginTimestamp; //如status为null if (null == status) { //如果业务的执行抛出了异常 if (hasException) { //设置returnType为EXCEPTION returnType = ConsumeReturnType.EXCEPTION; } else { //设置returnType为RETURNNULL returnType = ConsumeReturnType.RETURNNULL; } //如消费时间consumeRT大于等于consumeTimeout,默认15min } else if (consumeRT >= defaultMQPushConsumer.getConsumeTimeout() * 60 * 1000) { //设置returnType为TIME_OUT returnType = ConsumeReturnType.TIME_OUT; //如status为RECONSUME_LATER,即消费失败 } else if (ConsumeConcurrentlyStatus.RECONSUME_LATER == status) { //设置returnType为FAILED returnType = ConsumeReturnType.FAILED; //如status为CONSUME_SUCCESS,即消费成功 } else if (ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status) { //设置returnType为SUCCESS,即消费成功 returnType = ConsumeReturnType.SUCCESS; } //兜底策略,默认返回,没有返回的话,就认为是消费失败,等会在消费 //如果status为null if (null == status) { log.warn("consumeMessage return null, Group: {} Msgs: {} MQ: {}", ConsumeMessageConcurrentlyService.this.consumerGroup, msgs, messageQueue); //将status设置为RECONSUME_LATER,即消费失败 status = ConsumeConcurrentlyStatus.RECONSUME_LATER; } -
如果有钩子,后续执行钩子方法
/* * K2 5 如果有消费钩子,那么执行钩子函数的后置方法consumeMessageAfter * 我们可以注册钩子ConsumeMessageHook,在消费消息的前后调用 */ if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) { consumeMessageContext.setStatus(status.toString()); consumeMessageContext.setSuccess(ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status); ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext); } -
上面记录的消息消费之后的,状态, 但是针对不同的结果之后,没有具体的处理策略,显然不合适,只是执行了,消费前后的钩子方法,那么真正的处理方法如下,也是针对,成功,超时,异常,重试的后续一些操作,为啥要写一个方法呢,因为后续的特别多的逻辑需要处理,这样分开来说,每个方法指责不同,而且方便单侧
/* * K2 6 如果处理队列没有被丢弃,那么调用ConsumeMessageConcurrentlyService#processConsumeResult方法处理消费结果,包含重试等逻辑 */ if (!processQueue.isDropped()) { ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this); } else { log.warn("processQueue is dropped without process consume result. messageQueue={}, msgs={}", messageQueue, msgs); }
processConsumeResult
涉及:消息消费重试机制
从方法名称就可以得知,次方法主要是用于处理消费结果的,针对消费结果的响应,进一步处理
/*
* K2 1 判断消费状态,设置ackIndex的值
* 消费成功: ackIndex = 消息数量 - 1
* 消费失败: ackIndex = -1
*/
switch (status) {
//如果消费成功
case CONSUME_SUCCESS:
//限制回复的上限,因为当前还在处理一条数据,所以认为之前的量就是-1 条的消息
//如果大于等于消息数量,则设置为消息数量减1
//初始值为Integer.MAX_VALUE,因此一般都会设置为消息数量减1
if (ackIndex >= consumeRequest.getMsgs().size()) {
ackIndex = consumeRequest.getMsgs().size() - 1;
}
//消费成功的个数,即消息数量
int ok = ackIndex + 1;
//消费失败的个数,即0
int failed = consumeRequest.getMsgs().size() - ok;
//统计
this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), ok);
this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), failed);
break;
//如果消费失败
case RECONSUME_LATER:
//决定起始索引
//ackIndex初始化为-1
ackIndex = -1;
//统计
this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(),
consumeRequest.getMsgs().size());
break;
default:
break;
}
- 上面的操作,就记录 ackIndex,可以理解成,哪些消息消费成功,很显然,-1 代表了所有的没有消费成功,具体可见后面的逻辑
/*
* K2 2 判断消息模式,处理消费失败的情况
* 广播模式:打印日志
* 集群模式:向broker发送当前消息作为延迟消息,等待重试消费
*/
switch (this.defaultMQPushConsumer.getMessageModel()) {
//广播模式下
case BROADCASTING:
//从消费成功的消息在消息集合中的索引+1开始,仅仅是对于消费失败的消息打印日志,并不会重试
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());
//消费成功的消息在消息集合中的索引+1开始,遍历消息
for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
//针对每条信息进行依次处理
//获取该索引对应的消息
MessageExt msg = consumeRequest.getMsgs().get(i);
/*
* 2.1 消费失败后,将该消息重新发送至重试队列,延迟消费
*/
boolean result = this.sendMessageBack(msg, context);
//如果执行发送失败
if (!result) {
//设置重试次数+!
msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
//加入失败的集合
msgBackFailed.add(msg);
}
}
// 回复 ack 失败的消息,直接在本地进行执行就好了,防止 broker 坏了,消息无法进行消费
if (!msgBackFailed.isEmpty()) {
//从consumeRequest中移除消费失败并且发回broker失败的消息
consumeRequest.getMsgs().removeAll(msgBackFailed);
/*
* 2.2 调用submitConsumeRequestLater方法,延迟5s将sendMessageBack执行失败的消息再次提交到consumeExecutor进行消费
*/
this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
}
break;
default:
break;
}
-
CONSUME_SUCCESS:
- ackIndex = msg.size()-1,ackIndex + 1即所有的消息消费成,也就不需要重新消费
-
RECONSUME_LATER
-
ackIndex = -1,for (int i = ackIndex + 1;意味着,从第一条数据开始全部消费失败
-
【sendMessageBack】,延迟 5s 后重新在消费端重新消费
-
sendMessageBack消费失败
- 直接在本地处理该消息,默认是延迟 5s 后进行消费,怎么理解呢,既然发送不成功,那么就默认是消费成功,就在本地重新消费一下试试【submitConsumeRequestLater】
-
-
sendMessageBack
public void sendMessageBack(MessageExt msg, int delayLevel, final String brokerName)
throws RemotingException, MQBrokerException, InterruptedException, MQClientException {
try {
//K1 先根据 brokerName 得到 broker 地址信息,然后通过网络发送到指定的 Broker上。
String brokerAddr = (null != brokerName) ? this.mQClientFactory.findBrokerAddressInPublish(brokerName)
: RemotingHelper.parseSocketAddressAddr(msg.getStoreHost());
this.mQClientFactory.getMQClientAPIImpl().consumerSendMessageBack(brokerAddr, msg,
this.defaultMQPushConsumer.getConsumerGroup(), delayLevel, 5000, getMaxReconsumeTimes());
} catch (Exception e) {
// IMP 原来消费者还会产生消息给 Broker 呀
//K1 如果上述过程失败,则创建一条新的消息重新发送给 Broker,此时新消息的主题为重试主题:"%RETRY%" + ConsumeGroupName,
// 注意,这里的主题和原先的消息主题没任何关系而是和消费组相关
// 就是一个兜底策略,这个消费者组哪些消息没有消费掉,重新消费
log.error("sendMessageBack Exception, " + this.defaultMQPushConsumer.getConsumerGroup(), e);
Message newMsg = new Message(MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()), msg.getBody());
String originMsgId = MessageAccessor.getOriginMessageId(msg);
MessageAccessor.setOriginMessageId(newMsg, UtilAll.isBlank(originMsgId) ? msg.getMsgId() : originMsgId);
newMsg.setFlag(msg.getFlag());
MessageAccessor.setProperties(newMsg, msg.getProperties());
//IMP 真正的主题信息
MessageAccessor.putProperty(newMsg, MessageConst.PROPERTY_RETRY_TOPIC, msg.getTopic());
//重复消费次数+1
MessageAccessor.setReconsumeTime(newMsg, String.valueOf(msg.getReconsumeTimes() + 1));
MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(getMaxReconsumeTimes()));
MessageAccessor.clearProperty(newMsg, MessageConst.PROPERTY_TRANSACTION_PREPARED);
newMsg.setDelayTimeLevel(3 + msg.getReconsumeTimes());
this.mQClientFactory.getDefaultMQProducer().send(newMsg);
} finally {
msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace()));
}
}
-
上面的过程
- 获取 Broker 的地址,执行了【consumerSendMessageBack】,可以想想这个方法干了什么内容
- 如果发送失败了,此时会创建一个消息重新发送给 Broker,此时新消息的主题为重试主题:"%RETRY%" + ConsumeGroupName, 注意,这里的主题和原先的消息主题没任何关系而是和消费组相关,此时不会存在消息提,只需要 通过 msgId 就可以在 Broker 端获取到重新需要消费的消息
consumerSendMessageBack
public void consumerSendMessageBack(
final String addr,
final MessageExt msg,
final String consumerGroup,
final int delayLevel,
final long timeoutMillis,
final int maxConsumeRetryTimes
) throws RemotingException, MQBrokerException, InterruptedException {
ConsumerSendMsgBackRequestHeader requestHeader = new ConsumerSendMsgBackRequestHeader();
//按照偏移量重新获取数据 , CONSUMER_SEND_MSG_BACK
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONSUMER_SEND_MSG_BACK, requestHeader);
requestHeader.setGroup(consumerGroup);
requestHeader.setOriginTopic(msg.getTopic());
requestHeader.setOffset(msg.getCommitLogOffset());
requestHeader.setDelayLevel(delayLevel);
requestHeader.setOriginMsgId(msg.getMsgId());
requestHeader.setMaxReconsumeTimes(maxConsumeRetryTimes);
RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr),
request, timeoutMillis);
assert response != null;
switch (response.getCode()) {
case ResponseCode.SUCCESS: {
return;
}
default:
break;
}
throw new MQBrokerException(response.getCode(), response.getRemark(), addr);
}
- 会构建一个请求【CONSUMER_SEND_MSG_BACK】,此请求中加了 msgId,那么可以通过 msgId 获取到消息
对应消费失败的消息,会向 Broker 重新发送消息,那么当前队列的偏移量是不是都应该增加,这样不会阻塞消息的拉取和消费,所以,不管成功还是失败,都需要将便宜量增加
/*
* K2 3 从处理队列的msgTreeMap中将消费成功以及消费失败但是发回broker成功的这批消息移除,然后返回msgTreeMap中的最小的偏移量
* 消息进度的更新,此处就知道了 TreeMap 的作用,为了保存最小的消费进度,为了更新消费进度
*
* IMP 不管成功与否都不会阻止 offset 的变大,如果消费失败了,会将消息以重试主题下的消息放入,然后进行消费
* 根据消费结果,设置ackIndex的值。
* 如果是消费失败,根据消费模式(集群消费还是广播消费),广播模式,直接丢弃,集群模式发送 sendMessageBack,这里会创建新的消息(重试次数,延迟执行)。
* 更新消息消费进度,不管消费成功与否,上述这些消息消费成功,其实就是修改消费偏移量。(失败的,会进行重试,会创建新的消息)。
*/
long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());
//如果偏移量大于等于0并且处理队列没有被丢弃
if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
//尝试更新内存中的offsetTable中的最新偏移量信息,第三个参数是否仅单调增加offset为true
this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
}
具体 CONSUMER_SEND_MSG_BACK 的时候,broker 做了什么事儿呢,具体入口在
SendMessageProcessor
public CompletableFuture<RemotingCommand> asyncProcessRequest(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
final SendMessageContext mqtraceContext;
switch (request.getCode()) {
// 消息失败的情况
case RequestCode.CONSUMER_SEND_MSG_BACK:
return this.asyncConsumerSendMsgBack(ctx, request);
default:
//省略无关的内容
....
}
}
asyncConsumerSendMsgBack
- 处理消费失败的 ack 请求,重新消费消息。目的就是之前说的,重新消费,不能阻塞其他消息拉取,消费端其实已经做了处理,不管成功与否,偏移量都增加,可想而知,Broker 端肯定是作为新的消息进行消费了,具体看看这个方法的逻辑就明白了
-
又臭又长,直接上流程
-
根据消费组获取订阅信息
-
根据消费组获取新的主题,此主题和原始主题不一样,这也就可以想到的,这样就不会影响当前主题的消费了,消费进度就可以增加了,和 consumer 端保持一致.
-
通过偏移量获取到之前消费失败的消息信息
-
如果消费了太多次了,也不能阻塞重试队列的消费,不断的加到重试队列里面,一致失败,没有意义,所以太多次直接加入到死信队列中,方便后续通过人工干预进行处理,不得不说,各种处理都全面,也不会造成一致消费失败的消息
-
如果消息是,消费失败后的重新消费消息,那么会设置延迟级别信息 delayLevel = 3 + msgExt.getReconsumeTimes(); ,所以消费失败后的消息,不仅会重置其
- 新的主题为 RETRY_TOPIC + 消费组名称
- 设置延迟级别 DelayTimeLevel
-
-
通过新的主题,创建新的消息,然后加入到 commitLog 中。现在成功将消息发送到 commitlog 中,主题为 RETRY_TOPIC + 消费组名称,也就是消息重试的消息主题是基于消费组。而不是每一个主题都有一个重试主题。而是每一个消费组由一个重试主题。消息有几个重要点
- 新的主题为 RETRY_TOPIC + 消费组名称
- 设置了延迟级别 DelayTimeLevel
- 原始的真正的主题,记录在 properties 的 PROPERTY_RETRY_TOPIC 中
-
/**
* K1 处理消费失败的 ack 请求,重新消费消息
* @param ctx
* @param request
* @return
* @throws RemotingCommandException
*/
private CompletableFuture<RemotingCommand> asyncConsumerSendMsgBack(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final ConsumerSendMsgBackRequestHeader requestHeader =
(ConsumerSendMsgBackRequestHeader)request.decodeCommandCustomHeader(ConsumerSendMsgBackRequestHeader.class);
String namespace = NamespaceUtil.getNamespaceFromResource(requestHeader.getGroup());
if (this.hasConsumeMessageHook() && !UtilAll.isBlank(requestHeader.getOriginMsgId())) {
ConsumeMessageContext context = buildConsumeMessageContext(namespace, requestHeader, request);
this.executeConsumeMessageHookAfter(context);
}
//K2 获取消费组的订阅信息
SubscriptionGroupConfig subscriptionGroupConfig =
this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(requestHeader.getGroup());
if (null == subscriptionGroupConfig) {
response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST);
response.setRemark("subscription group not exist, " + requestHeader.getGroup() + " "
+ FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST));
return CompletableFuture.completedFuture(response);
}
if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission())) {
response.setCode(ResponseCode.NO_PERMISSION);
response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] sending message is forbidden");
return CompletableFuture.completedFuture(response);
}
if (subscriptionGroupConfig.getRetryQueueNums() <= 0) {
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return CompletableFuture.completedFuture(response);
}
//k2 根据重试主题创建或获取该主题的路由信息,此处的主题只和 消费者组有关系,所以是新的主题
String newTopic = MixAll.getRetryTopic(requestHeader.getGroup());
int queueIdInt = ThreadLocalRandom.current().nextInt(99999999) % subscriptionGroupConfig.getRetryQueueNums();
int topicSysFlag = 0;
if (requestHeader.isUnitMode()) {
topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
}
//K3 主题配置信息
TopicConfig topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
newTopic,
subscriptionGroupConfig.getRetryQueueNums(),
PermName.PERM_WRITE | PermName.PERM_READ, topicSysFlag);
if (null == topicConfig) {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("topic[" + newTopic + "] not exist");
return CompletableFuture.completedFuture(response);
}
if (!PermName.isWriteable(topicConfig.getPerm())) {
response.setCode(ResponseCode.NO_PERMISSION);
response.setRemark(String.format("the topic[%s] sending message is forbidden", newTopic));
return CompletableFuture.completedFuture(response);
}
//K2 通过偏移量 从 commitLog 中获取到需要重新消费的消息
MessageExt msgExt = this.brokerController.getMessageStore().lookMessageByOffset(requestHeader.getOffset());
if (null == msgExt) {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("look message by offset failed, " + requestHeader.getOffset());
return CompletableFuture.completedFuture(response);
}
// IMP 真正的主题
final String retryTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);
if (null == retryTopic) {
MessageAccessor.putProperty(msgExt, MessageConst.PROPERTY_RETRY_TOPIC, msgExt.getTopic());
}
msgExt.setWaitStoreMsgOK(false);
int delayLevel = requestHeader.getDelayLevel();
int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes();
if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal()) {
Integer times = requestHeader.getMaxReconsumeTimes();
if (times != null) {
maxReconsumeTimes = times;
}
}
//k2 超了最大重试次数,或者延迟级别 不合规,则加入死信队列中
if (msgExt.getReconsumeTimes() >= maxReconsumeTimes
|| delayLevel < 0) {
//IMP 死信队列,和消费组有关系
newTopic = MixAll.getDLQTopic(requestHeader.getGroup());
queueIdInt = ThreadLocalRandom.current().nextInt(99999999) % DLQ_NUMS_PER_GROUP;
topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic,
DLQ_NUMS_PER_GROUP,
PermName.PERM_WRITE | PermName.PERM_READ, 0);
if (null == topicConfig) {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("topic[" + newTopic + "] not exist");
return CompletableFuture.completedFuture(response);
}
msgExt.setDelayTimeLevel(0);
} else {
if (0 == delayLevel) {
delayLevel = 3 + msgExt.getReconsumeTimes();
}
msgExt.setDelayTimeLevel(delayLevel);
}
MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
msgInner.setTopic(newTopic);
msgInner.setBody(msgExt.getBody());
msgInner.setFlag(msgExt.getFlag());
MessageAccessor.setProperties(msgInner, msgExt.getProperties());
msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(null, msgExt.getTags()));
msgInner.setQueueId(queueIdInt);
msgInner.setSysFlag(msgExt.getSysFlag());
msgInner.setBornTimestamp(msgExt.getBornTimestamp());
msgInner.setBornHost(msgExt.getBornHost());
msgInner.setStoreHost(msgExt.getStoreHost());
msgInner.setReconsumeTimes(msgExt.getReconsumeTimes() + 1);
String originMsgId = MessageAccessor.getOriginMessageId(msgExt);
MessageAccessor.setOriginMessageId(msgInner, UtilAll.isBlank(originMsgId) ? msgExt.getMsgId() : originMsgId);
msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
//K2 重新将该消息写入到 commitLog 中
// IMP 成功将消息发送到 commitlog 中,主题为 RETRY_TOPIC + 消费组名称,,
// 也就是消息重试的消息主题是基于消费组。而不是每一个主题都有一个重试主题。
// 而是每一个消费组由一个重试主题。那这些主题的消息,又是如何在被消费者获取并进行消费的。
CompletableFuture<PutMessageResult> putMessageResult = this.brokerController.getMessageStore().asyncPutMessage(msgInner);
return putMessageResult.thenApply((r) -> {
if (r != null) {
switch (r.getPutMessageStatus()) {
//K2 保存成功
case PUT_OK:
String backTopic = msgExt.getTopic();
String correctTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);
if (correctTopic != null) {
backTopic = correctTopic;
}
if (TopicValidator.RMQ_SYS_SCHEDULE_TOPIC.equals(msgInner.getTopic())) {
this.brokerController.getBrokerStatsManager().incTopicPutNums(msgInner.getTopic());
this.brokerController.getBrokerStatsManager().incTopicPutSize(msgInner.getTopic(), r.getAppendMessageResult().getWroteBytes());
this.brokerController.getBrokerStatsManager().incQueuePutNums(msgInner.getTopic(), msgInner.getQueueId());
this.brokerController.getBrokerStatsManager().incQueuePutSize(msgInner.getTopic(), msgInner.getQueueId(), r.getAppendMessageResult().getWroteBytes());
}
this.brokerController.getBrokerStatsManager().incSendBackNums(requestHeader.getGroup(), backTopic);
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
default:
break;
}
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark(r.getPutMessageStatus().name());
return response;
}
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("putMessageResult is null");
return response;
});
}
目前,重试机制的前半部分已经讲解完成,再次复习一下:
- 根据消费结果,设置ackIndex的值。
- 如果是消费失败,根据消费模式(集群消费还是广播消费),广播模式,直接丢弃,集群模式发送 sendMessageBack,这里会创建新的消息(重试次数,延迟执行)。
- 更新消息消费进度,不管消费成功与否,上述这些消息消费成功,其实就是修改消费偏移量。(失败的,会进行重试,会创建新的消息)。
消息现在是存储到 commitlog 文件里了,那怎么消费呢。
继续看 asyncPutMessage方法,最终定位到了 CommitLog#asyncPutMessage 方法中
public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
// Set the storage time
//设置存储时间
msg.setStoreTimestamp(System.currentTimeMillis());
// Set the message body BODY CRC (consider the most appropriate setting
// on the client)
//设置消息正文CRC
msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
// Back to Results
AppendMessageResult result = null;
StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
String topic = msg.getTopic();
// int queueId msg.getQueueId();
/*
* K2 1 处理延迟消息的逻辑
*
* 替换topic和queueId,保存真实topic和queueId
*/
//根据sysFlag获取事务状态,普通消息的sysFlag为0
final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
//如果不是事务消息,或者commit提交事务小i
if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
|| tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
//IMP 延迟消息
// Delay Delivery
//获取延迟级别,判断是否是延迟消息
if (msg.getDelayTimeLevel() > 0) {
//如果延迟级别大于最大级别,则设置为最大级别
if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
}
//获取延迟队列的topic,固定为 SCHEDULE_TOPIC_XXXX
topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
//根据延迟等级获取对应的延迟队列id, id = level - 1
int queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
// Backup real topic, queueId
//使用扩展属性REAL_TOPIC 记录真实topic,此时的主题,进入到钱的主题,
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
//使用扩展属性REAL_QID 记录真实queueId
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
//更改topic和queueId为延迟队列的topic和queueId
msg.setTopic(topic);
msg.setQueueId(queueId);
}
}
//...省略无关的内容...
}
最后消息变成了,延迟消息,此时消息存了 3 种主题信息
- REAL_TOPIC 为 RETRY_TOPIC + 消费组名称
- Topic 为 SCHEDULE_TOPIC_XXXX
- PROPERTY_RETRY_TOPIC 为 消息失败之前的真正主题
得了,最后按照延迟消费的消息进行处理了,延迟消息消费的逻辑,后面学习,对于上面的消息重试,进行一波总结
1、如果返回结果是 CONSUME_SUCCESS,此时 ackIndex = msg.size() - 1, 再看发送 sendMessageBack 循环的条件,for (int i = ackIndex + 1; i < msg.size() ;;) 从这里可以看出如果消息成功,则无需发送 sendMsgBack 给 broker;如果返回结果是RECONSUME_LATER, 此时 ackIndex = -1 ,则这批所有的消息都会发送消息给 Broker,也就是这一批消息都得重新消费。
如果发送ack消息失败,则会延迟5s后重新在消费端重新消费。
首先消费者向 Broker 发送 ACK 消息,如果发生成功,重试机制由 broker 处理,如果发送 ack 消息失败,则将该任务直接在消费者这边,再次将本次消费任务,默认演出5S后在消费者重新消费。
根据消费结果,设置ackIndex的值。 如果是消费失败,根据消费模式(集群消费还是广播消费),广播模式,直接丢弃,集群模式发送sendMessageBack。 更新消息消费进度,不管消费成功与否,上述这些消息消费成功,其实就是修改消费偏移量。(失败的,会进行重试,会创建新的消息)。 2、需要延迟执行的消息,在存入 commitlog 之前,会备份原先的主题(retry+消费组名称)、与消费队列ID,然后将主题修改为SCHEDULE_TOPIC_XXXX,会被延迟任务 ScheduleMessageService 延迟拉取。
3、ScheduleMessageService 在执行过程中,会再次存入 commitlog 文件中放入之前,会清空延迟等级,并恢复主题与队列,这样,就能被消费者所消费,因为消费者在启动时就订阅了该消费组的重试主题。
总结
-
上面通过源码看到了,Consumer 启动的时候,启动了哪些数据
-
Consumer 中各种生产-消费模式进行获取消息,消费消息,涉及组件
- Consumer->PullMessageService(Thread,自旋获取PullRequest)->offStore->CosumerMessageService( 内部含有ThreadPool 进行异步执行)
遗留问题
对于消费端
- PullRequest是哪里产生的呢???
- 消费端消息负载均衡机制与重新分布
- MsgTreeMap 干嘛用的,为何要保存依次数据
- 消息消费进度保持机制