1.消费者提交偏移量导致的问题
- 当我们调用 poll 方法的时候,broker 返回的是生产者写入 Kafka 但是还没有被消费者读取过的记录,消费者可以使用 Kafka 来追踪消息在分区里的位 置,我们称之为偏移量。消费者更新自己读取到哪个消息的操作,我们称之为提交。
- 消费者是如何提交偏移量的呢?消费者会往一个叫做_consumer_offset 的特殊主题发送一个消息,里面会包括每个分区的偏移量。发生了再均衡之后, 消费者可能会被分配新的分区,为了能够继续工作,消费者者需要读取每个分区最后一次提交的偏移量,然后从指定的地方,继续做处理。
1)如果提交的偏移量小于消费者实际处理的最后一个消息的偏移量,处于两个偏移量之间的消息会被重复处理
2)如果提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息将会丢失 - 所以, 处理偏移量的方式对客户端会有很大的影响 。KafkaConsumer API 提供了很多种方式来提交偏移量 。
2.提交偏移量的方式
2.1 自动提交
- 最简单的提交方式是让消费者自动提交偏移量。 如果 enable.auto.comnit 被设为 true,消费者会自动把从 poll()方法接收到的最大偏移量提交上去。 提交时间间隔由 auto.commit.interval.ms 控制,默认值是 5s。自动提交是在轮询里进行的,消费者每次在进行轮询时会检査是否该提交偏移量了,如果是, 那么就会提交从上一次轮询返回的偏移量。
- 不过,在使用这种简便的方式之前,需要知道它将会带来怎样的结果。
- 假设我们仍然使用默认的 5s 提交时间间隔, 在最近一次提交之后的 3s 发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。 这个时候偏移量已经落后了 3s,所以在这 3s 内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量, 减小可能出现重复消息的 时间窗, 不过这种情况是无法完全避免的 。
- 在使用自动提交时,每次调用轮询方法都会把上一次调用返回的最大偏移量提交上去,它并不知道具体哪些消息已经被处理了,所以在再次调用之前最 好确保所有当前调用返回的消息都已经处理完毕(enable.auto.comnit 被设为 true 时,在调用 close()方法之前也会进行自动提交)。一般情况下不会有什么 问题,不过在处理异常或提前退出轮询时要格外小心。
- 自动提交虽然方便,但是很明显是一种基于时间提交的方式,不过并没有为我们留有余地来避免重复处理消息
2.2 手动同步提交
- 我们通过控制偏移量提交时间来消除丢失消息的可能性,并在发生再均衡时减少重复消息的数量。消费者 API 提供了另一种提交偏移量的方式,开发 者可以在必要的时候提交当前偏移量,而不是基于时间间隔。
- 把 auto.commit. offset 设为 false,自行决定何时提交偏移量。使用 commitsync()提交偏移量最简单也最可靠。这个方法会提交由 poll()方法返回的最 新偏移量,提交成功后马上返回,如果提交失败就抛出异常。
- 注意: commitsync()将会提交由 poll()返回的最新偏移量,所以在处理完所有记录后要确保调用了 commitsync(),否则还是会有丢失消息的风险。如果 发生了再均衡,从最近批消息到发生再均衡之间的所有消息都将被重复处理。
- 只要没有发生不可恢复的错误,commitSync()方法会阻塞,会一直尝试直至提交成功,如果失败,也只能记录异常日志。
- 创建主题simple,"./kafka-topics.sh --zookeeper localhost:2181 --create --topic simple --replication-factor 1 --partitions 8",创建消费者CommitSyncConsumer
package org.example.commit;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.example.config.BusiConst;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
/**
*手动提交偏移量,生产者使用同步发送
**/
public class CommitSyncConsumer {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.42.111:9092");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "CommitSync");
//取消自动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
try {
consumer.subscribe(Collections.singletonList(BusiConst.CONSUMER_COMMIT_TOPIC));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, String> record : records) {
System.out.println("主题:" + record.topic() + ";分区:" + record.partition() +
";偏移量:" + record.offset() +";key:" + record.key() + ";value:" + record.value());
//do your work
}
//同步提交(这个方法会阻塞)
consumer.commitSync();
}
} finally {
consumer.close();
}
}
}
2.3 异步提交
- 手动提交时,在 broker 对提交请求作出回应之前,应用程序会一直阻塞。这时我们可以使用异步提交 API,我们只管发送提交请求,无需等待 broker 的响应。
- 在成功提交或碰到无法恢复的错误之前, commitsync()会一直重试,但是 commitAsync 不会。它之所以不进行重试,是因为在它收到服务器响应的时候, 可能有一个更大的偏移量已经提交成功。
- 假设我们发出一个请求用于提交偏移量 2000,这个时候发生了短暂的通信问题,服务器收不到请求,自然也不会作出任何响应。与此同时,我们处理了另 外一批消息,并成功提交了偏移量 3000。如果 commitAsync()重新尝试提交偏移量 2000,它有可能在偏移量 3000 之后提交成功。这个时候如果发生再均衡, 就会出现重复消息。
- commitAsync()也支持回调,在 broker 作出响应时会执行回调。回调经常被用于记录提交错误或生成度量指标。
- 创建消费者CommitAsyncConsumer
package org.example.commit;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.example.config.BusiConst;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;
/**
*异步手动提交偏移量,生产者使用同步发送
**/
public class CommitAsyncConsumer {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.42.111:9092");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "CommitAsync");
//取消自动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
try {
consumer.subscribe(Collections.singletonList(BusiConst.CONSUMER_COMMIT_TOPIC));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, String> record : records) {
System.out.println("主题:" + record.topic() + ";分区:" + record.partition() +
";偏移量:" + record.offset() +";key:" + record.key() + ";value:" + record.value());
//do your work
}
//异步提交
consumer.commitAsync();
//允许执行回调,对提交失败的消息进行处理
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
if (e != null) {
System.out.println("Commit failed for offsets");
System.out.println(map);
e.printStackTrace();
}
}
});
}
} finally {
consumer.close();
}
}
}
2.4 同步和异步组合
- 因为同步提交一定会成功、异步可能会失败,所以一般的场景是同步和异步一起来做。
- 一般情况下,针对偶尔出现的提交失败,不进行重试不会有太大问题,因为如果提交失败是因为临时问题导致的,那么后续的提交总会有成功的。但如果这是发生在关闭消费者或再均衡前的最后一次提交,就要确保能够提交成功。
- 因此,在消费者关闭前一般会组合使用 commitAsync()和 commitSync()。
- 创建消费者SyncAndAsync
package org.example.commit;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.example.config.BusiConst;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
/**
*同步和异步组合
**/
public class SyncAndAsync {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.42.111:9092");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "SyncAndAsync");
//取消自动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
try {
consumer.subscribe(Collections.singletonList(BusiConst.CONSUMER_COMMIT_TOPIC));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, String> record : records) {
System.out.println("主题:" + record.topic() + ";分区:" + record.partition() +
";偏移量:" + record.offset() +";key:" + record.key() + ";value:" + record.value());
//do your work
}
//异步提交
consumer.commitAsync();
}
} catch (Exception e) {
System.out.println("Commit failed");
e.printStackTrace();
} finally {
try {
//同步提交
consumer.commitSync();
} finally {
consumer.close();
}
}
}
}
2.5 特定提交
- 在我们前面的提交中,提交偏移量的频率与处理消息批次的频率是一样的。但如果想要更频繁地提交该怎么办?
- 如果 poll()方法返回一大批数据,为了避免因再均衡引起的重复处理整批消息,想要在批次中间提交偏移量该怎么办?这种情况无法通过调用 commitSync()或 commitAsync()来实现,因为它们只会提交最后一个偏移量,而此时该批次里的消息还没有处理完。
- 消费者 API 允许在调用 commitsync()和 commitAsync()方法时传进去希望提交的分区和偏移量的 map。
- 创建消费者CommitSpecial
package org.example.commit;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.example.config.BusiConst;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
*特定提交
**/
public class CommitSpecial {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.42.111:9092");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "CommitSpecial");
//取消自动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
Map<TopicPartition, OffsetAndMetadata> currOffsets = new HashMap<>();
int count = 0;
try {
consumer.subscribe(Collections.singletonList(BusiConst.CONSUMER_COMMIT_TOPIC));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, String> record : records) {
System.out.println("主题:" + record.topic() + ";分区:" + record.partition() +
";偏移量:" + record.offset() +";key:" + record.key() + ";value:" + record.value());
//do your work
currOffsets.put(new TopicPartition(record.topic(), record.partition()),
new OffsetAndMetadata(record.offset() + 1, "no meta"));
if (count % 11 == 0) {
consumer.commitAsync(currOffsets,null);
}
count++;
}
}
} finally {
consumer.commitSync();
consumer.close();
}
}
}
2.6 创建生产者ProducerCommit
package org.example.commit;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import org.example.config.BusiConst;
import java.util.Properties;
public class ProducerCommit {
public static void main(String[] args) {
//生产者必须指定3个属性(broker地址清单,key和value的序列化器)
Properties properties = new Properties();
properties.put("bootstrap.servers", "192.168.42.111:9092");
properties.put("key.serializer", StringSerializer.class);
properties.put("value.serializer", StringSerializer.class);
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
try {
ProducerRecord<String, String> record;
for (int i = 0; i < 10; i++) {
record = new ProducerRecord<String, String>(BusiConst.CONSUMER_COMMIT_TOPIC,
String.valueOf(i), "Fisher" + i);
//发送并忘记
producer.send(record);
System.out.println(i + ", message is sent.");
}
} catch (Exception e) {
e.printStackTrace();
}finally {
producer.close();
}
}
}
2.7 先启动消费者,再启动生产者,生产10条消息
2.8 查看消费者打印