Kafka学习(二)——消费者Consumer

消费者Consumer

在这里插入图片描述
在这里插入图片描述

自动和手动提交offset

props.setProperty("enable.auto.commit", "true");

KafkaConsumer<String,String> consumer = new KafkaConsumer(props);
        // 消费订阅哪一个Topic或者几个Topic
        consumer.subscribe(Arrays.asList(TOPIC_NAME));
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(10000));
            for (ConsumerRecord<String, String> record : records)
                System.out.printf("patition = %d , offset = %d, key = %s, value = %s%n",
                        record.partition(),record.offset(), record.key(), record.value());
        }
props.setProperty("enable.auto.commit", "false");

KafkaConsumer<String, String> consumer = new KafkaConsumer(props);
        // 消费订阅哪一个Topic或者几个Topic
        consumer.subscribe(Arrays.asList(TOPIC_NAME));
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(10000));
            for (ConsumerRecord<String, String> record : records) {
                // 想把数据保存到数据库,成功就成功,不成功...
                // TODO record 2 db
                System.out.printf("patition = %d , offset = %d, key = %s, value = %s%n",
                        record.partition(), record.offset(), record.key(), record.value());
                // 如果失败,则回滚, 不要提交offset
            }

            // 如果成功,手动通知offset提交
            consumer.commitAsync();
        }

单Partition提交offset

每个Partition单独提交offset:

props.setProperty("enable.auto.commit", "false");

KafkaConsumer<String, String> consumer = new KafkaConsumer(props);

        // 消费订阅哪一个Topic或者几个Topic
        consumer.subscribe(Arrays.asList(TOPIC_NAME));
        
        while (true) {
           ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(10000));
           // 每个partition单独处理
           for(TopicPartition partition : records.partitions()){
               List<ConsumerRecord<String, String>> pRecord = records.records(partition);
               for (ConsumerRecord<String, String> record : pRecord) {
                   System.out.printf("patition = %d , offset = %d, key = %s, value = %s%n",
                           record.partition(), record.offset(), record.key(), record.value());

               }
               //这个一次消费单个partition,poll到的消息最后的一个消息的offset
               long lastOffset = pRecord.get(pRecord.size() -1).offset();
               // 单个partition中的offset,并且进行提交
               Map<TopicPartition, OffsetAndMetadata> offset = new HashMap<>();
               offset.put(partition,new OffsetAndMetadata(lastOffset+1));
               // 提交offset
               consumer.commitSync(offset);
               System.out.println("=============partition - "+ partition +" end================");
           }

手动控制一到多个分区

		TopicPartition p0 = new TopicPartition(TOPIC_NAME, 0);
        TopicPartition p1 = new TopicPartition(TOPIC_NAME, 1);

        // 消费订阅某个Topic的某个分区
        consumer.assign(Arrays.asList(p0));

        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(10000));
            // 每个partition单独处理
            for(TopicPartition partition : records.partitions()){
                List<ConsumerRecord<String, String>> pRecord = records.records(partition);
                for (ConsumerRecord<String, String> record : pRecord) {
                    System.out.printf("patition = %d , offset = %d, key = %s, value = %s%n",
                            record.partition(), record.offset(), record.key(), record.value());

                }
                long lastOffset = pRecord.get(pRecord.size() -1).offset();
                // 单个partition中的offset,并且进行提交
                Map<TopicPartition, OffsetAndMetadata> offset = new HashMap<>();
                offset.put(partition,new OffsetAndMetadata(lastOffset+1));
                // 提交offset
                consumer.commitSync(offset);
                System.out.println("=============partition - "+ partition +" end================");
            }		

多线程并发处理

kafka的producer是线程安全的

kafka的consumer不是线程安全的

方式一:这种类型是经典模式,每一个线程单独创建一个KafkaConsumer,用于保证线程安全在对数据一致性有较高要求的时候适合使用方式一,方式一对partition有较好的管控能力,方便处理失败rollback重新处理

在这里插入图片描述

public static void main(String[] args) throws InterruptedException {
        KafkaConsumerRunner r1 = new KafkaConsumerRunner();
        Thread t1 = new Thread(r1);

        t1.start();

        Thread.sleep(15000);

        r1.shutdown();
    }

    public static class KafkaConsumerRunner implements Runnable{
        private final AtomicBoolean closed = new AtomicBoolean(false);
        private final KafkaConsumer consumer;

        public KafkaConsumerRunner() {
            Properties props = new Properties();
            props.put("bootstrap.servers", KAFKA_SERVER);
            props.put("group.id", "test");
            props.put("enable.auto.commit", "false");
            props.put("auto.commit.interval.ms", "1000");
            props.put("session.timeout.ms", "30000");
            props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
            props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

            consumer = new KafkaConsumer<>(props);

            TopicPartition p0 = new TopicPartition(TOPIC_NAME, 0);
            TopicPartition p1 = new TopicPartition(TOPIC_NAME, 1);

            consumer.assign(Arrays.asList(p0,p1));
        }


        public void run() {
            try {
                while(!closed.get()) {
                    //处理消息
                    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(10000));

                    for (TopicPartition partition : records.partitions()) {
                        List<ConsumerRecord<String, String>> pRecord = records.records(partition);
                        // 处理每个分区的消息
                        for (ConsumerRecord<String, String> record : pRecord) {
                            System.out.printf("patition = %d , offset = %d, key = %s, value = %s%n",
                                    record.partition(),record.offset(), record.key(), record.value());
                        }

                        // 返回去告诉kafka新的offset
                        long lastOffset = pRecord.get(pRecord.size() - 1).offset();
                        // 注意加1
                        consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));
                    }

                }
            }catch(WakeupException e) {
                if(!closed.get()) {
                    throw e;
                }
            }finally {
                consumer.close();
            }
        }

        public void shutdown() {
            closed.set(true);
            consumer.wakeup();
        }
    }

方式二: 先把数据拿下来,在对每一条数据进行多线程处理
方式二 线程中处理消息失败成功也不知到,不能手动提交offset
适合流式数据,对于数据处理正确性要求不高,及非业务系统,推给我数据我就处理,成功失败不重要
例如:定位gps数据上传

在这里插入图片描述

public static void main(String[] args) throws InterruptedException {
        String brokerList = KAFKA_SERVER;
        String groupId = "test";
        int workerNum = 5;

        //创建一个CunsumerExecutor,也就是一个线程池
        CunsumerExecutor consumers = new CunsumerExecutor(brokerList, groupId, TOPIC_NAME);
        consumers.execute(workerNum);

        Thread.sleep(1000000);

        consumers.shutdown();

    }

    // Consumer处理
    public static class CunsumerExecutor{
        private final KafkaConsumer<String, String> consumer;
        private ExecutorService executors;

        public CunsumerExecutor(String brokerList, String groupId, String topic) {
            Properties props = new Properties();
            props.put("bootstrap.servers", brokerList);
            props.put("group.id", groupId);
            props.put("enable.auto.commit", "true");
            props.put("auto.commit.interval.ms", "1000");
            props.put("session.timeout.ms", "30000");
            props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
            props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
            consumer = new KafkaConsumer<>(props);
            consumer.subscribe(Arrays.asList(topic));
        }

        public void execute(int workerNum) {
            executors = new ThreadPoolExecutor(workerNum, workerNum, 0L, TimeUnit.MILLISECONDS,
                    new ArrayBlockingQueue<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());

            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(200);
                for (final ConsumerRecord record : records) {
                    executors.submit(new ConsumerRecordWorker(record));
                }
            }
        }

        public void shutdown() {
            if (consumer != null) {
                consumer.close();
            }
            if (executors != null) {
                executors.shutdown();
            }
            try {
                if (!executors.awaitTermination(10, TimeUnit.SECONDS)) {
                    System.out.println("Timeout.... Ignore for this case");
                }
            } catch (InterruptedException ignored) {
                System.out.println("Other thread interrupted this shutdown, ignore for this case.");
                Thread.currentThread().interrupt();
            }
        }


    }

    // 记录处理
    public static class ConsumerRecordWorker implements Runnable {

        private ConsumerRecord<String, String> record;

        public ConsumerRecordWorker(ConsumerRecord record) {
            this.record = record;
        }

        @Override
        public void run() {
            // 假如说数据入库操作
            System.out.println("Thread - "+ Thread.currentThread().getName());
            System.err.printf("patition = %d , offset = %d, key = %s, value = %s%n",
                    record.partition(), record.offset(), record.key(), record.value());
        }
    }

控制offset起始位置

		KafkaConsumer<String, String> consumer = new KafkaConsumer(props);

        // jiangzh-topic - 0,1两个partition
        TopicPartition p0 = new TopicPartition(TOPIC_NAME, 0);

        // 消费订阅某个Topic的某个分区
        consumer.assign(Arrays.asList(p0));

        while (true) {
            // 手动指定offset起始位置
            /*
                1、人为控制offset起始位置
                2、如果出现程序错误,重复消费一次
             */
            /*
                1、第一次从0消费【一般情况】
                2、比如一次消费了100条, offset置为101并且存入Redis
                3、每次poll之前,从redis中获取最新的offset位置
                4、每次从这个位置开始消费
             */
            consumer.seek(p0, 700);

            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(10000));
            // 每个partition单独处理
            for(TopicPartition partition : records.partitions()){
                List<ConsumerRecord<String, String>> pRecord = records.records(partition);
                for (ConsumerRecord<String, String> record : pRecord) {
                    System.err.printf("patition = %d , offset = %d, key = %s, value = %s%n",
                            record.partition(), record.offset(), record.key(), record.value());

                }
                long lastOffset = pRecord.get(pRecord.size() -1).offset();
                // 单个partition中的offset,并且进行提交
                Map<TopicPartition, OffsetAndMetadata> offset = new HashMap<>();
                offset.put(partition,new OffsetAndMetadata(lastOffset+1));
                // 提交offset
                consumer.commitSync(offset);
                System.out.println("=============partition - "+ partition +" end================");
            }
        }

Consumer限流

		KafkaConsumer<String, String> consumer = new KafkaConsumer(props);

        TopicPartition p0 = new TopicPartition(TOPIC_NAME, 0);
        TopicPartition p1 = new TopicPartition(TOPIC_NAME, 1);

        // 消费订阅某个Topic的某个分区
        consumer.assign(Arrays.asList(p0,p1));
        long totalNum = 40;
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(10000));
            // 每个partition单独处理
            for(TopicPartition partition : records.partitions()){
                List<ConsumerRecord<String, String>> pRecord = records.records(partition);
                long num = 0;
                for (ConsumerRecord<String, String> record : pRecord) {
                    System.out.printf("patition = %d , offset = %d, key = %s, value = %s%n",
                            record.partition(), record.offset(), record.key(), record.value());
                    /*
                        1、接收到record信息以后,去令牌桶中拿取令牌
                        2、如果获取到令牌,则继续业务处理
                        3、如果获取不到令牌, 则pause等待令牌
                        4、当令牌桶中的令牌足够, 则将consumer置为resume状态
                     */
                    num++;
                    if(record.partition() == 0){
                        if(num >= totalNum){
                            consumer.pause(Arrays.asList(p0));    //暂停消费
                        }
                    }

                    if(record.partition() == 1){
                        if(num == 40){
                            consumer.resume(Arrays.asList(p0));    //重启消费
                        }
                    }
                }

                long lastOffset = pRecord.get(pRecord.size() -1).offset();
                // 单个partition中的offset,并且进行提交
                Map<TopicPartition, OffsetAndMetadata> offset = new HashMap<>();
                offset.put(partition,new OffsetAndMetadata(lastOffset+1));
                // 提交offset
                consumer.commitSync(offset);
                System.out.println("=============partition - "+ partition +" end================");
            }

Consumer Rebalance解析

所有topic的分区总数应等于消费组中的消费者数,消费者大于总的分区数,就会有消费者闲置消费不到消息出现rebalance的情况

1.有新的消费者加如消费者组

在这里插入图片描述
2.消费组中的消费者退出

  • 消费组中的消费者异常离开消费组

在这里插入图片描述

  • 消费组中的消费者主动离开消费组

在这里插入图片描述
kafka存在的问题:提交为失败会造成重复消费

在这里插入图片描述

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在开始体验 Kafka 之前,我们需要先下载并安装 KafkaKafka 的官方网站为:https://kafka.apache.org/ ,在该网站的“Downloads”页面中,我们可以找到 Kafka进制文件,选择合适的版本进行下载。 本篇文章将以 Kafka 2.8.0 版本为例进行演示。 ## 安装 Kafka 1. 解压 Kafka 压缩包 将下载的 Kafka 压缩包解压到本地文件夹中,例如:`/usr/local/kafka_2.13-2.8.0/`。 2. 配置环境变量 将 Kafka 的 bin 目录添加到 PATH 环境变量中,以便在终端中能够直接执行 Kafka 的命令。 ```bash export PATH=/usr/local/kafka_2.13-2.8.0/bin:$PATH ``` 可以将该命令添加到 `~/.bashrc` 或 `~/.zshrc` 文件中,以便每次打开终端时自动加载。 ## 启动 Kafka Kafka 的启动需要同时启动 ZooKeeperKafka 服务。 ### 启动 ZooKeeper Kafka 使用 ZooKeeper 来存储集群的元数据和状态信息。在启动 Kafka 之前,我们需要先启动 ZooKeeper。 在终端中执行以下命令来启动 ZooKeeper: ```bash zookeeper-server-start.sh config/zookeeper.properties ``` 该命令将会默认使用 Kafka 的配置文件中的 `zookeeper.properties` 进行启动,该文件位于 Kafka 的安装目录下的 `config` 目录中。 ### 启动 Kafka 在启动 Kafka 之前,我们需要先创建一个 Kafka 主题(Topic),用于存储消息。 在终端中执行以下命令来创建一个名为 `test` 的主题: ```bash kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test ``` 该命令将会使用默认配置,在本地的 Kafka 服务中创建一个名为 `test` 的主题。 接下来,在终端中执行以下命令来启动 Kafka: ```bash kafka-server-start.sh config/server.properties ``` 该命令将会默认使用 Kafka 的配置文件中的 `server.properties` 进行启动,该文件位于 Kafka 的安装目录下的 `config` 目录中。 ### 发送和接收消息 Kafka 提供了一个命令行工具 `kafka-console-producer.sh`,用于向 Kafka 主题中发送消息。 在终端中执行以下命令来发送消息: ```bash kafka-console-producer.sh --broker-list localhost:9092 --topic test ``` 该命令将会打开一个新的终端窗口,在该窗口中输入要发送的消息,按下回车键即可发送。 Kafka 还提供了一个命令行工具 `kafka-console-consumer.sh`,用于从 Kafka 主题中接收消息。 在终端中执行以下命令来接收消息: ```bash kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning ``` 该命令将会打开一个新的终端窗口,用于显示从 Kafka 主题中接收到的消息。 在上述两个终端窗口中,可以分别输入和接收消息,进行 Kafka 的体验和测试。 ## 关闭 Kafka 在终端中执行以下命令来关闭 Kafka: ```bash kafka-server-stop.sh ``` 该命令将会停止当前正在运行的 Kafka 服务。 同样地,我们也需要关闭 ZooKeeper 服务: ```bash zookeeper-server-stop.sh ``` ## 总结 通过本篇文章的演示,我们学习了如何下载、安装和启动 Kafka,并且体验了 Kafka 的基本功能,包括创建主题、发送消息和接收消息等。 在实际的生产环境中,我们需要对 Kafka 进行更加详细的配置和管理,以便保证 Kafka 的高可用性、高性能和高可靠性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值