3.1 消费者与消费者组
消费者(Consumer)负责订阅Kafka中的主题(Topic),并且从订阅的主题上拉取(pull)消息。
在Kafka的消费理念里面还有一层消费者组(Consumer Group)的概念。
每个消费者对应一个消费者组,当消息发布到主题后,只会被投递给订阅他的每个消费者组中的一个消费者。
一个主题可以被多个消费者组消费,多个消费者组之间互不影响。
消费者的分区分配策略 可以通过消费者客户端参数partition.assignment.strategy 来设置。
有关消费者分区分配的细节可以参考7.1节。
消费者组是逻辑上的概念。每个消费组都会有固定的名称,消费者在消费前需要指定所属消费组的名称,
可以通过消费者客户端参数 group.id 来配置,默认值为空宇符串。
消费者不是逻辑上的概念,它是实际的应用实例,它可以是一个线程,也可以是一个进程。
同一个消费组内的消费者既可以部署在同一台机器上,也可以部署在不同的机器上
3.2 客户端开发
一个正常的消费逻辑需要具备以下几个步骤:
(1)配置消费者客户端参数 及 创建消费者实例
(2)订阅主题
(3)拉取消息并消费
(4)提交消费位移
(5)关闭消费者实例
消费者客户端代码示例
public class KafkaConsumerAnalysis {
//配置之消费者客户端参数
public static final String brokerList = "localhost:9092";
public static final String topic = "topic-demo";
public static final String groupid = "group.demo";
public static final AtomicBoolean isRunning =new AtomicBoolean(true);
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.GROUP_ID_CONFIG,groupid);
props . put(ConsumerConfig.CLIENT_ID_CONFIG,"client.id.demo");
return props;
}
public static void main (String [] args) {
//获取配置
Properties props =InitConfig()
//创建消费者
KafkaConsumer<String, String> consumer= new KafkaConsumer<>(props) ;
//订阅主题
consumer.subscribe(Arrays.asList(topic));
try {
while(isRunning.get()){
//拉取消息
ConsumerRecords<String,String> records = consumer.poll(Duration.ofMillis (1000));
for(ConsumerRecord<String, String> record : records) {
System.out.println("topic = "+ record.topic() + " , partition =" + record. partition()
+ ", offset =" + record . offset());
System.out.println(key = " + record.key()+ ", value =" + record.value ());
//实际开发中 根据需求 做对应逻辑操作 比如 消费消息 提交位移等等
}
}
}catch (Exception e) {
log.error ("occur exception ", e);
}finally {
//关闭消费者
consumer.close();
}
}
}
3.2.1 必备的参数配置
在创建真正的消费者实例之前需要做相应的参数配置,
KafkaConsumer 中的参数众多。每个参数在ConsumerConfig 类中都有对应的名称,
KafkaConsumer 中有4个参数是必填
① bootstrap.servers:
指定连接 Kafka 集群所需的 broker 地址清单,具体内容形式为host1:port1,host2:post2
可以设置一个或多个地址,并非需要设置集群中全部的 broker 地址,消费者会从现有的配置中查找到全部的Kafka集群成员。
推荐设置两个以上 broker 地址信息,当其中一个宕机时,消费者仍然可以连接到 Kafka 集群上。
②group.id:
消费者隶属的消费组的名称 默认值为" "。 一般而言,这个参数需要设置成具有一定的业务意义的名称。
③key.deserializer 与 value.deserializer
与生产者客户端KafkaProducer中的 key.serializer 与 value serializer 参数对应。
分别用来指定消息中key和value 需反序列化操作的反序列化器
3.2.2 订阅主题与分区
(1)订阅主题
一个消费者可以订阅一个或多个主题。
subscribe()方法主订阅主题,既可以以集合的形式订阅多个个主题,也可以以正则表达式的形式订阅特定模式的主题。
subscribe重载方法如下:
public void subscribe(Collection<String> topics ConsumerRebalanceListener listener)
public void subscribe(Collection<String> topics)
public void subscribe(Parttern pattern, ConsumerRebalanceListener listener)
public void subscribe(Parttern pattern)
①基于集合
使用集合的方式(subscribe(Collection)来订阅,如果前后订阅了不同的主题,那么消费者以最后一次为准。
②基于正则
使用正则表达式的方式(subscribe(Pattern))订阅, 如果创建了新的主题,且主题名字匹配正则表达式,
那这个消费者就可以消费到新添加的主题中的消息。
在Kafka和其他系统之间进行数据复制时,正则表达式的方式就很好。
(2)订阅分区
消费者不仅可以通过KafkaConsumer subscribe() 方法订阅主题,还可以直接订阅某些主题定分区 ,
在KafkaConsumer中还提供了assign()方法来实现这些功能,定义如下:
public void assign(Collection<TopicPartition> partitions)
这个方法只接受一个参数 partition ,用来指定订阅的分区集合。
演示只订阅 topic-demo 主题中分区编号为0的分区,相关代码如下
consumer.assign(Arrays.asList(new TopicPartition("topic-demo", 0)));
如果事先不知道主题中有多少个分区,可以使用 KafkaConsumer partitionsFor()可以查询指定主题的元数据信息,
public List <PartitionInfo> partitionsFor(Sting, topic)
通过partitionFor(),配合assign() 可以实现订阅主题(全部分区)的功能,示例参考如下:
List<TopicPartition> partitions = new ArrayList<>();
List<Partitioninfo> partitioninfos = consumer.partitionsFor(topic);
//添加主题的所有分区
if (partitioninfos != null) {
for (Partitioninfo tpinfo : partitioninfos) {
partitions.add(new TopicPartition(tpinfo.topic(), tpinfo.partition()));
}
}
//订阅主题
consumer.assign(partitions) ;
(3)取消订阅
①使用 KafkaConsumer 中的 unsubscribe()方法采取消主题的订阅。
②将 subscribe(Collection) assign(Collection) 集合参数设置为空集合。
以下 三行代码的效果相同
consumer.unsubscribe();
consumer.subscribe(new ArrayList<String>()) ;
consumer.assign(new ArrayList<TopicPartition>());
3.2.3 反序列化
Kafka 所提供的反序列器 以及对应作用的数据类型如下
ByteBufferDeserializer -> ByteBuffer
ByteArrayDeserializer -> ByteArray
BytesDeserializer -> Bytes
DoubleDeserializer -> Double
FloatDeserializer -> Float
IntegerDeserializer -> Integer
LongDeserializer -> Long
ShortDDeserializer -> Short
StringDeserializer -> String
无特殊需要,笔不建议使用自定义的序列化器或反序列化器。
因为这样会增加生产者与消费者之间的耦合度,在系统升级换代的时候很容易出错。
还要面对的问题就是 KafkaProducer 和 KafkaConsumer 之间的序列化和反序列化的兼容性
3.2.4 消息消费
Kafka 中的消费是基于拉模式(poll)的。
K消息消费是一个不断轮询的过程,消费者所要做的就是重复地调用poll()
poll()方法返回的是所订阅的主题(分区)上的一组消息。
poll() 方法具体定义如下:
public ConsumerRecords<K, V> poll(final Duration timeout)
poll超时参数timeout ,用来控制 poll()的阻塞时间,在消费者的缓冲区里没有可用数据时会发生阻塞
timeout 的设置取决于应用程序对响应速度的要求。
如果应用线程唯一的工作就是从 Kafka 中拉取并消费消息,则可以将这个参数设置为最大值 Long.MAX_VALUE
消费者消费到的每条消息的类型为 ConsumerRecord(注意与 ConsumerRecords区别),
这个和生产者发送的消息类型 ProducerRecord 相对应,不过 ConsumerRecord 中的内容更加丰富,
具体的结构参考如下代码
public class ConsumerRecord<K, V> {
private final String topic;
private final int partition;
pr vate final long offset;
private final long timestamp;
private final TimestampType timestampType;
private final int serial zedKeySize;
private final int serializedValueSize;
private final Headers headers;
private final K key ;
private final V value ;
private volatile Lo 口 g checksum;
//省略若干方法
}
到目前为止,可以简单地认为 poll()方法只是拉取 下消息而己,但就其内部逻辑而 并不简单
它涉及消费位移、消费者协调器、组协调器、消费者的选举、分区分配的分发、再均衡的逻辑、心跳等内容
3.2.5 位移提交
在旧消费者客户端,消费位移是存储在Zookeeper中。
新消费者客户端中,消费位移存储在Kafka的__consumer_offsets中。
把消费位移存储起来的动作称为"提交",消费者在消费完消息后需要执行消费位移的提交。
(1)位移提交引起的 重复消费 和 数据丢失问题
①数据丢失
消费者拉取消息后,还在消费, 进行位移提交。
此时消费者异常宕机,数据没有完全消费,故障恢复后,会从已提交的位移之后的数据开始消费
那么宕机前 提交的位移中 的 没有消费的数据 就丢失了。
②重复消费
消费者拉取消息后,等待消费者消费完, 才进行位移提交。
如果拉取数据后,消费者消费部分数据,然后宕机了,此时消费者还没有提交位移,故障恢复后,
因为没有提交上次的消费位移,消费者会重新消费上一次的消息。 那么 宕机前消费的数据 就会被重复消费。
(2)手动提交位移
Kafka 中默认的消费位移的是自动提交,这个由消费者客户端参数enable.auto.commit 配置,默认true;
自动位移提交的方式在正常情况下不会发生消息丢失或重复消费的现象,
但是在编程的世界里异常无可避免,与此同时,自动位移提交也无法做到精确的位移管理。
手动的提交方式可以让开发人员根据程序的逻辑在合适的地方进行位移提交。
开启手动提交功能的前提是消费者客户端参数 enable.auto.commit 配置为 false。
①手动提交——同步提交 commitSync()
这个方法很简单,下面使用它演示同步提交的简单用法:
while (isRunning .get()){
ConsumerRecords<String , String> records= consumer.poll(1000);
for (ConsumerRecord<String, String> record : records) {
//do some logical processing
}
consumer.commitSync();
}
针对上面的示例还可以修改为批量处理+批量提方式 关键代码如下:
final int minBatchSize = 200;
List<ConsumerRecord> buffer= new ArrayList<>() ;
while(isRunning.get()){
ConsumerRecords<Str ng Str ng> records = co sumer poll(lOOO)
for (ConsumerRecord<String , String> record : records) {
buffer add(record);
}
if{buffer. size() >= minBatchSize){
//do some logical processing with buffer .
consumer.commitSync()
buffer.clear()
}
}
②手动提交——异步提交commitAsync()
异步提交的方commitAsync() 在执行的时候消费者线程不会被阻塞
可能在提交消费位移的结果还未返回之前就开始了新一次的拉取操 。异步提交可以提升使消费者的性能。
while(isRunning.get()){
ConsumerRecords<String , String> records= consumer.poll(1OOO) ;
for(ConsumerRecord<String , String> record : records) {
//do some log cal processing
}
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition OffsetAndMetadata> offsets,
Exception exception ) {
if (exception == null) {
System . out . println(offsets) ;
}else {
log.error ("fail to commit offsets {} ", offsets , exception) ;
}
}
});
}
3.2.6 控制或者关闭消费
KafkaConsumer中使用 pause() resume() 方法来分别实现暂停 和 恢复 分区向客户端返回数据的操作。
KafkaConsumer提供close()方法来实现关闭。
3.2.7 指定位移消费
正是有了消费位移的持久化,才使消费者在关闭、崩溃或者在遇到再均衡的时候,
可以让接替的消费者能够根据存储的消费位移继续进行消费。
①auto.offset.reset
每当消费者查找不到所记录的消费位移时, 就会根据消费者客户端参数auto.offset.reset消费
默认"latest":从分区尾部开始消费消息;"earliest":从分区起始处;"none":不消费
②seek()
有些时候,我们需要一种更细粒度的掌控,可以让我们从特定的位移处开始拉取消息,
而 KafkaConsumer 中的 seek()方法提供此功能。
public void seek(TopicPartition partition long offset)
参数 partition 表示分区,offset 参数用来指定从分区的哪个位置开始消费。
seek()方法只能重置消费者分配到的分区的消费位置,而分区的分配是在poll()法的调用过程中实现的
也就是说,在执行seek() 方法之前需要先执行一次poll() 方法等到分配到分区之后才可以重置消费位置。
seek提供了从特定位置读取消息的能力,通过这个方法来向前跳过若干消息
也可以通过这个方法来 向后回溯若干消息,为消费提供了很大的灵活性。
3.2.8 再均衡
再均衡是指分区的所属权从一个消费者转移到另一消费者的行为,它为消费组具备高可用性和伸缩性提供保障,
使我们可以既方便又安全地删除消费组内的消费者或往消费组内添加消费者。
不过在再均衡发生期间,消费组内的消费者是无法读取消息的。在再均衡发生期间,消费组会变得不可用。
一般情况下,应尽量避免不必要的再均衡的发生。
3.2.9 消费者拦截器
消费者拦截器主要在消费到消息或在提交消费位移时进行一些定制化的操作。
消费者拦截器需要自定义实现 org.apache.kafka.clients.consumer.ConsumerInterceptor