源码分析RocketMQ顺序消息消费实现原理

pullRequest, offset, brokerBusy);

if (brokerBusy) {

log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset:

{}", pullRequest, offset);

}

pullRequest.setLockedFirst(true);

pullRequest.setNextOffset(offset);

}

} else {

this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);

log.info(“pull message later because not locked in broker, {}”, pullRequest);

return;

}

}

根据 PullRequest 拉取消息。如果处理队列未被锁定,则延迟拉取消息,也就说消息消费需要在ProceeQueue 队列被自己锁定的情况下才会拉取消息,否则将 PullRequest 延迟3s再拉取。并且PullRequest 的初始拉取点在拉取时只在第一次拉取时设置。

3、消息顺序消息消费


实现类:org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService

3.1核心属性与构造函数

这里写图片描述

  • private final static long MAX_TIME_CONSUME_CONTINUOUSLY =

Long.parseLong(System.getProperty(“rocketmq.client.maxTimeConsumeContinuously”, “60000”));

MAX_TIME_CONSUME_CONTINUOUSLY :消费任务一次运行的最大时间。可以通过-Drocketmq.client.maxTimeConsumeContinuously来设置,默认为60s。

  • DefaultMQPushConsumerImpl defaultMQPushConsumerImpl:消息消费者实现类。

  • DefaultMQPushConsumer defaultMQPushConsumer:消息消费者。

  • MessageListenerOrderly messageListener:顺序消息消费监听器。

  • BlockingQueue< Runnable> consumeRequestQueue:消息消费任务。

  • ThreadPoolExecutor consumeExecutor:消息消费线程池。

  • String consumerGroup:消息消费组。

  • MessageQueueLock messageQueueLock:消息消费队列锁,其内如实现为:

这里写图片描述

构造方法如下:

public ConsumeMessageOrderlyService(DefaultMQPushConsumerImpl defaultMQPushConsumerImpl,

MessageListenerOrderly messageListener) {

this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl;

this.messageListener = messageListener;

this.defaultMQPushConsumer = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer();

this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup();

this.consumeRequestQueue = new LinkedBlockingQueue(); // @1

this.consumeExecutor = new ThreadPoolExecutor(

this.defaultMQPushConsumer.get(),

this.defaultMQPushConsumer.getConsumeThreadMax(),

1000 * 60,

TimeUnit.MILLISECONDS,

this.consumeRequestQueue,ConsumeThreadMin

new ThreadFactoryImpl(“ConsumeMessageThread_”)); // @2

this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl(“ConsumeMessageScheduledThread_”));

//@3

}

代码@1:创建任务拉取队列,注意,这里使用的是无界队列。

代码@2:创建消费者消费线程池,注意由于消息任务队列 consumeRequestQueue 使用的是无界队列,故线程池中最大线程数量取自 consumeThreadMin。

代码@3:创建调度线程,该线程主要调度定时任务,延迟延迟消费等。

3.2 start方法

public void start() {

if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) {

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

@Override

public void run() {

ConsumeMessageOrderlyService.this.lockMQPeriodically();

}

}, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS);

}

}

如果消息消费模式为集群模式,启动定时任务,默认每隔20s执行一次锁定分配给自己的消息消费队列。通过 -Drocketmq.client.rebalance.lockInterval=20000 设置间隔,该值建议与一次消息负载频率设置相同。该方法最终将调用RebalanceImpl#lockAll方法.

public void lockAll() {

HashMap<String, Set> brokerMqs = this.buildProcessQueueTableByBrokerName(); // @1

Iterator<Entry<String, Set>> it = brokerMqs.entrySet().iterator();

while (it.hasNext()) {

Entry<String, Set> entry = it.next();

final String brokerName = entry.getKey();

final Set mqs = entry.getValue();

if (mqs.isEmpty())

continue;

FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, true); // @2

if (findBrokerResult != null) {

LockBatchRequestBody requestBody = new LockBatchRequestBody();

requestBody.setConsumerGroup(this.consumerGroup);

requestBody.setClientId(this.mQClientFactory.getClientId());

requestBody.setMqSet(mqs);

try {

Set lockOKMQSet =

this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000); // @3

for (MessageQueue mq : lockOKMQSet) { // @4

ProcessQueue processQueue = this.processQueueTable.get(mq);

if (processQueue != null) {

if (!processQueue.isLocked()) {

log.info(“the message queue locked OK, Group: {} {}”, this.consumerGroup, mq);

}

processQueue.setLocked(true);

processQueue.setLastLockTimestamp(System.currentTimeMillis());

}

}

for (MessageQueue mq : mqs) { // @5

if (!lockOKMQSet.contains(mq)) {

ProcessQueue processQueue = this.processQueueTable.get(mq);

if (processQueue != null) {

processQueue.setLocked(false);

log.warn(“the message queue locked Failed, Group: {} {}”, this.consumerGroup, mq);

}

}

}

} catch (Exception e) {

log.error("lockBatchMQ exception, " + mqs, e);

}

}

}

}

代码@1:根据当前负载的消息队列,按照 Broker分类存储在Map。负载的消息队列在RebalanceService时根据当前消费者数量与消息消费队列按照负载算法进行分配,然后尝试对该消息队列加锁,如果申请锁成功,则加入到待拉取任务中。

代码@2:根据Broker获取主节点的地址。

代码@3:向Broker发送锁定消息队列请求,该方法会返回本次成功锁定的消息消费队列,关于Broker端消息队列锁定实现见下文详细分析。

代码@4:遍历本次成功锁定的队列来更新对应的ProcessQueue的locked状态,如果locked为false,则设置成true,并更新锁定时间。

代码@5:遍历mqs,如果消息队列未成功锁定,需要将ProceeQueue的locked状态为false,在该处理队列未被其他消费者锁定之前,该消息队列将暂停拉取消息。

3.3 submitConsumeRequest

提交消息消费。

public void submitConsumeRequest(

final List msgs,

final ProcessQueue processQueue,

final MessageQueue messageQueue,

final boolean dispathToConsume) {

if (dispathToConsume) {

ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);

this.consumeExecutor.submit(consumeRequest);

}

}

顺序消息消费,不会处理传入的消息,而是从消息队列中顺序去获取。接下来重点探讨ConsumeMessageOrderlyService#ConsumeRequest。

3.4 ConsumeMessageOrderlyService#ConsumeRequest

这里写图片描述

  • ProcessQueue processQueue:消息处理队列。

  • MessageQueue messageQueue:消息队列。

这里并没有要处理的消息,而是等下需要从 ProcessQueue 中获取消息。

顺序消息消费流程通过 ConsumeMessageOrderlyService#ConsumeRequest#run 方法来实现。

if (this.processQueue.isDropped()) {

log.warn(“run, the message queue not be able to consume, because it’s dropped. {}”, this.messageQueue);

return;

}

Step1:如果消息队列状态为 dropped 为true,则停止本次消息消费。

final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);

synchronized (objLock) { …

Step2:获取 MessageQueue 对应的锁,在消费某一个消息消费队列时先加锁,意味着一个消费者内消费线程池中的线程并发度是消息消费队列级别,同一个消费队列在同一时刻只会被一个线程消费,其他线程排队消费。

if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())

|| (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {

//消息消费逻辑

} else {

if (this.processQueue.isDropped()) {

log.warn(“the message queue not be able to consume, because it’s dropped. {}”, this.messageQueue);

return;

}

ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100);

}

Step3:如果是广播模式的话,直接进入消费,无需锁定处理对列,因为相互直接无竞争,如果是集群模式,能消息消息 的前提条件就是必须proceessQueue被锁定并且锁未超时。会不会出现这样一种情况:发生消息队列重新负载时,原先由自己处理的消息队列被另外一个消费者分配,此时如果还未来的及将ProceeQueue解除锁定,就被另外一个消费者添加进去,此时会存储多个消息消费者同时消费个消息队列,答案是不会的,因为当一个新的消费队列分配给消费者时,在添加其拉取任务之前必须先向Broker发送对该消息队列加锁请求,只有加锁成功后,才能添加拉取消息,否则等到下一次负载后,该消费队列被原先占有的解锁后,才能开始新的拉取任务。集群模式下,如果未锁定处理队列,则延迟该队列的消息消费。

final long beginTime = System.currentTimeMillis();

for (boolean continueConsume = true; continueConsume; ) {

… 省略相关代码

long interval = System.currentTimeMillis() - beginTime;

if (interval > MAX_TIME_CONSUME_CONTINUOUSLY) {

ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, messageQueue, 10);

break;

}

}

Step4:顺序消息消费处理逻辑,每一个ConsumeRequest消费任务不是以消费消息条数来计算,而是根据消费时间,默认当消费时长大于MAX_TIME_CONSUME_CONTINUOUSLY,默认60s后,本次消费任务结束,由消费组内其他线程继续消费。

if (this.processQueue.isDropped()) {

log.warn(“the message queue not be able to consume, because it’s dropped. {}”, this.messageQueue);

break;

}

if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())

&& ( !this.processQueue.isLocked() || this.processQueue.isLockExpired() )) {

log.warn(“the message queue not locked, so consume later, {}”, this.messageQueue);

ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);

break;

}

Step5:如果消息消费队列被丢弃,则直接结束本次消息消费。如果是集群模式,消息处理队列未加锁或锁过期,则尝试对消息队列加锁,加锁成功则再提交消费任务,否则延迟3s再提交消费任务。

final int consumeBatchSize =

ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();

List msgs = this.processQueue.takeMessags(consumeBatchSize);

Step6:每次从处理队列中按顺序取出consumeBatchSize消息,如果未取到消息,则设置continueConsume为false,本次消费任务结束。

顺序消息消费时,从ProceessQueue中取出的消息,会临时存储在ProceeQueue的consumingMsgOrderlyTreeMap属性中。

this.lockTreeMap.writeLock().lockInterruptibly();

this.lastConsumeTimestamp = now;

try {

if (!this.msgTreeMap.isEmpty()) {

for (int i = 0; i < batchSize; i++) {

Map.Entry<Long, MessageExt> entry = this.msgTreeMap.pollFirstEntry();

if (entry != null) {

result.add(entry.getValue());

consumingMsgOrderlyTreeMap.put(entry.getKey(), entry.getValue());

} else {

break;

}

}

}

if (result.isEmpty()) {

consuming = false;

}

} finally {

this.lockTreeMap.writeLock().unlock();

}

Step7:执行消息消费钩子函数(消息消费之前before方法),通过

DefaultMQPushConsumerImpl#registerConsumeMessageHook(ConsumeMessageHook consumeMessagehook)注册消息消费钩子函数,可以注册多个。

long beginTimestamp = System.currentTimeMillis();

ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;

boolean hasException = false;

try {

this.processQueue.getLockConsume().lock();

if (this.processQueue.isDropped()) {

log.warn(“consumeMessage, the message queue not be able to consume, because it’s dropped. {}”,

this.messageQueue);

break;

}

status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);

} catch (Throwable e) {

log.warn(“consumeMessage exception: {} Group: {} Msgs: {} MQ: {}”,RemotingHelper.exceptionSimpleDesc(e),

ConsumeMessageOrderlyService.this.consumerGroup,msgs,messageQueue);

hasException = true;

} finally {

this.processQueue.getLockConsume().unlock();

}

Step8:调用消息消费监听器供业务程序消息消费,并返回消息消费结果。 返回结果:ConsumeOrderlyStatus.SUCCESS成功或SUSPEND_CURRENT_QUEUE_A_MOMENT挂起,延迟的意思。

Step9:执行消息消费钩子函数,就算messageListener.consumeMessage抛出异常,钩子函数同样会执行。

Step10:如果消费结果为ConsumeOrderlyStatus.SUCCESS,执行ProceeQueue的commit方法,并返回待更新的消息消费进度。

ProceeQueue#commit

public long commit() {

try {

this.lockTreeMap.writeLock().lockInterruptibly();

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

分享

首先分享一份学习大纲,内容较多,涵盖了互联网行业所有的流行以及核心技术,以截图形式分享:

(亿级流量性能调优实战+一线大厂分布式实战+架构师筑基必备技能+设计思想开源框架解读+性能直线提升架构技术+高效存储让项目性能起飞+分布式扩展到微服务架构…实在是太多了)

其次分享一些技术知识,以截图形式分享一部分:

Tomcat架构解析:

算法训练+高分宝典:

Spring Cloud+Docker微服务实战:

最后分享一波面试资料:

切莫死记硬背,小心面试官直接让你出门右拐

1000道互联网Java面试题:

Java高级架构面试知识整理:

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
/>

分享

首先分享一份学习大纲,内容较多,涵盖了互联网行业所有的流行以及核心技术,以截图形式分享:

(亿级流量性能调优实战+一线大厂分布式实战+架构师筑基必备技能+设计思想开源框架解读+性能直线提升架构技术+高效存储让项目性能起飞+分布式扩展到微服务架构…实在是太多了)

其次分享一些技术知识,以截图形式分享一部分:

Tomcat架构解析:

[外链图片转存中…(img-xuO3SUgu-1713751460075)]

算法训练+高分宝典:

[外链图片转存中…(img-03b1qeCM-1713751460075)]

Spring Cloud+Docker微服务实战:

[外链图片转存中…(img-XdCpKQZN-1713751460076)]

最后分享一波面试资料:

切莫死记硬背,小心面试官直接让你出门右拐

1000道互联网Java面试题:

[外链图片转存中…(img-TwKMjltM-1713751460076)]

Java高级架构面试知识整理:

[外链图片转存中…(img-PN9fceyI-1713751460076)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 28
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值