再均衡
再均衡是指分区的所属权从一个消费者转移到另一消费者的行为,它为消费组具备高可用
性和伸缩性提供保障,使我们可以既方便 又安全地删除消费组内的消费者或往消费组内添加消 费者。
不过在再均衡发生期间,消费组内的消费者是无法读取消息的。 也就是说,在再均衡发生期间的这一小段时间内,消费组会变得不可用 。另外,当 一个分区被重新分配给另一个消费 者时, 消费者当前的状态也会丢失。
比如消费者消费完某个分区中的一部分消息时还没有来得 及提交消费位移就发生了再均衡操作 , 之后这个分区又被分配给了消费组内的另一个消费者, 原来被消费完的那部分消息又被重新消费一遍,这也是Kafka消息重复消息的一个例子。
为了再均衡发生时候保证系统的稳定,Kafka为我们提供了 ConsumerRebalanceListener用来在在均衡发生的开始和末尾 做一些我们需要做的事情。
ConsumerRebalanceListener是一个接口,包含两个方法:
void onPartitionsRevoked(Collection<TopicPartition> partitions)
这个方法会在再均衡开始之前和消费者停止读取消息之后被调用。可以通过这个回调方法
来处理消费位移 的提交, 以此来避免一些不必要的重复消费现象的发生。参数 partitions 表 示再均衡前所分配到的分区。
void onPartitionsAssigned(Collection<TopicPartition> partitions)
这个方法会在重新分配分区之后和消费者开始读取消费之前被调用 。参数 partitions 表
示再均衡后所分配到的分区 。
实例代码:
public static Properties initConfig() {
Properties props = new Properties();
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
return props;
}
public static void main(String[] args) {
Properties props = initConfig();
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
consumer.subscribe(Arrays.asList(topic), new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
consumer.commitSync(currentOffsets);
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
//do nothing.
}
});
try {
while (isRunning.get()) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
//process the record.
currentOffsets.put(
new TopicPartition(record.topic(), record.partition()),
new OffsetAndMetadata(record.offset() + 1));
}
consumer.commitAsync(currentOffsets, null);
}
} finally {
consumer.close();
}
}
指定位移消费
通过seek()来指定从分区某个offset处开始消费, 代码从所有分区的头部开始读取
public static void main(String[] args) {
Properties props = initConfig();
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList(topic));
consumer.poll(Duration.ofMillis(7000));
Set<TopicPartition> assignment = consumer.assignment();
System.out.println("assignment :" + assignment);
for (TopicPartition tp : assignment) {
consumer.seek(tp, 0);
}
// consumer.seek(new TopicPartition(topic,0),10);
while (true) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(1000));
//consume the record.
for (ConsumerRecord<String, String> record : records) {
System.out.println(record.partition() + ": " + record.offset() + ":" + record.value());
}
}
}
不过Kafka也为我们提供了一些专门用作从特定位置消费的API, 和基于时间轮度的消费API.
@Override
public Map<TopicPartition, Long> endOffsets(Collection<TopicPartition> partitions) {
return endOffsets(partitions, Duration.ofMillis(requestTimeoutMs));
}
@Override
public Map<TopicPartition, Long> beginningOffsets(Collection<TopicPartition> partitions) {
return beginningOffsets(partitions, Duration.ofMillis(defaultApiTimeoutMs));
}
这些API都在KafkaConsumer类中,我们可以自行去看代码.
消费者拦截器
用过Kafka的都知道,生产者用起来比消费者要简单得多。那么对应生产者拦截器的使用,对应 的消费者也有相应的拦截器的概念。
消费者拦截器主要在消费到消息或在提交消费位移时进行一些定制化的操作。与生产者拦截器对应的,消费者拦截器需要自定义实现 org.apache.kafka.clients.consumer.Consumerlnterceptor接口。 ConsumerInterceptor接口包含 3 个方法:
- public ConsumerR巳cords<K, V> onConsume(ConsumerRecords<K, V> records);
- public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets);
- public void close()
KafkaConsumer会在 poll()方法返回之前调用拦截器的 onConsume()方法来对消息进行相应 的定制化操作, 比如修改返回的消息内容、按照某种规则过滤消息。
KafkaConsumer会在提交完消费位移之后调用拦截器的 onCommit()方法,可以使用这个方 法来记录跟踪所提交的位移信息,比如当消费者使用 commitSync 的无参方法时,我们不知道提 交的消费位移的具体细节,而使用拦截器的 onCommit()方法却可以做到这一点。