kafka Consumer 消费者API操作详解

一、准备工作

  • 在IDE上创建maven项目,pom文件添加依赖
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>1.1.1</version>
</dependency>
  • 启动zookeeper集群
bin/zkServer.sh start
  • 启动kafka集群
bin/kafka-server-start.sh -daemon config/server.properties
  • kafka集群开一个生产者
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic mydata 

二、创建一个普通的消费者(自动提交 offset)

Consumer 消费数据时的可靠性是很容易保证的,因为数据在 Kafka 中是持久化的,故不用担心数据丢失问题。

由于 consumer 在消费过程中可能会出现断电宕机等故障,consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。所以 offset 的维护是 Consumer 消费数据是必须考虑的问题

下方代码,这种消费只会记录最大的offset,相当于在命令行不加 --from-beginning,以前的数据消费不到

import java.util.Collections;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

public class MyConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();

        /* 指定连接kafka集群 */
        properties.put("bootstrap.servers", "centos7-1:9092");

        /* 开启自动提交 */
        properties.put("enable.auto.commit", Boolean.valueOf(true));

        /* 自动提交的延时时间 */
        properties.put("auto.commit.interval.ms", "1000");

        /* key的反序列化 */
        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        /* value的反序列化 */
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        /* 指定消费者组 */
        properties.put("group.id", "mygroup");

        /* 创建消费者对象 */
        KafkaConsumer<String, String> consumer = new KafkaConsumer(properties);

        /* 订阅的主题,可同时订阅多个 */
        consumer.subscribe(Collections.singletonList("bigdata"));


        while (true) {
    
    
            /* 获取数据,设置拉取数据延迟时间 */
            ConsumerRecords<String, String> consumerRecords = consumer.poll(100);


            for (ConsumerRecord<String, String> consumerRecord : consumerRecords)
            {
    
    
                /* 打印消息的键和值 */
                System.out.println(consumerRecord.key() + "==>" + consumerRecord.value());
            }
        }
    }
}

运行截图:
在这里插入图片描述

三、那么如何重新消费某一个主题的数据(自动提交 offset)

可以换一个消费者组,并且重置offset

/* 指定消费者组 */
properties.put("group.id", "mygroup");

/* 重置消费者的offset */
properties.put("auto.offset.reset","earliest");

auto.offset.reset 的源码如下:满足两种条件任一种即可:当换消费者组时或保存数据失效(offset失效时)时,就会生效,重置offset。生效有两种:默认是latest最新的offset,可以修改为earliest,最早的offset。

    public static final String AUTO_OFFSET_RESET_CONFIG = "auto.offset.reset";
    public static final String AUTO_OFFSET_RESET_DOC = "What to do when there is no 
    initial offset in Kafka or if the current offset does not exist any more on the 
    server (e.g. because that data has been deleted): <ul><li>earliest: automatically 
    reset the offset to the earliest offset<li>latest: automatically reset the offset to 
    the latest offset</li><li>none: throw exception to the consumer if no previous offset 
    is found for the consumer's group</li><li>anything else: throw exception to the 
    consumer.</li></ul>";

详情代码如下:

import java.util.Collections;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

public class MyConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();

        /* 指定连接kafka集群 */
        properties.put("bootstrap.servers", "centos7-1:9092");

        /* 开启自动提交 */
        properties.put("enable.auto.commit", Boolean.valueOf(true));

        /* 自动提交的延时时间 */
        properties.put("auto.commit.interval.ms", "1000");

        /* key的反序列化 */
        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        /* value的反序列化 */
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        /* 指定消费者组 */
        properties.put("group.id", "mygroup1");

        /* 重置消费者的offset */
        properties.put("auto.offset.reset","earliest");

        /* 创建消费者对象 */
        KafkaConsumer<String, String> consumer = new KafkaConsumer(properties);

        /* 订阅的主题,可同时订阅多个 */
        consumer.subscribe(Collections.singletonList("bigdata"));


        while (true) {
    
    
            /* 获取数据,设置拉取数据延迟时间 */
            ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
            /* 遍历 */
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords)
            {
    
    
                /* 打印消息的键和值 */
                System.out.println(consumerRecord.key() + "==>" + consumerRecord.value());
            }
        }
    }
}

四、手动提交 offset

  • 虽然自动提交 offset 十分简洁,但由于其是基于时间提交的,开发人员难以把握offset 提交的时机(延迟时间)。因此Kafka 还提供了手动提交 offset 的 API。

  • 手动提交 offset 的方法有两种:分别是 commitSync(同步提交)和 commitAsync(异步提交)。

    两者的相同点是,都会将本次 poll 的一批数据最高的偏移量提交;

    不同点是,commitSync 阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);commitAsync 则没有失败重试机制,故有可能提交失败。

  • 无论是同步提交还是异步提交 offset,都有可能会造成数据的漏消费或者重复消费。先提交 offset后消费,有可能造成数据的漏消费;而先消费后提交 offset,有可能会造成数据的重复消费

同步提交

import java.util.Collections;
import java.util.Properties;
import org.apache.kafka.clients.consumer.*;

public class MyConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();

        /* 指定连接kafka集群 */
        properties.put("bootstrap.servers", "centos7-1:9092");

        /* 开启手动提交 */
        properties.put("enable.auto.commit", Boolean.valueOf(false));

        /* key的反序列化 */
        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        /* value的反序列化 */
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        /* 指定消费者组 */
        properties.put("group.id", "mygroup");

        /* 创建消费者对象 */
        KafkaConsumer<String, String> consumer = new KafkaConsumer(properties);

        /* 订阅的主题,可同时订阅多个 */
        consumer.subscribe(Collections.singletonList("bigdata"));

        while (true) {
    
    
            /* 获取数据,设置拉取数据延迟时间 */
            ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
            /* 遍历 */
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords)
            {
    
    
                /* 打印消息的键和值 */
                System.out.println(consumerRecord.key() + "==>" + consumerRecord.value());
            }

            /* 同步提交,当前线程会阻塞直到 offset 提交成功 */
            consumer.commitSync();

        }
    }
}

异步提交:

import java.util.Collections;
import java.util.Map;
import java.util.Properties;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;

public class MyConsumer {
    
    
    public static void main(String[] args) {
    
    
        Properties properties = new Properties();

        /* 指定连接kafka集群 */
        properties.put("bootstrap.servers", "centos7-1:9092");

        /* 开启手动提交 */
        properties.put("enable.auto.commit", Boolean.valueOf(false));

        /* key的反序列化 */
        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        /* value的反序列化 */
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        /* 指定消费者组 */
        properties.put("group.id", "mygroup");

        /* 创建消费者对象 */
        KafkaConsumer<String, String> consumer = new KafkaConsumer(properties);

        /* 订阅的主题,可同时订阅多个 */
        consumer.subscribe(Collections.singletonList("bigdata"));


        while (true) {
    
    
            /* 获取数据,设置拉取数据延迟时间 */
            ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
            /* 遍历 */
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords)
            {
    
    
                /* 打印消息的键和值 */
                System.out.println(consumerRecord.key() + "==>" + consumerRecord.value());
            }

            /* 异步提交 */
            consumer.commitAsync(new OffsetCommitCallback() {
    
    
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
    
    
                    if (exception != null){
    
    
                        System.err.println("提交失败" + offsets);
                    }else {
    
    
                        System.err.println("提交成功" + offsets);
                    }
                }
            });
        }
    }
}

五、自定义存储 offset

  • Kafka 0.9 版本之前,offset 存储在 zookeeper,0.9 版本及之后,默认将 offset 存储在Kafka的一个内置的 topic 中。除此之外,Kafka 还可以选择自定义存储 offset。 offset的维护是相当繁琐的,因为需要考虑到消费者的 Rebalance

  • 当有新的消费者加入消费者组、已有的消费者推出消费者组或者所订阅的主题的分区发 生变化,就会触发到分区的重新分配,重新分配的过程叫做Rebalance。

  • 消费者发生 Rebalance 之后,每个消费者消费的分区就会发生变化。因此消费者要首先获取到自己被重新分配到的分区,并且定位到每个分区最近提交的 offset 位置继续消费。

  • 要实现自定义存储 offset,需要借助 ConsumerRebalanceListener 类,其中提交和获取 offset 的方法,需要根据所选的 offset 存储系统自行实现

import java.util.*;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;

public class MyConsumer {
    
    
    private static Map<TopicPartition, Long> currentOffset = new  HashMap<>();

    public static void main(String[] args) {
    
    
        Properties properties = new Properties();

        /* 指定连接kafka集群 */
        properties.put("bootstrap.servers", "centos7-1:9092");

        /* 开启手动提交 */
        properties.put("enable.auto.commit", Boolean.valueOf(false));

        /* key的反序列化 */
        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        /* value的反序列化 */
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        /* 指定消费者组 */
        properties.put("group.id", "mygroup");

        /* 创建消费者对象 */
        KafkaConsumer<String, String> consumer = new KafkaConsumer(properties);

        /* 消费者订阅的主题 */
        consumer.subscribe(Arrays.asList("bigdata"), new ConsumerRebalanceListener() {
    
    

            /* 该方法会在 Rebalance 之前调用 */
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
    
    
                /* 调用自己写的提交所有分区的 offset 方法 */
                commitOffset(currentOffset);
            }

            /* 该方法会在 Rebalance 之后调用 */
            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
    
    
                currentOffset.clear();
                /* 遍历 */
                for (TopicPartition partition : partitions) {
    
    
                    /* 定位到最近提交的 offset 位置继续消费 */
                    consumer.seek(partition, getOffset(partition));
                }
            }
        });

        while (true) {
    
    
            /* 获取数据,设置拉取数据延迟时间 */
            ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
            /* 遍历 */
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords)
            {
    
    
                /* 打印消息的键和值 */
                System.out.println(consumerRecord.key() + "==>" + consumerRecord.value());

                currentOffset.put(new TopicPartition(consumerRecord.topic(), consumerRecord.partition()),consumerRecord.offset());
            }

            /* 异步提交 */
            commitOffset(currentOffset);
        }
    }

    /* 获取某分区的最新 offset */
    private static long getOffset(TopicPartition partition) {
    
    
        return 0;
    }

    /* 提交该消费者所有分区的 offset */
    private static void commitOffset(Map<TopicPartition, Long> currentOffset) {
    
    
        /* 自己定义的方法,根据业务逻辑,也可以提交到mysql上,写一个jdbc,创建消费者组、主题、分区字段 */
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_46122692/article/details/109280040