再均衡
再均衡是指分區的所屬權從一個消費者轉移到另一消費者的行爲,它爲消費組具備高可用
性和伸縮性提供保障,使我們可以既方便 又安全地刪除消費組內的消費者或往消費組內添加消 費者。
不過在再均衡發生期間,消費組內的消費者是無法讀取消息的。 也就是說,在再均衡發生期間的這一小段時間內,消費組會變得不可用 。另外,當 一個分區被重新分配給另一個消費 者時, 消費者當前的狀態也會丟失。
比如消費者消費完某個分區中的一部分消息時還沒有來得 及提交消費位移就發生了再均衡操作 , 之後這個分區又被分配給了消費組內的另一個消費者, 原來被消費完的那部分消息又被重新消費一遍,這也是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()方法卻可以做到這一點。