第4章 Kafka消费者——从Kafka读取数据

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010819416/article/details/84205882

4.1 KafkaConsumer概念

4.1.1 消费者和消费者群组
Kafka消费者从属于消费者群组。一个群组里的消费者订阅的是同一个主题,每个消费者接受主题一部分分区的消息。

4.1.2 消费者群组和分区再平衡
分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为再均衡。

消费者通过向被指派为群组协调器的broker发送心跳来维持它们和群组的从属关系以及它们对分区的所有权关系。

4.2 创建Kafka消费者

创建KafkaConsumer对象:

 Properties props = new Properties();
 props.put("bootstrap.servers", "localhost:9092");
 props.put("group.id", "test");
 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);

4.3 订阅主题
subscribe()方法接受一个主题列表作为参数,使用起来很简单:

consumer.subscribe(Collections.singletonList("my-topic"));
 //创建一个只包含单个元素的列表

调用subscribe()方法时传入一个正则表达式。

//调用subscribe()方法时传入一个正则表达式
consumer.subscribe(Pattern.compile("test.*"));

4.4 轮询
消息轮询是消费者API的核心,通过一个简单的轮询向服务器请求数据。一旦消费者订阅了主题,轮询就会处理所有的细节,包括群组协调,分区再均衡,发送心跳,获取数据。

// 轮询
try {
    while (true) {//无限循环
        //poll()方法的参数是一个超时时间,用于控制poll()方法的阻塞时间
        //poll()方法返回一个记录列表
        ConsumerRecords<String, String> records = consumer.poll(100);
        for(ConsumerRecord<String,String> record : records) {
            System.out.println("topic = " + record.topic() + ", partition = " + record.partition() +
            ", offset = " + record.offset() + ", customer = " + record.key() + ", country = " + record.value());
        }
    }
} finally {
    consumer.close();
    //关闭消费者
}

4.5 消费者的配置
1 fetch.min.bytes
指定了消费者从服务器获取记录的最小字节数。

2 fetch.max.wait.ms
用于指定broker的等待时间,如果没有足够的数据流入Kafka,消费者获取最小数据量的要求就得不到满足,最终导致500ms的延迟。

3 max.partition.fetch.bytes
指定服务器从每个分区里返回给消费者的最大字节数。

4 session.timeout.ms
指定了消费者在被认为死亡之前可以与服务器断开连接的时间,默认是3s。

5 auto.offset.reset
指定了消费者在读取一个偏移量的分区或者偏移量无效的情况下该作何处理。
默认latest,从最新的记录开始读取数据;earliest,从起始位置读取分区的记录。

6 enable.auto.commit
该属性指定了消费者是否自动提交偏移量,默认值是true。

7 partition.assignment.strategy
分区会被分配给群组里的消费者。

两个默认的分配策略:
Range
该策略会把主题的若干个连续的分区分配给消费者。
RoundRobin
该策略把主题的所有分区逐个分配给消费者。

8 client.id
任意字符串,用它来标识从客户端发送过来的消息

9 max.poll.records
用于控制单次调用call()方法能返回的记录数量

10 receive.buffer.bytes和send.buffer.bytes
socket在读写数据时用到的TCP缓存区也可以设置大小。

4.6 提交和偏移量
每次调用poll()方法,它总是返回由生产者写入Kafka但还没有被消费者读取过的记录。
消费者可以使用Kafka来追踪消息在分区里的位置(偏移量)。
把更新分区当前位置的操作叫作提交。
消费者往一个叫作_consumer_offset的特殊主题发送消息,消息里包含每个分区的偏移量。

4.6.1 自动提交
让消费者自动提交偏移量。enable.auto.commit设为true。

4.6.2 提交当前偏移量
把auto.commit.offset设为false,让应用程序决定何时提交偏移量。使用commitSync()提交由poll()方法返回的最新偏移量。

while (true) {//无限循环
   //poll()方法的参数是一个超时时间,用于控制poll()方法的阻塞时间
   //poll()方法返回一个记录列表
   ConsumerRecords<String, String> records = consumer.poll(100);
   for(ConsumerRecord<String,String> record : records) {
       System.out.println("topic = " + record.topic() + ", partition = " + record.partition() +
       ", offset = " + record.offset() + ", customer = " + record.key() + ", country = " + record.value());
   }
   try {
       //在轮询更多的消息之前,调用commitSync()方法提交当前批次最新的偏移量
       consumer.commitSync();
   } catch (Exception e) {
       e.printStackTrace();
   }
}

4.6.3 异步提交

 while (true) {//无限循环
     //poll()方法的参数是一个超时时间,用于控制poll()方法的阻塞时间
     //poll()方法返回一个记录列表
     ConsumerRecords<String, String> records = consumer.poll(100);
     for(ConsumerRecord<String,String> record : records) {
         System.out.println("topic = " + record.topic() + ", partition = " + record.partition() +
         ", offset = " + record.offset() + ", customer = " + record.key() + ", country = " + record.value());
     }
     //异步提交最后一个偏移量,然后继续做其他事情
     consumer.commitAsync();
     //支持回调
     consumer.commitAsync(new OffsetCommitCallback() {
         @Override
         public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
             if(e != null) {
                 e.printStackTrace();
             }
         }
     });
 }

4.6.4 同步和异步组合提交

try {
    while (true) {//无限循环
        //poll()方法的参数是一个超时时间,用于控制poll()方法的阻塞时间
        //poll()方法返回一个记录列表
        ConsumerRecords<String, String> records = consumer.poll(100);
        for(ConsumerRecord<String,String> record : records) {
            System.out.println("topic = " + record.topic() + ", partition = " + record.partition() +
            ", offset = " + record.offset() + ", customer = " + record.key() + ", country = " + record.value());
        }
        //异步提交最后一个偏移量,然后继续做其他事情
        consumer.commitAsync();
    }
} finally{
    try{
        consumer.commitSync();
    } finally {
        consumer.close();
    }
}

4.6.5 提交特定的偏移量
跟踪所有分区的偏移量,使用commitSync()方法提交。
示例:

//提交特定偏移量开始
HashMap<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
int count = 0;
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(100);
    for(ConsumerRecord<String,String> record : records) {
        System.out.println(record.topic() + record.partition() + record.offset() + record.key()
        + record.value());
        currentOffsets.put(
                new TopicPartition(record.topic(),record.partition()),
                new OffsetAndMetadata(record.offset(), "no metadata"));
        if(count % 1000 == 0) {
            consumer.commitAsync(currentOffsets,null);
        }
        count++;
    }
}
//提交特定偏移量结束

4.7 再均衡监听器
在为消费者分配新分区或移除旧分区时,可以通过消费者API执行一些应用程序代码,在调用subscribe()方法时传进去一个ComsumerRebalanceListener实例就可以了。

private static class HandleRebalance implements ConsumerRebalanceListener {

    /**
     * 在再均衡开始之前和消费者停止读取消息之后被调用
     * @param collection
     */
    @Override
    public void onPartitionsRevoked(Collection<TopicPartition> collection) {
        System.out.println("Lost partition in rebalance.Committing current offsets:" + currentOffsets);
        consumer.commitSync(currentOffsets);
    }

    /**
     * 在重新分配分区之后和消费者开始读取消息之前被调用
     * @param collection
     */
    @Override
    public void onPartitionsAssigned(Collection<TopicPartition> collection) {

    }
}

//再均衡监听器开始
try {
    consumer.subscribe(Collections.singletonList("my-topic"), new HandleRebalance());
    while (true) {
        ConsumerRecords<String, String> records = consumer.poll(100);
        for(ConsumerRecord<String,String> record : records) {
            System.out.println(record.topic() + record.partition() + record.offset() + record.key()
                    + record.value());
            currentOffsets.put(
                    new TopicPartition(record.topic(),record.partition()),
                    new OffsetAndMetadata(record.offset(), "no metadata"));
        }
        consumer.commitAsync(currentOffsets,null);
    }
} catch (Exception e) {

} finally {
    try {
        consumer.commitSync(currentOffsets);
    } finally {
        consumer.close();
        System.out.println("Closed consumer and we are done");
    }
}
//再均衡监听器结束

4.8 从特定偏移量处开始处理记录
使用ConsumerRebalanceListener和seek()方法确保我们是从数据库里保存的偏移量所指定的位置开始处理消息的。

public static class SaveOffsetsOnRebalance implements ConsumerRebalanceListener {

    @Override
    public void onPartitionsRevoked(Collection<TopicPartition> collection) {
        //提交数据库事物,
        commitDBTransaction();
    }

    private void commitDBTransaction() {
    }

    @Override
    public void onPartitionsAssigned(Collection<TopicPartition> collection) {
        for(TopicPartition partition : collection) {
            //从数据库获取偏移量,在分配到新分区的时候,使用seek()方法定位到那些记录
            consumer.seek(partition,getOffsetFromDb(partition));
        }
    }

    private long getOffsetFromDb(TopicPartition partition) {
        return 0;
    }
}

//从特定偏移量处开始处理记录开始
/**
 * 订阅主题之后,开始启动消费者,调用一次poll()方法,让消费者加入到消费者群组里,
 * 并获取分配到的分区,调用seek()方法定位分区的偏移量
 */
consumer.subscribe(Collections.singletonList("my-topic"), new SaveOffsetsOnRebalance());
consumer.poll(0);
for(TopicPartition partition : consumer.assignment()) {
    consumer.seek(partition,getOffSetFromDB(partition));
}
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(100);
    for(ConsumerRecord<String,String> record : records) {
        processRecord(record);
        storeRecordInDB(record);
        storeOffsetInDb(record.topic(),record.partition(),record.offset());
    }
    commitDBTransaction();
}
//从特定偏移量处开始处理记录结束

4.9 如何退出
如果确定要退出循环,需要通过另一个线程调用consumer.wakeup()方法。

4.10 反序列器
如何为对象自定义反序列化器,以及如何使用Avro和Avro反序列化器。

4.11 独立消费者——为什么以及怎样使用没有群组的消费者
一个消费者可以订阅主题(并加入消费者群组),或者为自己分配分区,但不能同时做这两件事情。

如何为自己分配分区,并从分区里读取消息的:

 //独立消费者开始
List<PartitionInfo> partitionInfos = null;
//向集群请求主题可用的分区
partitionInfos = consumer.partitionsFor("topic");
if(partitionInfos != null) {
    Collection<TopicPartition> partitions = null;
    for(PartitionInfo partition : partitionInfos) {
        partitions.add(new TopicPartition(partition.topic(),partition.partition()));
    }
    //知道需要哪些分区之后,调用assign方法
    consumer.assign(partitions);
    while (true) {
        ConsumerRecords<String, String> records = consumer.poll(100);
        for(ConsumerRecord<String,String> record : records) {
            processRecord(record);
        }
        consumer.commitSync();
    }
}
//独立消费者结束

4.12 旧版的消费者API

4.13 总结

猜你喜欢

转载自blog.csdn.net/u010819416/article/details/84205882
今日推荐