思维导图:
引言
这篇文章的主要内容是介绍Kafka消费者的一般的使用流程以及比较特殊的操作,所以本文共分为以下两个部分:
- 基本流程 : 主要包括创建实例,消息订阅,消息消费,位移提交,消费控制等内容。
- 其他处理 : 主要是一些其他的处理,例如指定位移消费,消费者拦截器等操作。
一.基本流程
Kafka消费者的基本流程一般来说都是必要的。也代表着消费者的基本使用逻辑。
1.1 创建实例
创建一个消费者实例必要的参数设置如下:
public class ConsumerClient {
/**
* Kafka集群地址
*/
private static final String BROKE_LIST = "192.168.42.128:9092,192.168.42.128:9093,192.168.42.128:9094";
/**
* 主题名称
*/
private static final String TOPIC_NAME = "topic-testTopic";
/**
* 组ID
*/
private static final String GROUP_ID = "group.demo";
/**
* 消费者对象
*/
private KafkaConsumer<String,String> consumer;
/**
* 构造方法,初始化消费者对象实例
*
* @author : zhouhao
* @date : 2019/5/29 7:47
*/
public ConsumerClient(){
Properties properties = new Properties();
//服务器集群地址
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,BROKE_LIST);
//Key 的 反序列器
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
//Value 的反序列化器
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
// 消费者所属消费组
properties.put(ConsumerConfig.GROUP_ID_CONFIG,GROUP_ID);
consumer = new KafkaConsumer<>(properties);
}
}
可以看到,与生产者不同的时,消费者创建时的必要参数多了一个消费组的概念,所以,我们需要了解到底什么是消费组?
1.1.1 消费组
消费组是在消费者之上的一层概念,他的主要作用是使得同一个消费组的的消费者可以均匀的分配所订阅主题的分区。如下图:消费者A,B都订阅了某主题,但是A,B中的消费者个数不同,所以A,B消费组内对于此主题的分区的分配情况也不同
按照如上所述的分区分配情况分析,同一消费组内的消费者个数超过主题分区数时,会出现消费者被闲置的情况。
对于消息中间件而言,一般来说有两种消息投递模式:点对点模式和发布/订阅模式。我们可以通过消费组的概念来实现这两种模式:
- 点对点模式:所有的消费者都隶属于同一个消费者,那么,此主题内的每一条消息只会被一个消费者处理。
- 发布/订阅模式:每个消费者内都只有一个消费者,那么,此主题的消息会被派送给所有订阅的消费者进行处理。
1.1.2 反序列化器
消费者的反序列化器和生产者的序列化器相似,只需要实现Derializer接口并实现方法即可。同时,如果有序列化和反序列化自定义类的需求时,不推荐自己写新的序列化器和反序列化器。因为耦合度太高而且必须使得生产者和消费者的序列化器和反序列化器同步,极易出错。使用JSON等其他的序列化工具是个不错的选择。
1.2 消息订阅
1.2.1 主题订阅
主题的订阅有两种方式:列表订阅和正则表达式订阅,值得注意的时,订阅都是全量订阅而不是增量订阅,也就是说,如果当前已订阅了某些主题,如果想要多订阅一些主题的话,必须将以前的主题和需要增加订阅的主题一起放入主题列表中,不然,消费者将不会订阅之前已订阅的主题。
/**
* 主题订阅,主题列表订阅及正则表达式订阅
*
* @param list 主题列表数组或正则表达式
* @author : zhouhao
* @date : 2019/5/29 8:26
*/
public void subscribeTopic(String... list){
//主题列表订阅
consumer.subscribe(Arrays.asList(list));
//主题正则表达式订阅,此时,列表订阅将被覆盖掉
consumer.subscribe(Pattern.compile(list[0]));
//取消订阅
consumer.unsubscribe();
}
可以看到,主题订阅的方法是有重载的,重载后添加了一个ConsumerRebalanceListener,这个类叫做再均衡监听器,其作用如下:当消息组中增加或减少了消费者时,此时消费者所对应的分区就会进行再次分配,这就叫做再均衡。而在再均衡发生的这段时间里,消费组是不可用的。此时就可能会发生消息丢失或重复消费 。再均衡监听器可以在再均衡开始之前和重新分配分区之后进行特殊的处理,以避免上述情况的发生。
1.2.2 分区订阅
/**
* 分区订阅
*
* @param list 分区对象集合
* @author : zhouhao
* @date : 2019/5/30 6:49
*/
public void subscribePartition(List<TopicPartition> list){
//分区订阅
consumer.assign(list);
//取消订阅
consumer.unsubscribe();
}
1.3 消息消费
Kafka的消息消费是基于拉模型的。即消费者会不停轮询以获取消息。Kafka使用poll获取消息,poll时可以添加阻塞时间timeout以控制拉取的时间
/**
* 拉取消息,Duration是新的时间段表示对象
*
* @throws
* @author : zhouhao
* @date : 2019/5/30 7:01
*/
public void getMessage(){
ConsumerRecords<String,String> records = consumer.poll(Duration.ofMillis(1000));
}
1.4 位移提交
1.4.1 位移基本概念
对于Kafka的分区中的消息而言,每一条消息都有自己的位置,我们把这个位置称为偏移量offset。对于消费者而言,消费者中也会保存一个offset以记录消息消费的位置。我们将消费者消息消费的位置提交到kafka服务器的这一过程称之为位移提交。
首先,我们介绍三个位置的概念:
- lastConsumOffset:当前所消费的消息的偏移量
- committed offset :提交的需要保存的消息偏移量,特别的是,若当前的lastConsumOffset = 1,那么committed offset = 2.
- position :下一次拉取开始的偏移量
1.4.2 自动提交
在默认情况下,消费者是每隔一段时间自动提交的。提交间隔有参数 auto.commit.intercal.ms控制。默认为5秒。自动提交的位移是我们当前所拉取的批次的offset + 1.如下图:当前拉取批次x+2 到x+7.
但是,自动提交可能会产生消息丢失或重复消费
- 消息丢失:例如,当前处理到x+5时,出现异常,此时当前批次已提交,重新拉取后,x+6 到x+7丢失
- 重复消费:例如,当前处理到x+5时,出现异常,此时当前批次未提交,重新拉取后,x+2到x+5会重复消费
1.4.3 手动提交
如需手动提交,需要设置自动提交参数为false。
//手动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
手动提价分为同步提交和异步提交,以下代码用同步提交演示。
- 根据拉取批次提交
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(1000);
for (ConsumerRecord<String, String> record : records) {
//do some logical processing.
}
consumer.commitSync();
}
} finally {
consumer.close();
}
- 消费每条消息后都提交
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(1000);
for (ConsumerRecord<String, String> record : records) {
//do some logical processing.
long offset = record.offset();
TopicPartition partition =
new TopicPartition(record.topic(), record.partition());
consumer.commitSync(Collections
.singletonMap(partition, new OffsetAndMetadata(offset + 1)));
}
}
} finally {
consumer.close();
}
- 根据批次分区提交
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(1000);
for (TopicPartition partition : records.partitions()) {
List<ConsumerRecord<String, String>> partitionRecords =
records.records(partition);
for (ConsumerRecord<String, String> record : partitionRecords) {
//do some logical processing.
}
long lastConsumedOffset = partitionRecords
.get(partitionRecords.size() - 1).offset();
consumer.commitSync(Collections.singletonMap(partition,
new OffsetAndMetadata(lastConsumedOffset + 1)));
}
}
} finally {
consumer.close();
}
1.5 消费控制
/**
* 消费者控制
*
* @param list 分区列表
* @author : zhouhao
* @date : 2019/5/30 7:59
*/
public void consumerControl(List<TopicPartition> list){
//暂停分区拉取
consumer.pause(list);
//恢复分区拉取
consumer.resume(list);
//关闭消费者
consumer.close();
}
二.其他处理
2.1 指定消费位移
在消费者找不到消费位移时,会根据auto.offset.reset参数决定把分区首部还是末尾设置为消费位移。默认设置为末尾,即即将要写入的最新消息的offset。若auto.offset.reset=none,当找不到消费位移时会直接报错。
我们也可以把特定的分区位置设置为消费位移,需要使用到seek()方法。我们将消费者的消费位移设置为10
long start = System.currentTimeMillis();
Set<TopicPartition> assignment = new HashSet<>();
//直到获取分区后才设置分区的消费位移
while (assignment.size() == 0) {
consumer.poll(Duration.ofMillis(100));
assignment = consumer.assignment();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
System.out.println(assignment);
for (TopicPartition tp : assignment) {
consumer.seek(tp, 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.offset() + ":" + record.value());
}
}
2.2 消费者拦截器
消费者拦截器和生产者拦截器原理相同,实现ConsumerInterceptor接口并实现方法,再设置拦截器参数即可。