RocketMQ一个实例能消费另一个就不能消费?

本文深入探讨了RocketMQ的集群模式和广播模式。集群模式中消息仅被消费者组的一个成员消费,广播模式下则每个消费者都会消费消息。在广播模式下,消费者不考虑重试,偏移量保存在本地,且不支持顺序消息。同时,文章指出广播模式的消费者订阅了Topic下的所有MessageQueue,不会进行重平衡。了解这些关键点有助于理解RocketMQ的广播消息实现机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

RocketMQ 有两种消费模式,集群模式和广播模式。
集群模式是指 RocketMQ 中的一条消息只能被同一个消费者组中的一个消费者消费。如下图,Producer 向 TopicTest 这个 Topic 并发写入 3 条新消息,分别被分配到了 MessageQueue1~MessageQueue3 这 3 个队列,然后 Group 中的三个 Consumer 分别消费了一条消息:
 

广播模式是 RocketMQ 中的消息会被消费组中的每个消费者都消费一次,如下图:
 

使用 RocketMQ 的广播模式时,需要在消费端进行定义,下面是一段官方示例:
public static void main(String[] args) throws InterruptedException, MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_1");

consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

consumer.setMessageModel(MessageModel.BROADCASTING);

consumer.subscribe("TopicTest", "TagA || TagC || TagD");

consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});

consumer.start();
System.out.printf("Broadcast Consumer Started.%n");
}

从代码中可以看到,在定义 Consumer 时,通过 messageModel 这个属性指定消费模式,这里指定为 BROADCASTING,也就启动了广播模式的消费者。
1 消费者启动
以 RocketMQ 推模式为例,看一下消费者调用关系类图:
 

DefaultMQPushConsumer 作为启动入口类,它的 start 方法调用了 DefaultMQPushConsumerImpl 类的 start 方法,下面重点看一下这个方法。
1.1 拷贝订阅关系
start 方法中调用了 copySubscription 方法,代码如下:
private void copySubscription() throws MQClientException {
try {
//拷贝订阅关系
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
break;
case CLUSTERING:
final String retryTopic = MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup());
SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(retryTopic, SubscriptionData.SUB_ALL);
this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData);
break;
default:
break;
}
} catch (Exception e) {
throw new MQClientException("subscription exception", e);
}
}

这里的代码有一点需要注意:集群模式会创建一个重试 Topic 的订阅关系,而广播模式是不会创建这个订阅关系的。也就是说广播模式不考虑重试。
1.2 初始化偏移量
下面是初始化 offset 的代码:
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}

从上面的代码可以看到,广播模式使用了 LocalFileOffsetStore,也就是说偏移量保存在客户端本地,除了在内存中会保存,在本地文件中也会保存。
2 消息拉取
ConsumeMessageService 是真正拉取消息的地方,消费者初始化时会初始化 ConsumeMessageService,并且这里会区分并发消息还是顺序消息。
2.1 顺序消息
在集群模式下,需要获取到 processQueue 的锁才会拉取消息,而在广播模式下,不用获取锁,直接就可以拉取消息。判断逻辑如下:
//ConsumeMessageOrderlyService.ConsumeRequest
final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
synchronized (objLock) {
if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
|| (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
}
}

这里有个疑问,对于顺序消息,获取锁是必须的,这样才能保证一个 processQueue 只能由一个线程进行处理,从而保证消费的顺序性。那对于广播模式,为什么不用获取 processQueue 的 锁呢?难道广播模式不支持顺序消息?
2.2 并发消息
对于并发消息,广播模式不同的是,对消费结果的处理。集群模式消费失败后需要把消息发送回 Broker 等待再次被拉取,而广播模式则不需要重试。代码如下:
//ConsumeMessageConcurrentlyService.rocessConsumeResult
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
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);
boolean result = this.sendMessageBack(msg, context);
if (!result) {
msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
msgBackFailed.add(msg);
}
}

if (!msgBackFailed.isEmpty()) {
consumeRequest.getMsgs().removeAll(msgBackFailed);

this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
}
break;
default:
break;
}

这再次说明,广播模式是不支持消息重试的。
重平衡
在消费者启动过程中,会调用 RebalanceService 的 start 方法,进行重平衡。从重平衡的代码中可以看到,广播模式消费者会消费所有 MessageQueue,而集群模式下会根据负载均衡策略选择其中几个 MessageQueue。代码如下:
private void rebalanceByTopic(final String topic, final boolean isOrder) {
switch (messageModel) {
case BROADCASTING: {
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
if (mqSet != null) {
boolean changed = this.updateProcessQueueTableInRebalance(topic, mqSet, isOrder);
//省略部分逻辑
} else {
}
break;
}
case CLUSTERING: {
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
//省略部分逻辑
if (mqSet != null && cidAll != null) {
//省略部分逻辑
try {
allocateResult = strategy.allocate(
this.consumerGroup,
this.mQClientFactory.getClientId(),
mqAll,
cidAll);
} catch (Throwable e) {
return;
}

Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
if (allocateResult != null) {
allocateResultSet.addAll(allocateResult);
}

boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
//省略部分逻辑
}
break;
}
default:
break;
}
}

上面 updateProcessQueueTableInRebalance 这个方法调用前,要获取到需要消费的 MessageQueue 集合。广播模式下,直接取了订阅的 Topic 下的所有集合元素,而集群模式下,则需要通过负责均衡获取当前消费者自己要消费的 MessageQueue 集合。
4 总结
本文主要讲解了 RocketMQ 广播消息的实现机制,理解广播消息,要把握下面几点:
1.偏移量保存在消费者本地内存和文件中;
2.广播消息不支持重试;
3.从源码上看,广播模式并不能支持顺序消息;
4.广播模式消费者订阅了 Topic 下的所有 MessageQueue,不会重平衡。
·············· END ··············

发布于 2022-07-20 13:19・IP 属地湖南

​ 修改​赞同​添加评论

​分享

​收藏一键生成视频

​设置

收起​

更多回答

Jaskey Lam

腾讯 架构师

7 人赞同了该回答

从你现象上看,最可能的情况是:你的消费端A 和 B属于同一个消费组(ConsumerGroup)。但是两个实例(消费端)却订阅了不同的topic即不同的订阅关系(一个订阅T1,一个订阅T2)。这在RocketMQ中是明确声明过的——“同一个消费组应该拥有完全一样的订阅关系”。

拥有不同的订阅关系就有可能会导致你的现象。具体底层的原因来自于RocketMQ对于负载均衡分配queue的策略。简单的说就是每次分配queue的时候,相互的计算互相被干扰了,导致了漏分queue的现象。

更多负载均衡的解析请参看:

RocketMQ中关于消费者Rebalance过程? - Jaskey Lam 的回答 - 知乎RocketMQ--水平扩展及负载均衡详解 - 薛定谔的风口猪

编辑于 2017-02-04 10:29

​赞同 7​​1 条评论

​分享

​收藏​喜欢

后端进阶

1 人赞同了该回答

1.

假设有消费组 g1,有消费者 c1 和 c2,c1 订阅了 topicA,c2 订阅了 topicB,集群内有 broker1 和broker2,假设 topicA 有 8 个消息队列,broker_a(q0/q1/q2/q3) 和 broker_b(q0/q1/q2/q3),前面我们知道 findConsumerIdList 方法会获取消费组内所有消费者客户端 ID,topicA 经过平均分配算法进行分配之后的消费情况如下:

c1:broker_a(q0/q1/q2/q3)

c2:broker_b(q0/q1/q2/q3)

问题就出现在这里,c2 根本没有订阅 topicA,但根据分配算法,却要加上 c2 进行分配,这样就会导致这种情况有一半的消息被分配到 c2 进行消费,被分配到 c2 的消息队列会延迟十几秒甚至更久才会被消费,topicB 同理

2.

假设有消费者组 g1,g1下有消费者 c1 和消费者 c2,c1 订阅了 topicA,c2 订阅了 topicB,此时c2 先启动,将 g1 的订阅信息更新为 topicB,c1 随后启动,将 g1 的订阅信息覆盖为 topicA,c1 的 Rebalance 负载将 topicA 的 pullRequest 添加到 pullRequestQueue 中,而恰好此时 c2 心跳包又将 g1 的订阅信息更新为 topicB,那么此时 c1 的 PullMessageService 线程拿到 pullRequestQueue 中 topicA 的 pullRequest 进行消息拉取,然而在 broker 端找不到消费者组 g1 下 topicA 的订阅信息(因为此时恰好被 c2 心跳包给覆盖了),就会报消费者订阅信息不存在的错误了

Spring Cloud Alibaba RocketMQ中,编写一个用于消费消息的`main`方法通常涉及以下几个步骤: 1. 引入依赖: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-rocketmq-consumer</artifactId> </dependency> ``` 2. 创建ConsumerConfig实例并设置配置: ```java @Value("${rocketmq.consumer.group.id}") private String consumerGroup; ConsumerConfig consumerConfig = new DefaultMQPushConsumerConfig(); consumerConfig.setInstanceName("myConsumer"); consumerConfig.setConsumerGroup(consumerGroup); ``` 3. 实例化`RocketMQTemplate`和`RocketMQPushConsumer`: ```java RocketMQPushConsumer consumer = new RocketMQPushConsumer(consumerConfig); consumer.registerMessageListener(new MessageListenerConcurrently consumingTopic, new MyMessageListener()); ``` 4. 启动消费者: ```java consumer.start(); try { while (true) { Thread.sleep(1000 * 60 * 60); // 每小时检查一次新的消息 } } catch (InterruptedException e) { e.printStackTrace(); } ``` 5. 实现`MyMessageListener`来处理接收到的消息: ```java public class MyMessageListener implements MessageListenerConcurrently { @Override public ConsumeStatus consume(List<MessageExt> msgs, ConsumptionContext context) { for (MessageExt msg : msgs) { System.out.printf("Received message: %s from topic %s\n", msg.getBody(), msg.getTopic()); } return ConsumeStatus.CONSUME_SUCCESS; } } ``` 完整的`main`方法示例: ```java public static void main(String[] args) { SpringApplication.run(MyConsumerApplication.class, args); // ...上面的配置和启动消费者代码... } ``` 注意:你需要根据实际环境调整`consumerGroup`和`consumingTopic`等参数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值