kafka学习笔记二

一、kafka的java客户端-生产者

1、引入依赖
 <dependency>
     <groupId>org.springframework.kafka</groupId>
     <artifactId>spring-kafka</artifactId>
</dependency>
2、生产者的基本实现
package com.producer;

import com.alibaba.fastjson.JSON;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;
import java.util.concurrent.ExecutionException;

public class MySimpleProducer {
    private static final String TOPIC_NAME="my-replicated-topic";

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Properties properties=new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.111.1:9092,192.168.111.1:9093,192.168.111.1:9094");
        //将发送的key从字符串转换为字节数组
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //把发送消息的value转换为字节数组
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
        //发消息的客户端
        Producer<String,String> producer=new KafkaProducer<>(properties);

        ProducerRecord<String,String> producerRecord=new ProducerRecord<>(TOPIC_NAME,"mykey","hellokafka");

        RecordMetadata recordMetadata = producer.send(producerRecord).get();
        System.out.println("同步发送消息结果"+"topic-"+recordMetadata.topic()+"|partition-"+recordMetadata.partition()+"|offset"+recordMetadata.offset());

        producer.close();
    }
}
3、指定分区发送消息
ProducerRecord<String,String> producerRecord=new ProducerRecord<>(TOPIC_NAME,0,"mykey","hellokafka");
4、未指定分区,则会通过业务key的hash运算,算出消息发送往哪个分区
ProducerRecord<String,String> producerRecord=new ProducerRecord<>(TOPIC_NAME,"mykey","hellokafka");
5、生产者同步发送消息

在这里插入图片描述
如果生产者没有收到ack,生产者会阻塞,阻塞到3s的时间,如果还没有收到消息,会进行重试,重试次数3次。

RecordMetadata recordMetadata = producer.send(producerRecord).get();
        System.out.println("同步发送消息结果" + "topic-" + recordMetadata.topic()
                + "|partition-" + recordMetadata.partition()
                + "|offset" + recordMetadata.offset());
6、生产者异步发送消息

在这里插入图片描述
异步发送生产者发送完消息后就可以执行之后的业务,broker在收到消息后异步调用生产者提供的callback回调方法。

 producer.send(producerRecord, new Callback() {
            @Override
            public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                if (e != null) {
                    System.out.println("异步发送消息失败" + e.getStackTrace());
                }
                if (recordMetadata != null) {
                    System.out.println("异步发送消息结果" + "topic-" + recordMetadata.topic()
                            + "| partition-" + recordMetadata.partition()
                            + "| offset-" + recordMetadata.offset());
                }
            }
        });

二、生产者中的配置

1、ack以及重试配置

在同步发送的前提下,生产者在获得集群的ack之前会一直阻塞。

ack有三个配置:

  • ack=0:kafka-cluser不需要任何broker收到的消息,就立即返回ack给生产者,最容易丢消息,效率是最高的。

    • 如果设置为零,那么生产者根本不会等待来自服务器的任何确认。该记录将立即添加到套接字缓冲区并视为已发送。这种情况下retries不能保证服务端已经收到记录,配置不会生效(因为客户端一般不会知道任何故障)。为每个记录返回的偏移量将始终设置为-1。
      
  • ack=1(默认):多副本之间的leader已经收到消息,并把消息写入到本地log中,才会返回ack给生产者,性能和安全性是最均衡的。

    • 这意味着领导者会将记录写入其本地日志,但会在不等待所有追随者的完全确认的情况下做出响应。在这种情况下,如果领导者在确认记录后立即失败,但在追随者复制它之前,则记录将丢失。
      
  • ack=all/1:里面有默认的配置min.insync.replicas=2(默认为 1,推荐配置大于 2),此时就需要leader和一个follower同步完后,才会返回ack给生产者(此时集群中有2个broker已经完全接收数据),这种方式最安全,性能最差。

    • 这意味着领导者将等待完整的同步副本集来确认记录。这保证了只要至少一个同步副本保持活动状态,记录就不会丢失。这是最强的可用保证。这相当于 acks=-1 设置。
      

ack(没有收到ack,就进行重试)以及重试配置

  properties.put(ProducerConfig.ACKS_CONFIG, 1);
  /* 发送失败会进行重试,默认重试间隔为100ms,重试能保证消息发送的可靠性,
  但是也可能造成消息重复发送,比如网络波动,所以需要在接收者那边进行幂等性校验*/
  properties.put(ProducerConfig.RETRIES_CONFIG,3);
  //重试时间间隔
  properties.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG,300);
2、消息发送的缓冲区
  • kafka默认会创建一个消息缓冲区,用来存放重要消息,缓冲区是32M
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
  • kafka本地线程会去缓冲区中一次拉16KB的数据,发送到broker
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);
  • 如果线程拉不到16k的数据,那么间隔10ms也会将已拉到的数据发送到broker
properties.put(ProducerConfig.LINGER_MS_CONFIG,10);

三、kafka的java客户端-消费者

1、消费者的基本实现
package com.producer;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;

import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;

public class MyConsumer {
    private static final String CONSUMER_GROUP_NAME="testGroup";
    private static final String TOPIC_NAME = "my-replicated-topic";

    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.111.1:9092,192.168.111.1:9093,192.168.111.1:9094");
        //消费者分组名
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,CONSUMER_GROUP_NAME);
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());

        KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(properties);
        consumer.subscribe(Arrays.asList(TOPIC_NAME));
        while (true){
            ConsumerRecords<String,String> records=consumer.poll(Duration.ofMillis(1000));
            for(ConsumerRecord<String,String> record:records){
                System.out.printf("收到的消息: partition = %d ,offset = %d ,key = %s ,value = %s%n",
                        record.partition(),record.offset(),record.key(),record.value());
            }
        }
    }
}
2、关于消费者自动提交与手动提交offset
1、提交的内容

消费者无论是自动提交还是手动提交,都要把所属的消费组+消费的某个主题+消费的分区及消费的偏移量,这样的信息提交到集群的_consumer_offsets主题里面。

2、自动提交

消费poll消息下来以后就会提交offset

 //是否自动提交偏移量
 properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");
 //自动提交offset时间
 properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000")

注意:自动提交会丢消息。因为消费者在提交offset之后,有可能在还未消费之前宕机。

3、手动提交

需要把自动配置改为false

 //是否自动提交偏移量
 properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");
  • 手动同步提交

在消费完消息后调用同步提交的方法,当集群返回ack前一直阻塞,返回ack后表示提交成功,执行之后的逻辑。

  while (true){
            ConsumerRecords<String,String> records=consumer.poll(Duration.ofMillis(1000));
            for(ConsumerRecord<String,String> record:records){
                System.out.printf("收到的消息: partition = %d ,offset = %d ,key = %s ,value = %s%n",
                        record.partition(),record.offset(),record.key(),record.value());
            }
            //所有的消息已消费完
            if(records.count()>0){
                //手动提交offset 当前线程阻塞 知道提交offset成功
                //一般使用同步提交
                consumer.commitSync(); // 造成阻塞 等集群返回ack
            }
        }
  • 手动异步提交

在消息消费完后提交,不需要等待集群返回ack,直接执行后续的逻辑,可以设置一个回调方法,供集群调用。

 while (true){
            ConsumerRecords<String,String> records=consumer.poll(Duration.ofMillis(1000));
            for(ConsumerRecord<String,String> record:records){
                System.out.printf("收到的消息: partition = %d ,offset = %d ,key = %s ,value = %s%n",
                        record.partition(),record.offset(),record.key(),record.value());
            }
            //所有的消息已消费完
            if(records.count()>0){
                //手动提交offset 当前线程阻塞 知道提交offset成功
                //一般使用同步提交
				//consumer.commitSync(); // 造成阻塞 等集群返回ack
                consumer.commitAsync(new OffsetCommitCallback() {
                    @Override
                    public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
                        if(e != null){
                            System.out.println("Commit failed for " + map);
                            System.out.println("Commit failed exception" + e.getStackTrace());
                        }
                    }
                });
            }
        }
3、长轮询poll消息
  • 默认情况下消费者一次会 poll 500 条消息
properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG,500);
  • 代码中设置了长轮询的时间是 1000ms
 while (true){
            ConsumerRecords<String,String> records=consumer.poll(Duration.ofMillis(1000));
            for(ConsumerRecord<String,String> record:records){
                System.out.printf("收到的消息: partition = %d ,offset = %d ,key = %s ,value = %s%n",
                        record.partition(),record.offset(),record.key(),record.value());
            }
 }

意味着:

    • 如果一次poll到500条 直接执行for循环

    • 如果一次没有poll到500条。且时间在1s内,那么长轮询继续poll。要么到500条,要么到1s

    • 如果多次poll 都没达到500条,且时间到了1s ,那么直接执行for循环。

  • 如果两次poll的间隔超过30s,集群会认为该消费者的消费能力弱,会将该消费者踢出消费者组,由另一个消费者进行消费对应分区。(触发rebalance)会造成性能开销,可以通过设置参数,来调节一次所poll消息条数。

//一次最大拉取信息条数 
properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG,500);
//两次poll之间时间间隔
properties.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG,30*1000);
4、消费者消费健康状态检查

消费者每隔1s向kafka集群发送一次心跳,如果kafka集群间隔10s未收到消费者心跳,将会把消费者踢出消费者组,触发该消费组的rebalance,将该分区交由其他消费者进行消费。

//consumer给broker发送心跳时间间隔
properties.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG,1000);
//kafka集群间隔10s未收到消费者心跳
properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG,10*1000);
5、指定分区和偏移量进行消费
  • 指定分区消费
//指定分区消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
  • 从头开始消费
 //消息从头开始消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME,0)))
  • 指定offset消费
//从指定offset开始消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seek(new TopicPartition(TOPIC_NAME,0),10);
  • 指定时间开始消费

根据时间,去所有的partition中确定该时间对应的offset,然后去所有的partition中找到该offset之后的消息进行消费。

 List<PartitionInfo> topicPartition = consumer.partitionsFor(TOPIC_NAME);
        long date = new Date().getTime() - 1000 * 60 * 60;
        Map<TopicPartition,Long> map=new HashMap<>();
        for(PartitionInfo pa:topicPartition){
            map.put(new TopicPartition(TOPIC_NAME,pa.partition()),date);
        }
        Map<TopicPartition,OffsetAndTimestamp> parMap=consumer.offsetsForTimes(map);
        for(Map.Entry<TopicPartition,OffsetAndTimestamp> entry: parMap.entrySet()){
            TopicPartition key=entry.getKey();
            OffsetAndTimestamp timestamp=entry.getValue();
            if(key==null||timestamp==null){
                continue;
            }
            Long offset=timestamp.offset();
            System.out.println("partition "+ key.partition()+"| offset"+offset);
            System.out.println();
            if(timestamp!=null){
                consumer.assign(Arrays.asList(key));
                consumer.seek(key,offset);
            }
        }
6、新消费组消费规则

新消费组中的消费者在启动之后,会从当前分区的最后一个offset+1开始消费新消息。可以通过配置,让新的消费组中的消费者第一次执行时从头开始消费,之后按照offset开始消费。

  • latest(默认):只消费自己启动之后的消息。

  • earliest:第一次从头开始消费,之后按照offset开始消费。

properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值