场景:2 个消费者进程中,创建了 2 个消费者,同属于 1 个消费组,但是订阅了不同的 topic,会因为订阅信息相互覆盖,导致拉不到消息。

原因是 rocketMQ 的订阅关系,是根据 group 来管理的,c1 订阅 t1,c2 订阅 t2,他们同属于 group,当 c1 拉取 t1 的消息时,broker 发现 group 订阅的是 t2,就不会返回消息给 c1。

client 在 MQClientInstance 中发送心跳给所有的 broker,心跳中包含 consumer 的订阅信息,

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
public void run() {
try {
MQClientInstance.this.cleanOfflineBroker();
MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
} catch (Exception e) {
log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
}
}
}, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);
// org.apache.rocketmq.client.impl.factory.MQClientInstance#prepareHeartbeatData
private HeartbeatData prepareHeartbeatData() {
HeartbeatData heartbeatData = new HeartbeatData(); // clientID
heartbeatData.setClientID(this.clientId); // Consumer
for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
MQConsumerInner impl = entry.getValue();
if (impl != null) {
ConsumerData consumerData = new ConsumerData();
consumerData.setGroupName(impl.groupName());
consumerData.setConsumeType(impl.consumeType());
consumerData.setMessageModel(impl.messageModel());
consumerData.setConsumeFromWhere(impl.consumeFromWhere());
consumerData.getSubscriptionDataSet().addAll(impl.subscriptions());
consumerData.setUnitMode(impl.isUnitMode()); heartbeatData.getConsumerDataSet().add(consumerData);
}
} // Producer
for (Map.Entry<String/* group */, MQProducerInner> entry : this.producerTable.entrySet()) {
MQProducerInner impl = entry.getValue();
if (impl != null) {
ProducerData producerData = new ProducerData();
producerData.setGroupName(entry.getKey()); heartbeatData.getProducerDataSet().add(producerData);
}
} return heartbeatData;
}

broker 处理心跳请求

// org.apache.rocketmq.broker.processor.ClientManageProcessor#heartBeat
public RemotingCommand heartBeat(ChannelHandlerContext ctx, RemotingCommand request) {
RemotingCommand response = RemotingCommand.createResponseCommand(null);
HeartbeatData heartbeatData = HeartbeatData.decode(request.getBody(), HeartbeatData.class);
ClientChannelInfo clientChannelInfo = new ClientChannelInfo(
ctx.channel(),
heartbeatData.getClientID(),
request.getLanguage(),
request.getVersion()
); for (ConsumerData data : heartbeatData.getConsumerDataSet()) {
SubscriptionGroupConfig subscriptionGroupConfig =
this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(
data.getGroupName());
boolean isNotifyConsumerIdsChangedEnable = true;
if (null != subscriptionGroupConfig) {
isNotifyConsumerIdsChangedEnable = subscriptionGroupConfig.isNotifyConsumerIdsChangedEnable();
int topicSysFlag = 0;
if (data.isUnitMode()) {
topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
}
String newTopic = MixAll.getRetryTopic(data.getGroupName());
this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
newTopic,
subscriptionGroupConfig.getRetryQueueNums(),
PermName.PERM_WRITE | PermName.PERM_READ, topicSysFlag);
} boolean changed = this.brokerController.getConsumerManager().registerConsumer(
data.getGroupName(),
clientChannelInfo,
data.getConsumeType(),
data.getMessageModel(),
data.getConsumeFromWhere(),
data.getSubscriptionDataSet(),
isNotifyConsumerIdsChangedEnable
); if (changed) {
log.info("registerConsumer info changed {} {}",
data.toString(),
RemotingHelper.parseChannelRemoteAddr(ctx.channel())
);
}
} for (ProducerData data : heartbeatData.getProducerDataSet()) {
this.brokerController.getProducerManager().registerProducer(data.getGroupName(),
clientChannelInfo);
}
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}

broker 比较当前 consumer group 的订阅信息,如果当前消费组加入了新的消费者,或者订阅的 topic 发生了变化,则通知消费者进行 rebalance

// org.apache.rocketmq.broker.client.ConsumerManager#registerConsumer
public boolean registerConsumer(final String group, final ClientChannelInfo clientChannelInfo,
ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere,
final Set<SubscriptionData> subList, boolean isNotifyConsumerIdsChangedEnable) { ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group);
if (null == consumerGroupInfo) {
ConsumerGroupInfo tmp = new ConsumerGroupInfo(group, consumeType, messageModel, consumeFromWhere);
ConsumerGroupInfo prev = this.consumerTable.putIfAbsent(group, tmp);
consumerGroupInfo = prev != null ? prev : tmp;
} boolean r1 =
consumerGroupInfo.updateChannel(clientChannelInfo, consumeType, messageModel,
consumeFromWhere);
// 本文讨论的场景,重点在这,group 的订阅数据被覆盖
boolean r2 = consumerGroupInfo.updateSubscription(subList); if (r1 || r2) {
if (isNotifyConsumerIdsChangedEnable) {
this.consumerIdsChangeListener.handle(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel());
}
} this.consumerIdsChangeListener.handle(ConsumerGroupEvent.REGISTER, group, subList); return r1 || r2;
}

发生 ConsumerGroupEvent.CHANGE 事件,broker 通知 client 进行 rebalance

// org.apache.rocketmq.broker.client.DefaultConsumerIdsChangeListener#handle
public void handle(ConsumerGroupEvent event, String group, Object... args) {
if (event == null) {
return;
}
switch (event) {
case CHANGE:
if (args == null || args.length < 1) {
return;
}
List<Channel> channels = (List<Channel>) args[0];
if (channels != null && brokerController.getBrokerConfig().isNotifyConsumerIdsChangedEnable()) {
for (Channel chl : channels) {
this.brokerController.getBroker2Client().notifyConsumerIdsChanged(chl, group);
}
}
break;
case UNREGISTER:
this.brokerController.getConsumerFilterManager().unRegister(group);
break;
case REGISTER:
if (args == null || args.length < 1) {
return;
}
Collection<SubscriptionData> subscriptionDataList = (Collection<SubscriptionData>) args[0];
this.brokerController.getConsumerFilterManager().register(group, subscriptionDataList);
break;
default:
throw new RuntimeException("Unknown event " + event);
}
}

broker 向 client 发送 NOTIFY_CONSUMER_IDS_CHANGED 请求

// org.apache.rocketmq.broker.client.net.Broker2Client#notifyConsumerIdsChanged
public void notifyConsumerIdsChanged(
final Channel channel,
final String consumerGroup) {
if (null == consumerGroup) {
log.error("notifyConsumerIdsChanged consumerGroup is null");
return;
} NotifyConsumerIdsChangedRequestHeader requestHeader = new NotifyConsumerIdsChangedRequestHeader();
requestHeader.setConsumerGroup(consumerGroup);
RemotingCommand request =
RemotingCommand.createRequestCommand(RequestCode.NOTIFY_CONSUMER_IDS_CHANGED, requestHeader); try {
this.brokerController.getRemotingServer().invokeOneway(channel, request, 10);
} catch (Exception e) {
log.error("notifyConsumerIdsChanged exception, " + consumerGroup, e.getMessage());
}
}

client 收到 NOTIFY_CONSUMER_IDS_CHANGED 请求,触发 rebalance

// org.apache.rocketmq.client.impl.ClientRemotingProcessor#notifyConsumerIdsChanged
public RemotingCommand notifyConsumerIdsChanged(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
try {
final NotifyConsumerIdsChangedRequestHeader requestHeader =
(NotifyConsumerIdsChangedRequestHeader) request.decodeCommandCustomHeader(NotifyConsumerIdsChangedRequestHeader.class);
log.info("receive broker's notification[{}], the consumer group: {} changed, rebalance immediately",
RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
requestHeader.getConsumerGroup());
this.mqClientFactory.rebalanceImmediately();
} catch (Exception e) {
log.error("notifyConsumerIdsChanged exception", RemotingHelper.exceptionSimpleDesc(e));
}
return null;
}

需要说明的是,client 一直在进行 rebalance,只是间隔 10s 而已。这里只是让 client 立马 rebalance,但其实并 没有任何效果,因为消费者数量没有变化。

但是由于 broker 保存的订阅关系一直被修改,导致 consumer 去拉数据时,会因为订阅关系不存在,而拉不到数据。

subscriptionData = consumerGroupInfo.findSubscriptionData(requestHeader.getTopic());
if (null == subscriptionData) {
log.warn("the consumer's subscription not exist, group: {}, topic:{}", requestHeader.getConsumerGroup(), requestHeader.getTopic());
response.setCode(ResponseCode.SUBSCRIPTION_NOT_EXIST);
response.setRemark("the consumer's subscription not exist" + FAQUrl.suggestTodo(FAQUrl.SAME_GROUP_DIFFERENT_TOPIC));
return response;
}

rocketMQ 订阅关系的更多相关文章

  1. [置顶] 吃论扯谈---吃货和Office 365订阅的关系

    什么事物都可以和吃联系起来,在女孩子穿裙子的季节这是一个悲伤的故事! 说明: :Office365是微软云计算产品之一,其采取订阅的方式,按人头*每月的方式付费,用户可以选择自己需要的服务 2:Off ...

  2. RocketMQ原理解析-Broker

    broker 1. broker的启动 brker的启动 Broker向namesrv注册 1. 获取namesrv的地址列表(是乱序的) 2. 遍历向每个namesrv注册topic的配置信息top ...

  3. RocketMQ原理解析-Consumer

    consumer 1.启动 有别于其他消息中间件由broker做负载均衡并主动向consumer投递消息,RocketMq是基于拉模式拉取消息,consumer做负载均衡并通过长轮询向broker拉消 ...

  4. 阿里 RocketMQ 安装与简介

    一.简介 官方简介: l  RocketMQ是一款分布式.队列模型的消息中间件,具有以下特点: l  能够保证严格的消息顺序 l  提供丰富的消息拉取模式 l  高效的订阅者水平扩展能力 l  实时的 ...

  5. rocketmq(1)

    参考: 开源社区:https://github.com/alibaba/RocketMQ rocketmq入门: http://www.cnblogs.com/LifeOnCode/p/4805953 ...

  6. 入门rocketmq从浅到深

    目录 一.引言 二.介绍 三.Rocketmq关键概念 1.主题与标签 2.发送与订阅群组 3.Broker与NameServer 4.广播消费与集群消费 5.消息队列 6.集群方式 7.顺序消息 8 ...

  7. RocketMQ详解

    原文链接:http://www.cnblogs.com/xiaodf/p/5075167.html 简介 官方简介: RocketMQ是一款分布式.队列模型的消息中间件,具有以下特点:  能够保证严格 ...

  8. RocketMQ 自己的整理和理解

    每个人的想法不同, RocketMQ 介绍的时候就说 是阿里从他们使用的上 解耦出来 近一步简化 便捷的 目的当然是 让其能快速入手和开发 如果不是在项目设计层面上 只是使用的话 从Git上下载该项目 ...

  9. 关于RocketMQ消息消费与重平衡的一些问题探讨

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

随机推荐

  1. orm之peewee

    peewee是一款orm框架,为什么选择peewee,是因为它比较简单和Django比较类似,而且还有一个async-peewee,可以进行异步化. 如何定义model和生成表 ''' 我们要定义两张 ...

  2. ELK监控交换机日志

    一.首先部署logstash监控UDP514端口,新建一个配置文件cisco.conf 交换机是通过配置rsyslog服务器来将日志发送到日志服务器的,所以需要在logstash上配置rsyslog监 ...

  3. Foo, Bar的含义

    有些朋友问:foo, bar是什么意思, 为什么C++书籍中老见到这个词.我google了一下, 发现没有很好的中文答案.这个问题,在维基百科上有很好的回答.在这里翻译给大家. 译文: 术语fooba ...

  4. 《编译原理》求 FIRSTVT 集和 LASTVT 集的步骤 - 例题解析

    <编译原理>求 FIRSTVT 集和 LASTVT 集的步骤 - 例题解析 算符优先关系表的构造中涉及到求 FIRSTVT 集和 LASTVT 集. 表示及含义: FIRSTVT(T) 非 ...

  5. 【hiho1715】树的联通问题

    题目大意:给定一棵 1~n 标号的树.Tree[L,R]表示最少需要选择的边的数量使得 L~R 号点两两连通.求: \[ \sum_{L=1}^{n} \sum_{R=L}^{n} \operator ...

  6. VCL界面控件DevExpress VCL Controls v19.1.3全新来袭

    DevExpress VCL Controls是 Devexpress公司旗下最老牌的用户界面套包.所包含的控件有:数据录入,图表,数据分析,导航,布局,网格,日程管理,样式,打印和工作流等,让您快速 ...

  7. Spring 静态代理和动态代理

    现在我们来模拟一下,某位学生去考试. 假设他(小明)正常的考试. 运行结果:        结果: 突然某一天,他睡过头了,来不急去考试,所有他打算叫另一个人(Cheater)去代替他考试. 运行结果 ...

  8. shell中的控制流结构

    shell中的控制流结构 1.if...then..else..fi语句 2.case语句 3.for循环 4.until 语句 5.while循环 6.break控制 7.continue 控制 1 ...

  9. Python CGI编程Ⅲ

    GET和POST方法 浏览器客户端通过两种方法向服务器传递https://www.xuanhe.net/信息,这两种方法就是 GET 方法和 POST 方法. 使用GET方法传输数据 GET方法发送编 ...

  10. 【LOJ3156】「NOI2019」回家路线

    [题目链接] [点击打开链接] [题目概括] 现在有\(n\)个站点,\(m\)条火车路线,每一条货车路线都有一个起点站点.终点站点.开始时间和到站时间. 对于一直在起点\(1\)的人,终点是\(n\ ...