RocketMQ的消费者,在订阅topic的时候需要遵循“订阅关系一致性”原则,即:一个消费者分组(group)下的所有消费者实例的处理逻辑必须一致,一旦订阅关系不一致就会导致消费混乱,甚至消息丢失。对大多数分布式应用来说,一个group下通常会挂有多个consumer实例。由于RocketMq的消费者订阅关系由Topic+Tag组成,因此保持订阅一致就意味着,所有consumer实例需要保证:
- 订阅的topic必须一致
- 订阅topic中的tag必须一致
通俗的讲就是一个消费者组GroupA,有consumerA和consumerB,消费者A订阅了topicA、tagA,消费者B订阅了topicB、tagB就会导致订阅不一致问题。
不一致原因解析
问题一:订阅消息相互覆盖
消费者的信息在broker里面是通过一个map存储的,key是groupName,value是组的信息
//org.apache.rocketmq.broker.client.ConsumerManager
private final ConcurrentMap<String/* Group */, ConsumerGroupInfo> consumerTable =
new ConcurrentHashMap<String, ConsumerGroupInfo>(1024);
ConsumerGroupInfo里面存储了该group里面订阅的topic信息,同样使用map存储,key是topic,所以当相同group下面的消费者,订阅的topic如果不一致,就会覆盖map里面的值。
private final ConcurrentMap<String/* Topic */, SubscriptionData> subscriptionTable =
new ConcurrentHashMap<String, SubscriptionData>();
下面我们来看看具体的源码实现:当我们启动消费者的时候(调用DefaultMQPushConsumer.start()方法),会启动MQ客户端。
//DefaultMQPushConsumerImpl.start()
///
mQClientFactory.start();
mq客户端会启动心跳发送线程,定时向broker发送心跳信息。
//MQClientInstance.startScheduledTask()
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
//清理离线broker
MQClientInstance.this.cleanOfflineBroker();
//发送心跳信息到broker
MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
} catch (Exception e) {
log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
}
}
}, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);
重点看这个sendHeartbeatToAllBrokerWithLock方法,点进去之后继续看sendHeartbeatToAllBroker()方法
private void sendHeartbeatToAllBroker() {
final HeartbeatData heartbeatData = this.prepareHeartbeatData();
final boolean producerEmpty = heartbeatData.getProducerDataSet().isEmpty();
final boolean consumerEmpty = heartbeatData.getConsumerDataSet().isEmpty();
//如果消费者和生产者信息为空,直接返回
if (producerEmpty && consumerEmpty) {
log.warn("sending heartbeat, but no consumer and no producer");
return;
}
if (!this.brokerAddrTable.isEmpty()) {
//记录了发送心跳的次数
long times = this.sendHeartbeatTimesTotal.getAndIncrement();
Iterator<Entry<String, HashMap<Long, String>>> it = this.brokerAddrTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, HashMap<Long, String>> entry = it.next();
String brokerName = entry.getKey();
HashMap<Long, String> oneTable = entry.getValue();
if (oneTable != null) {
for (Map.Entry<Long, String> entry1 : oneTable.entrySet()) {
Long id = entry1.getKey();
String addr = entry1.getValue();
if (addr != null) {
if (consumerEmpty) {
if (id != MixAll.MASTER_ID)
continue;
}
try {
//向broker心跳。
int version = this.mQClientAPIImpl.sendHearbeat(addr, heartbeatData, 3000);
if (!this.brokerVersionTable.containsKey(brokerName)) {
this