1.消费者组概念
一个消费者属于消费者组。一个群组里的消费者订阅的是同一个主题,每个消费者接收主题的一部分分区消息。一个主题可以被多个消费群组使用,消费者群组之间互不影响。
假设一个Topic有四个分区,有两个消费者组,分配情况如下:
注意:不要让一个consumer group的消费者数量多于订阅主题的分区数,多于的消费者只会被闲置。
2.创建消费者
消费者正常的消费逻辑,如下:
- 配置消费者客户端参数
- 创建消费者实例
- 订阅主题
- 拉取并消费消息
- 位移提交
- 关闭消费者
2.1一个简单的消费者
public static void main(String[] args) {
//参数设置
Properties properties = new Properties();
properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.put("bootstrap.servers", "127.0.0.1:9092");
properties.put("group.id", "test kafka");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
//主题订阅
consumer.subscribe(Collections.singletonList(topic));
//消费消息
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record.value());
}
}
}
消费者的参数设置也不止上述几个,详细的内容可以参考中文官网。
2.2订阅主题
- 一个消费者可以订阅一个或多个主题。
订阅主题通过调用subscribe()方法即可:
nsumer.subscribe(Collections.singletonList("test.1"));
另外,可是使用正则表达式,订阅多个主题
consumer.subscribe("test.*");
- 订阅主题的特定分区,kafkaConsumer提供了assign()方法实现这些功能
2.3消费消息
消费消息分长轮询、pull模式、push模式三种。
(1)Push
Push方式是Server端接收到消息后,主动把消息推送给Client端,实时性高。对于一个提供队列服务的Server来说,用Push方式主动推送消息有很多弊端:
a.首先是加大Server端的工作量,进而影响Server的性能;
b.其次,Client的处理能力各不相同,Client的状态不收Server控制,如果Client不能及时处理Server推送过来的消息,会造成各种潜在问题;
(2)Pull
Pull方式是Client端循环地从Server端拉取消息,主动权在Client手里,自己拉取到一定量的消息后,处理稳妥了再接着拉取。Pull方式的主要问题是:
a.循环拉取消息的间隔不好设定,间隔太短就处于一个"忙等"的状态,浪费资源;
b.每个Pull的时间间隔太长,Server端有消息到来时,有可能没有被技术处理。
(3)长轮询
a: push + pull 模式结合的方式,通过Client和Server端的配合,达到既有Pull的有点,又能达到保证实时性的目的。长轮询的核心是,Broker端HOLD住客户端过来的请求一小段时间,在这个时间内,有新消息到达,就利用现有的连接立刻返回消息给Consumer。长轮询的主动权还是掌握在Consumer手中,Broker即使有大量的消息积压,也不会主动推送给Consumer。
b: 长轮询的局限性是,HOLD住Consumer请求的时候需要占用资源,它适合用在消息队列这种客户端连接数可控的场景中。
2.4反序列化
和序列化相对于的是反序列化,kafka服务器的消息只有经过反序列化才能获取对应的实体。kafka反序列化方式表:
(1)如果自定义了序列化,自然是需要自定义对应反序列话工具;
(2)使用了第三方的序列化工具,反之,也是需要第三方对应的反序列化工具;
2.5位移提交
(1)自动提交
kafka默认的提交方式,客户端参数enable.auto.commit配置默认值true.。默认5s内,查询一次是需要提交。自动提交很简单,但是会存在重复消费和消息丢失的坑你。在5s内,有消费者离开,进行再平衡,就能造成重复消费的问题。
(2)手动提交
- 同步提交
commitSync(),失败的时候回一直重试提交,直到成功
- 异步提交
commitAysc()
- 同步和异步组合提交
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("topic = %s, partition = %s, offset = %d,
customer = %s, country = %s\n",
record.topic(), record.partition(),
record.offset(), record.key(), record.value());
}
//异步提交
consumer.commitAsync();
}
} catch (Exception e) {
log.error("Unexpected error", e);
} finally {
try {
//同步提交
consumer.commitSync();
} finally {
consumer.close();
}
}
2.6再均衡监听器
一个消费者组内的 consumer 共同读取 Topic 的分区。
- 当一个 consumer 加入组时,读取的是原本由其他 consumer 读取的分区。
- 当一个 consumer 离开组时(被关闭或发生崩溃),原本由它读取的分区将由组里的其他 consumer 来读取。
- 当 Topic 发生变化时,比如添加了新的分区,会发生分区重分配。
分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为再均衡(rebalance)。再均衡非常重要,为消费者组带来了高可用性和伸缩性,可以放心的增加或移除消费者。
再均衡期间,消费者无法读取消息,造成整个 consumer group 一小段时间的不可用。另外,当分区被重新分配给另一个消费者时,当前的读取状态会丢失。
消费者通过向作为组协调器(GroupCoordinator)的 broker(不同的组可以有不同的协调器)发送心跳来维持和群组以及分区的关系。心跳表明消费者在读取分区里的消息。消费者会在轮询消息或提交偏移量(offset)时发送心跳。如果消费者停止发送心跳的时间足够长,会话就会过期,组协调器认为消费者已经死亡,会触发一次再均衡。
在 Kafka 0.10的版本中,对心跳行为进行了修改,由一个独立的线程负责心跳。
订阅主题相关函数,存在再平衡的监听器:
实现该监听器,需要实现oNPartitionRevoked和oNPartitonsAssiged两个接口即可,代码示例如下,
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.
}
});
2.7多线程
kafkaConsumer线程不安全,但是这并不能说明,我们只能用单线程的方式执行。
方式一:
public static void main(String[] args) {
Properties props = initConfig();
int consumerThreadNum = 4;
for (int i = 0; i < consumerThreadNum; i++) {
new KafkaConsumerThread(props, topic).start();
}
}
public static class KafkaConsumerThread extends Thread {
private KafkaConsumer<String, String> kafkaConsumer;
public KafkaConsumerThread(Properties props, String topic) {
this.kafkaConsumer = new KafkaConsumer<>(props);
this.kafkaConsumer.subscribe(Arrays.asList(topic));
}
@Override
public void run() {
try {
while (true) {
ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
//process record.
System.out.println(record.value());
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
kafkaConsumer.close();
}
}
}
这种方式等于是开启多个Consumer消费者,优点是能保证顺序消费,缺点是每个线程都要维护一个独立的TCP链接,如果线程数比较大,网络开销比较大。
方式二:
当然就是线程池的方式。这种方式缺点是不能保证顺序消费,如果需要顺序消费,实现比较麻烦。