一个分区的数据最多只能被一个消费者消费,增加或者减少会触发kafka集群的负载均衡**
consumer采用pull(拉)模式从broker中读取数据
Kafka有两种分配策略,一是roundrobin,一是range
对于每一个topic,RangeAssignor策略会将消费组内所有订阅这个topic的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区
RoundRobinAssignor策略的原理是将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序,然后通过轮询消费者方式逐个将分区分配给每个消费者。
RoundRobinAssignor策略对应的partition.assignment.strategy参数值为:org.apache.kafka.clients.consumer.RoundRobinAssignor
分区的分配要尽可能的均匀;
分区的分配尽可能的与上次分配的保持相同。
1.同一个Consumer Group内新增或减少Consumer
2.Topic分区发生变化
Consumer消费数据时的可靠性是很容易保证的,因为数据在Kafka中是持久化的,故不用担心数据丢失问题。
由于consumer在消费过程中可能会出现断电宕机等故障,consumer恢复后,需要从故障前的位置的继续消费,所以consumer需要实时记录自己消费到了哪个offset,以便故障恢复后继续消费。
所以offset的维护是Consumer消费数据是必须考虑的问题
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
手动提交offset的方法有两种:分别是commitSync(同步提交)和commitAsync(异步提交)。两者的相同点是,都会将本次poll的一批数据最高的偏移量提交;不同点是,commitSync会失败重试,一直到提交成功(如果由于不可恢复原因导致,也会提交失败);而commitAsync则没有失败重试机制,故有可能提交失败
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.199.100:9092");
props.put("group.id", "test");//消费者组,只要group.id相同,就属于同一个消费者组
props.put("enable.auto.commit", "false");//自动提交offset
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("mytopic"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
consumer.commitSync();
我们把上面的代码改一下,我们拿到records 先不做处理,先commit offset,那么commit之后发生异常,你这个数据其实还没处理,但是kafka认为你已经消费了
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
consumer.commitSync();
//比如在这加一个异常
throw new Exception()
for (ConsumerRecord<String, String> record : records) {
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
如果consumer消费到了5,挂了,offset只提交到了3,
下次启动记录的3开始消费,但是其实4和5已经消费过了
这就导致重复消费
为了使我们能够专注于自己的业务逻辑,Kafka提供了自动提交offset的功能
在构建consumer时候加上自动提交offset的配置
props.put("enable.auto.commit", "true");:是否开启自动提交offset功能
props.put("auto.commit.interval.ms", "1000");:自动提交offset的时间间隔
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.199.100:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("mytopic"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
做多一次消费语义是kafka消费者的默认实现。配置这种消费者最简单的方式是
由于上面的配置,就可以使得kafka有线程负责按照指定间隔提交offset。
但是其实这种方式会使得kafka消费者有两种消费语义:
最多一次语义->at-most-once
消费者的offset已经提交,但是消息还在处理,这个时候挂了,再重启的时候会从上次提交的offset处消费,导致上次在处理的消息部分丢失。
最少一次消费语义->at-least-once
消费者已经处理完了,但是offset还没提交,那么这个时候消费者挂了,就会导致消费者重复消费消息处理。但是由于auto.commit.interval.ms设置为一个较低的时间范围,会降低这种情况出现的概率。
实现最少一次消费语义的消费者也很简单。
这种方式就是要手动在处理完该次poll得到消息之后,调用offset异步提交函数consumer.commitSync()。建议是消费者内部实现密等,来避免消费者重复处理消息进而得到重复结果。最多一次发生的场景是消费者的消息处理完已经输出到结果库,但是offset还没提交,这个时候消费者挂掉了,再重启的时候会重新消费并处理消息。
使用subscribe实现Exactly-once 很简单,具体思路如下:
使用assign实现Exactly-once 也很简单,具体思路如下: