其实最好的学习方式就是互相交流,最近也有跟网友讨论了一些关于 RocketMQ 消息拉取与重平衡的问题,我姑且在这里写下我的一些总结。

## 关于 push 模式下的消息循环拉取问题

之前发表了一篇关于重平衡的文章:「[Kafka 重平衡机制](https://mp.weixin.qq.com/s/4DFup_NziFJ1xdc4bZnVcg)」,里面有说到 RocketMQ 重平衡机制是每隔 20s 从任意一个 Broker 节点获取消费组的消费 ID 以及订阅信息,再根据这些订阅信息进行分配,然后将分配到的信息封装成 pullRequest 对象 pull 到 pullRequestQueue 队列中,拉取线程唤醒后执行拉取任务,流程图如下:

![](https://raw.githubusercontent.com/objcoding/objcoding.github.io/master/images/rocketmq_16.png)

但是其中有一些是没有详细说的,比如每次拉消息都要等 20s 吗?真的有个网友问了我如下问题:

![](https://img2018.cnblogs.com/blog/1860306/201911/1860306-20191106215340830-1811833513.png)

很显然他的项目是用了 push 模式进行消息拉取,要回答这个问题,就要从 RockeMQ 的消息拉取说起:

RocketMQ 的 push 模式的实现是基于 pull 模式,只不过在 pull 模式上套了一层,所以RocketMQ push 模式并不是真正意义上的 ”推模式“,因此,在 push 模式下,消费者拉取完消息后,立马就有开始下一个拉取任务,并不会真的等 20s 重平衡后才拉取,至于 push 模式是怎么实现的,那就从源码去找答案。

之前有写过一篇文章:「[RocketMQ为什么要保证订阅关系的一致性?](https://mp.weixin.qq.com/s/8fB-Z5oFPbllp13EcqC9dw)」,里面有说过 消息拉取是从 PullRequestQueue 阻塞队列中取出 PullRequest 拉取任务进行消息拉取的,但 PullRequest 是怎么放进 PullRequestQueue 阻塞队列中的呢?

RocketMQ 一共提供了以下方法:

org.apache.rocketmq.client.impl.consumer.PullMessageService#executePullRequestImmediately:

```java
public void executePullRequestImmediately(final PullRequest pullRequest) {
try {
this.pullRequestQueue.put(pullRequest);
} catch (InterruptedException e) {
log.error("executePullRequestImmediately pullRequestQueue.put", e);
}
}
```

从调用链发现,除了重平衡会调用该方法之外,在 push 模式下,PullCallback 回调对象中的 onSuccess 方法在消息消费时,也调用了该方法:

org.apache.rocketmq.client.consumer.PullCallback#onSuccess:

case FOUND:

```java
// 如果本次拉取消息为空,则继续将pullRequest放入阻塞队列中
if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
} else {
// 将消息放入消费者消费线程去执行
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(//
pullResult.getMsgFoundList(), //
processQueue, //
pullRequest.getMessageQueue(), //
dispathToConsume);
// 将pullRequest放入阻塞队列中
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}

```

当从 broker 拉取到消息后,如果消息被过滤掉,则继续将pullRequest放入阻塞队列中继续循环执行消息拉取任务,否则将消息放入消费者消费线程去执行,在pullRequest放入阻塞队列中。

case NO_NEW_MESSAGE:

case NO_MATCHED_MSG:

```java
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
```

如果从 broker 端没有可拉取的新消息或者没有匹配到消息,则将pullRequest放入阻塞队列中继续循环执行消息拉取任务。

从以上消息消费逻辑可以看出,当消息处理完后,立即将 pullRequest 重新放入阻塞队列中,因此这就很好解释为什么 push 模式可以持续拉取消息了:

在 push 模式下消息消费完后,还会调用该方法重新将 PullRequest 对象放进 PullRequestQueue 阻塞队列中,不断地从 broker 中拉取消息,实现 push 效果。

## 重平衡后队列被其它消费者分配后如何处理?

继续再想一个问题,如果重平衡后,发现某个队列被新的消费者分配了,怎么办,总不能继续从该队列中拉取消息吧?

RocketMQ 重平衡后会检查 pullRequest 是否还在新分配的列表中,如果不在,则丢弃,调用 isDrop() 可查出该pullRequest是否已丢弃:

org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage:

```java
final ProcessQueue processQueue = pullRequest.getProcessQueue();
if (processQueue.isDropped()) {
log.info("the pull request[{}] is dropped.", pullRequest.toString());
return;
}
```

在消息拉取之前,首先判断该队列是否被丢弃,如果已丢弃,则直接放弃本次拉取任务。

那什么时候队列被丢弃呢?

org.apache.rocketmq.client.impl.consumer.RebalanceImpl#updateProcessQueueTableInRebalance:

```java
Iterator> it = this.processQueueTable.entrySet().iterator();
while (it.hasNext()) {
Entry next = it.next();
MessageQueue mq = next.getKey();
ProcessQueue pq = next.getValue();

if (mq.getTopic().equals(topic)) {
// 判断当前缓存 MessageQueue 是否包含在最新的 mqSet 中,如果不存在则将队列丢弃
if (!mqSet.contains(mq)) {
pq.setDropped(true);
if (this.removeUnnecessaryMessageQueue(mq, pq)) {
it.remove();
changed = true;
log.info("doRebalance, {}, remove unnecessary mq, {}", consumerGroup, mq);
}
} else if (pq.isPullExpired()) {
// 如果队列拉取过期则丢弃
switch (this.consumeType()) {
case CONSUME_ACTIVELY:
break;
case CONSUME_PASSIVELY:
pq.setDropped(true);
if (this.removeUnnecessaryMessageQueue(mq, pq)) {
it.remove();
changed = true;
log.error("[BUG]doRebalance, {}, remove unnecessary mq, {}, because pull is pause, so try to fixed it",
consumerGroup, mq);
}
break;
default:
break;
}
}
}
}
```
updateProcessQueueTableInRebalance 方法在重平衡时执行,用于更新 processQueueTable,它是当前消费者的队列缓存列表,以上方法逻辑判断当前缓存 MessageQueue 是否包含在最新的 mqSet 中,如果不包含其中,则说明经过这次重平衡后,该队列被分配给其它消费者了,或者拉取时间间隔太大过期了,则调用 setDropped(true) 方法将队列置为丢弃状态。

可能你会问,processQueueTable 跟 pullRequest 里面 processQueue 有什么关联,往下看:

org.apache.rocketmq.client.impl.consumer.RebalanceImpl#updateProcessQueueTableInRebalance:

```java
// 新建 ProcessQueue
ProcessQueue pq = new ProcessQueue();
long nextOffset = this.computePullFromWhere(mq);
if (nextOffset >= 0) {
// 将ProcessQueue放入processQueueTable中
ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
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);
// 将ProcessQueue放入pullRequest拉取任务对象中
pullRequest.setProcessQueue(pq);
pullRequestList.add(pullRequest);
changed = true;
}
}
```

可以看出,重平衡时会创建 ProcessQueue 对象,将其放入 processQueueTable 缓存队列表中,再将其放入 pullRequest 拉取任务对象中,也就是 processQueueTable 中的 ProcessQueue 与 pullRequest 的中 ProcessQueue 是同一个对象。

## 重平衡后会导致消息重复消费吗?

之前在群里有个网友提了这个问题:

![](https://img2018.cnblogs.com/blog/1860306/201911/1860306-20191106215344621-1407846309.png)

我当时回答他 RocketMQ 正常也是没有重复消费,但后来发现其实 RocketMQ 在某些情况下,也是会出现消息重复消费的现象。

前面讲到,RocketMQ 消息消费时,会将消息放进消费线程中去执行,代码如下:

org.apache.rocketmq.client.consumer.PullCallback#onSuccess:

```java
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(//
pullResult.getMsgFoundList(), //
processQueue, //
pullRequest.getMessageQueue(), //
dispathToConsume);
```

ConsumeMessageService 类实现消息消费的逻辑,它有两个实现类:

```java
// 并发消息消费逻辑实现类
org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService;
// 顺序消息消费逻辑实现类
org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService;
```

先看并发消息消费相关处理逻辑:

ConsumeMessageConcurrentlyService:

org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService.ConsumeRequest#run:

```java
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;
}

// 消息消费逻辑
// ...

// 如果队列被设置为丢弃状态,则不提交消息消费进度
if (!processQueue.isDropped()) {
ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this);
} else {
log.warn("processQueue is dropped without process consume result. messageQueue={}, msgs={}", messageQueue, msgs);
}
```

ConsumeRequest 是一个继承了 Runnable 的类,它是消息消费核心逻辑的实现类,submitConsumeRequest 方法将 ConsumeRequest 放入 消费线程池中执行消息消费,从它的 run 方法中可看出,如果在执行消息消费逻辑中有节点加入,重平衡后该队列被分配给其它节点进行消费了,此时的队列被丢弃,则不提交消息消费进度,因为之前已经消费了,此时就会造成消息重复消费的情况。

再来看看顺序消费相关处理逻辑:

ConsumeMessageOrderlyService:

org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService.ConsumeRequest#run:

```java
public void run() {
// 判断队列是否被丢弃
if (this.processQueue.isDropped()) {
log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
return;
}

final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
synchronized (objLock) {
// 如果不是广播模式,且队列已加锁且锁没有过期
if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
|| (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
final long beginTime = System.currentTimeMillis();
for (boolean continueConsume = true; continueConsume; ) {
// 再次判断队列是否被丢弃
if (this.processQueue.isDropped()) {
log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
break;
}

// 消息消费处理逻辑
// ...

continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);
} else {
continueConsume = false;
}
}
} 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);
}
}
}
```

RocketMQ 顺序消息消费会将队列锁定,当队列获取锁之后才能进行消费,所以,即使消息在消费过程中有节点加入,重平衡后该队列被分配给其它节点进行消费了,此时的队列被丢弃,依然不会造成重复消费。
> 更多精彩文章请关注作者维护的公众号「后端进阶」,这是一个专注后端相关技术的公众号。
> 关注公众号并回复「后端」免费领取后端相关电子书籍。
> 欢迎分享,转载请保留出处。

关于RocketMQ消息消费与重平衡的一些问题探讨的更多相关文章

  1. RocketMQ 消息消费

    消息消费 难点:如何保证消息只消费一次? 消费模式: 1.单一消费模式:一条消息,仅被一个消费者进行消费. 如何进行负载?负载算法有 a.平均分配.b.平均轮询分配.c.一致性hash(不推荐).d. ...

  2. RockerMQ消息消费、重试

    消息中间件—RocketMQ消息消费(一) 消息中间件—RocketMQ消息消费(二)(push模式实现) 消息中间件—RocketMQ消息消费(三)(消息消费重试) MQ中Pull和Push的两种消 ...

  3. RocketMQ(消息重发、重复消费、事务、消息模式)

    分布式开放消息系统(RocketMQ)的原理与实践 RocketMQ基础:https://github.com/apache/rocketmq/tree/rocketmq-all-4.5.1/docs ...

  4. 程序重启RocketMQ消息重复消费

    最近在调试RocketMQ消息发送与消费的Demo时,发现一个问题:只要重启程序,RocketMQ消息就会重复消费. 那么这是什么原因导致的,又该如何解决呢? 经过一番排查,发现程序使用的Rocket ...

  5. rocketMq消息的发送和消息消费

    rocketMq消息的发送和消息消费 一.消息推送 public void pushMessage() { String message = "推送消息内容!"; try { De ...

  6. 一张图进阶 RocketMQ - 消息发送

    前 言 三此君看了好几本书,看了很多遍源码整理的 一张图进阶 RocketMQ 图片链接,关于 RocketMQ 你只需要记住这张图!觉得不错的话,记得点赞关注哦. [重要]视频在 B 站同步更新,欢 ...

  7. RocketMQ(7)---RocketMQ顺序消费

    RocketMQ顺序消费 如果要保证顺序消费,那么他的核心点就是:生产者有序存储.消费者有序消费. 一.概念 1.什么是无序消息 无序消息 无序消息也指普通的消息,Producer 只管发送消息,Co ...

  8. kafka Poll轮询机制与消费者组的重平衡分区策略剖析

    注意本文采用最新版本进行Kafka的内核原理剖析,新版本每一个Consumer通过独立的线程,来管理多个Socket连接,即同时与多个broker通信实现消息的并行读取.这就是新版的技术革新.类似于L ...

  9. 详细解析kafka之 kafka消费者组与重平衡机制

    消费组组(Consumer group)可以说是kafka很有亮点的一个设计.传统的消息引擎处理模型主要有两种,队列模型,和发布-订阅模型. 队列模型:早期消息处理引擎就是按照队列模型设计的,所谓队列 ...

随机推荐

  1. 玩转 SpringBoot 2 之整合 JWT 下篇

    前言 在<玩转 SpringBoot 2 之整合 JWT 上篇> 中介绍了关于 JWT 相关概念和JWT 基本使用的操作方式.本文为 SpringBoot 整合 JWT 的下篇,通过解决 ...

  2. Chrome浏览器启动报错:应用程序无法启动,因为应用程序的并行配置不正确。

    因为国庆节了,难得关一次机(可能搞IT的习惯吧),结果给祖国庆祝完70寿辰之后归来,启动电脑,就打不开Chrome浏览器了,报错如下: 应用程序无法启动,因为应用程序的并行配置不正确.有关详细信息,请 ...

  3. Python 正则re匹配中文、英式数字

    #coding:utf-8 import re s = u''' 或多或少的好好读书电锯惊魂20202 和水电费后是否会时候1212没收到风10.12海大富的是粉红色的和办法的1244525.000 ...

  4. [动态规划]高数Umaru系列(9)——哈士奇(背包问题)

    高数Umaru系列(9)——哈士奇 http://acm.sdut.edu.cn/onlinejudge2/index.php/Home/Index/problemdetail/pid/3358.ht ...

  5. Proving Equivalences UVA - 12167

    题文:https://vjudge.net/problem/UVA-12167 题解: 很明显,先要缩点.然后画一下图就会发现是入度为0的点和出度为0的点取max. 代码: #include < ...

  6. GUI tkinter (Canvas)绘图篇

    from tkinter import * root = Tk()root.title("中国象棋棋盘手绘") can = Canvas(root,width = 400, hei ...

  7. Linux 命令之 chmod

    命令格式 chmod有以下三种格式 chmod [-cfvR] MODE[,MODE]... FILE... chmod [-cfvR] OCTAL-MODE FILE... chmod [-cfvR ...

  8. What is neural network?

    It is a powerful learning algoithm inspired by how the brain work. Example 1 - single neural network ...

  9. Unreal Engine 4 系列教程 Part 3:材质教程

    .katex { display: block; text-align: center; white-space: nowrap; } .katex-display > .katex > ...

  10. mysql 延时注入新思路

    转自先知社区https://xz.aliyun.com/t/2288 在4月的pwnhub比赛中,我们遇到了一个比较神奇的问题,如果在注入中遇到需要延时注入的情况,但服务端过滤了我们一般使用的slee ...