1.問題が再発する
1.説明
2つの同一のコンシューマーグループのコンシューマーは、同じトピックで異なるタグをサブスクライブします。コンシューマー1はトピックタグ1をサブスクライブし、コンシューマー2はトピックタグ2をサブスクライブしてから、別々に開始します。このとき、トピックのtag1に10個のデータが送信され、トピックのtag2に10個のデータが送信されます。目視検査では、Consumer1とConsumer2がそれぞれ10個の対応するメッセージを受信したことを確認する必要があります。その結果、Consumer2のみがメッセージを受信し、4〜6個のメッセージのみが受信されましたが、これは修正されていません。
2.コード
2.1、消費者
public class Consumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-consumer");
consumer.setNamesrvAddr("124.57.180.156:9876");
consumer.subscribe("TopicTest2","tag1");
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(
List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
MessageExt msg = msgs.get(0);
System.out.println(msg.getTags());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("ConsumerStarted.");
}
}
TopicTest2のtag1タグをサブスクライブするコンシューマーを起動してから、tag1をtag2に変更して、コンシューマーを再起動します。これは、2つのコンシューマープロセスを開始することと同じです。1つはTopicTest2のtag1タグにサブスクライブし、もう1つはTopicTest2のtag2タグにサブスクライブします。
2.2、プロデューサー
public class Producer {
public static void main(String[] args) throws MQClientException {
final DefaultMQProducer producer = new DefaultMQProducer("test-producer");
producer.setNamesrvAddr("124.57.180.156:9876");
producer.start();
for (int i = 0; i < 10; i++){
try {
Message msg = new Message("TopicTest2", "tag1", ("Hello tag1 - "+i).getBytes());
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}catch(Exception e) {
e.printStackTrace();
}
}
}
}
プロデューサーを起動し、TopicTest2のtag1に10個のメッセージを送信します。tag1をtag2に再度変更してから、Producerを再度起動して送信します。これにより、TopicTest2のtag1の下に10個のメッセージがあり、TopicTest2のtag2の下に10個のメッセージがあります。
3.結果
コンシューマーとプロデューサーの両方が開始された後、以下が見つかります。
-
プロデューサーは通常20メッセージを送信しました。
-
Consumer1はtag1の下のデータを消費しませんでした
-
Consumer2は、メッセージの半分(必ずしも少数ではなく、場合によっては5、場合によっては6)のメッセージを消費します。
第二に、質問への答え
-
まず第一に、これは消費者ではなくブローカーによって決定されます
私は以前に記事を読んだことがあり、それは正当化され、十分に根拠があります。それは消費者側で書かれ、デバッグソースコードも投稿され、後者は前者をカバーしていると言っていますが、言いたいのは、あなたは2つの独立したものを始めました消費者、つまり2つの独立したプロセスであり、カバーする、またはカバーしないという問題はなく、独立しています。たった1つのJVM。共有JVMではありませんが、どのように上書きできますか?
-
コンシューマーはハートビートをブローカーに送信し、ブローカーはそれを受信した後、それをconsumerTable(つまり、Map)に格納します。キーはGroupNameで、値はConsumerGroupInfoです。
-
ConsumerGroupInfoにはトピックやその他の情報が含まれていますが、問題は前のステップにあります。キーはgroupNameです。GroupNameと同じ場合、ブローカーのハートビートによって最後に受信されたコンシューマーが前者を上書きします。これは、次のコードと同等です。
map.put(groupName, ConsumerGroupInfo);
このようにして、同じキーを上書きする必要があります。したがって、Consumer1はメッセージを受信しませんが、Consumer2が受信したメッセージが半分(固定ではない)しかないのはなぜですか?
これは、クラスターモードで消費している場合、負荷分散して各ノードに分散して消費するため、メッセージの半分(固定数ではない)がConsumer1に送信され、その結果、Consumer1がtag1にサブスクライブするためです。出力なし。
BROADCASTINGに変更すると、ブロードキャストはすべてのコンシューマーをブロードキャストするため、後者は確実に半分ではなくすべてのメッセージを受信します。
3、ソースコードの検証
1.コールチェーン
# 核心在于如下这个方法
org.apache.rocketmq.broker.client.ConsumerManager#registerConsumer()
# 关键调用链如下
# 入口是Broker启动的时候
org.apache.rocketmq.broker.BrokerStartup#start()
org.apache.rocketmq.broker.BrokerController#start()
org.apache.rocketmq.remoting.netty.NettyRemotingServer#start()
org.apache.rocketmq.remoting.netty.NettyRemotingServer#prepareSharableHandlers()
org.apache.rocketmq.remoting.netty.NettyRemotingServer.NettyServerHandler#channelRead0()
org.apache.rocketmq.remoting.netty.NettyRemotingAbstract#processMessageReceived()
org.apache.rocketmq.remoting.netty.NettyRemotingAbstract#processRequestCommand()
org.apache.rocketmq.broker.processor.ClientManageProcessor#processRequest()
org.apache.rocketmq.broker.processor.ClientManageProcessor#heartBeat()
org.apache.rocketmq.broker.client.ConsumerManager#registerConsumer()
2.ソースコード
2.1、registerConsumer
/**
* Consumer信息
*/
public class ConsumerGroupInfo {
// 组名
private final String groupName;
// topic信息,比如topic、tag等
private final ConcurrentMap<String/* Topic */, SubscriptionData> subscriptionTable =
new ConcurrentHashMap<String, SubscriptionData>();
// 客户端信息,比如clientId等
private final ConcurrentMap<Channel, ClientChannelInfo> channelInfoTable =
new ConcurrentHashMap<Channel, ClientChannelInfo>(16);
// PULL/PUSH
private volatile ConsumeType consumeType;
// 消费模式:BROADCASTING/CLUSTERING
private volatile MessageModel messageModel;
// 消费到哪了
private volatile ConsumeFromWhere consumeFromWhere;
}
/**
* 通过心跳将Consumer信息注册到Broker端。
*/
public boolean registerConsumer(final String group, final ClientChannelInfo clientChannelInfo,
ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere,
final Set<SubscriptionData> subList, boolean isNotifyConsumerIdsChangedEnable) {
// consumerTable:维护所有的Consumer
ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group);
// 如果没有Consumer,则put到map里
if (null == consumerGroupInfo) {
ConsumerGroupInfo tmp = new ConsumerGroupInfo(group, consumeType, messageModel, consumeFromWhere);
// put到map里
ConsumerGroupInfo prev = this.consumerTable.putIfAbsent(group, tmp);
consumerGroupInfo = prev != null ? prev : tmp;
}
// 更新Consumer信息,客户端信息
boolean r1 =
consumerGroupInfo.updateChannel(clientChannelInfo, consumeType, messageModel,
consumeFromWhere);
// 更新订阅Topic信息
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;
}
このステップから、消費者情報がマップ(consumerTable)に格納され、groupNameがキー、ConsumerGroupInfoが値であることがわかります。キーが同じであるため、後者が前者を確実に上書きすることは明らかです。後者のタグはtag2であり、ConsumerGroupInfoのsubscriptionTableに格納されている前者のtag1をカバーする必要があります。
private final ConcurrentMap<String/* Topic */, SubscriptionData> subscriptionTable = new ConcurrentHashMap<String, SubscriptionData>();
SubscriptionDataには、トピックおよびその他の情報が含まれています
public class SubscriptionData implements Comparable<SubscriptionData> { // topic private String topic; private String subString; // tags private Set<String> tagsSet = new HashSet<String>(); private Set<Integer> codeSet = new HashSet<Integer>(); }
2.22つの問題
1.トピック、タグ、その他の情報はどのようにカバーされていますか?
boolean r1 = consumerGroupInfo.updateChannel(clientChannelInfo, consumeType, messageModel,consumeFromWhere);
/**
* 其实很简单,就是以topic为key,SubscriptionData为value。而SubscriptionData里包含了tags信息,所以直接覆盖掉
*/
public boolean updateSubscription(final Set<SubscriptionData> subList) {
for (SubscriptionData sub : subList) {
SubscriptionData old = this.subscriptionTable.get(sub.getTopic());
if (old == null) {
SubscriptionData prev = this.subscriptionTable.putIfAbsent(sub.getTopic(), sub);
} else if (sub.getSubVersion() > old.getSubVersion()) {
this.subscriptionTable.put(sub.getTopic(), sub);
}
}
}
待って、ここに新しい発見があるようですConsumerGroupInfo#subscriptionTable
// {@link org.apache.rocketmq.broker.client.ConsumerGroupInfo#subscriptionTable}
private final ConcurrentMap<String/* Topic */, SubscriptionData> subscriptionTable =
new ConcurrentHashMap<String, SubscriptionData>();
トピックがマップの鍵であるという急降下が発生する可能性があります。1人のコンシューマーが複数のトピックをサブスクライブできる可能性はありませんか?はい、このソースコードには何の問題もないことがわかりました。私もテストしました。
2.このように見ると、同じグループの登録が上書きされるため、コンシューマー側のプロセスは1つだけになりますか?
ブラザー、ConsumerGroupInfoのchannelInfoTableに注意してください
// 客户端信息,比如clientId等
private final ConcurrentMap<Channel, ClientChannelInfo> channelInfoTable =
new ConcurrentHashMap<Channel, ClientChannelInfo>(16);
ClientChannelInfoには、clientIdなどの情報が含まれ、コンシューマーを表します。登録方法は次のとおりです。
boolean r2 = consumerGroupInfo.updateSubscription(subList);
/**
* 下面是删减后的代码,其实就是以Channel作为key,每个Consumer的Channel是不一样的。所以能存多个Consumer客户端
*/
public boolean updateChannel(final ClientChannelInfo infoNew, ConsumeType consumeType,
MessageModel messageModel, ConsumeFromWhere consumeFromWhere) {
ClientChannelInfo infoOld = this.channelInfoTable.get(infoNew.getChannel());
if (null == infoOld) {
ClientChannelInfo prev = this.channelInfoTable.put(infoNew.getChannel(), infoNew);
}
}