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

if (pre != null) {

log.info(“doRebalance, {}, mq already exists, {}”, consumerGroup, mq);

} else {

log.info(“doRebalance, {}, add a new mq, {}”, consumerGroup, mq);

PullRequest pullRequest = new PullRequest();

pullRequest.setConsumerGroup(consumerGroup);

pullRequest.setNextOffset(nextOffset);

pullRequest.setMessageQueue(mq);

pullRequest.setProcessQueue(pq);

pullRequestList.add(pullRequest);

changed = true;

}

} else {

log.warn(“doRebalance, {}, add new mq failed, {}”, consumerGroup, mq);

}

}

}

上述的重点是代码@1,顺序消息时,添加该消息队列的拉取任务之前,首先要先尝试锁定消费者(消费组+CID),不同消费组的消费者可以同时锁定同一个消息消费队列,集群模式下同一个消费组内只能被一个消费者锁定,如果锁定成功,则添加到拉取任务中,如果锁定未成功,说明虽然发送了消息队列重新负载,但该消息队列还未被释放,本次负载周期不会进行消息拉取。锁定消息队列的具体实现,下文详细讲解。

2、消息拉取


DefaultMQPushConsumerImpl#pullMessage(final PullRequest pullRequest)

if (this.consumeOrderly) {

if (processQueue.isLocked()) {

if (!pullRequest.isLockedFirst()) {

final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());

boolean brokerBusy = offset < pullRequest.getNextOffset();

log.info(“the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}”,

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再提交消费任务。

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

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

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

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

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

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

img

总结

我个人认为,如果你想靠着背面试题来获得心仪的offer,用癞蛤蟆想吃天鹅肉形容完全不过分。想必大家能感受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。

这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。

大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。

这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。

大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:

[外链图片转存中…(img-2zGY3BCI-1712486952819)]

[外链图片转存中…(img-7WZqdkzB-1712486952819)]

[外链图片转存中…(img-62Jv4VNr-1712486952820)]

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 11
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值