7. Kafka Producer和Consumer API

文章目录

    • Kafka Producer和Consumer API
      • 1. Producer API
        • 1.1 消息发送流程
        • 1.2 异步发送API
            • 不带回调函数的 API
            • 自定义分区生产者
            • 带回调函数的API
        • 1.3 同步发送API
      • 2. Consumer API
        • 2.1 自动提交 offset
        • 2.2 手动提交 offset
            • 同步提交 offset
            • 异步提交 offset
            • 自定义存储 offset

Kafka Producer和Consumer API

1. Producer API

1.1 消息发送流程

Kafka 的 Producer 发送消息采用的是 异步发送的方式。在消息发送的过程中,涉及到了两个线程 ——main 线程和 Sender 线程,以及 一个线程共享变量 ——RecordAccumulator。

  • main 线程将消息发送给 RecordAccumulator
  • Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka broker
  • 7. Kafka Producer和Consumer API_第1张图片

1.2 异步发送API

引入依赖

<dependency>
    <groupId>org.apache.kafkagroupId>
    <artifactId>kafka-clientsartifactId>
    <version>0.11.0.0version>
dependency>
不带回调函数的 API
/**
 * @Date 2020/8/19 15:25
 * @Version 10.21
 * @Author DuanChaojie
 */
public class MyProducer {
    public static void main(String[] args) {
        // 1,生产者的配置信息
        Properties properties = new Properties();
        //配置属性可以使用工具类,ProducerConfig,ConsumerConfig,CommonClientConfigs

        // 指定连接的kafka集群
        properties.put("bootstrap.servers","hadoop:9092");
        System.out.println("ProducerConfig.BOOTSTRAP_SERVERS_CONFIG = " + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG);


        // ACK应答级别
        properties.put("acks","all");
        System.out.println("ProducerConfig.ACKS_CONFIG = " + ProducerConfig.ACKS_CONFIG);

        // 重试次数
        properties.put("retries",3);
        System.out.println("ProducerConfig.RETRIES_CONFIG = " + ProducerConfig.RETRIES_CONFIG);

        // 批次大小16K
        // 只有数据积累到 batch.size 之后,sender 才会发送数据。
        properties.put("batch.size",16382);
        System.out.println("ProducerConfig.BATCH_SIZE_CONFIG = " + ProducerConfig.BATCH_SIZE_CONFIG);

        // 等待时间,1ms
        // 如果数据迟迟未达到 batch.size,sender 等待 linger.time 之后就会发送数据。
        properties.put("linger.ms",1);
        System.out.println("ProducerConfig.LINGER_MS_CONFIG = " + ProducerConfig.LINGER_MS_CONFIG);

        // RecordAccumulator缓冲区大小
        properties.put("buffer.memory",33554432);
        System.out.println("ProducerConfig.BUFFER_MEMORY_CONFIG = " + ProducerConfig.BUFFER_MEMORY_CONFIG);


        // key和value的序列化类
        properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");

        // 需要创建一个生产者对象,用来发送数据
        Producer<String,String> producer = new KafkaProducer<>(properties);

        // 发送数据
        for (int i = 0; i < 10; i++) {
            // 每条数据都要封装成一个 ProducerRecord 对象
            producer.send(new ProducerRecord<>("bigdata","woshikey","atguigu---"+i));
        }

        // 关闭资源
        producer.close();
    }
}
自定义分区生产者

MyPartitioner

/**
 * @Date 2020/8/20 17:55
 * @Version 10.21
 * @Author DuanChaojie
 */
public class MyPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        return 0;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> configs) {

    }
}

PartitionProducer

/**
 * @Date 2020/8/20 18:01
 * @Version 10.21
 * @Author DuanChaojie
 */
public class PartitionProducer {
    public static void main(String[] args) {
        // 创建配置信息
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop:9092");

        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");

        // 配置自定义分区
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.atguigu.partitioner.MyPartitioner");


        Producer<String,String> producer = new KafkaProducer<>(properties);

        for (int i = 0; i < 10; i++) {
            producer.send(new ProducerRecord<>("bigdata", "atguigu++++++" + i), (metadata, exception) -> {
                if (exception == null) {
                    System.out.println(metadata.partition() + "-----------"+ metadata.offset());
                }else {
                    exception.printStackTrace();
                }
            });
        }

        producer.close();

    }
}

默认的分区策略源码:DefaultPartitioner

public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
    List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
    int numPartitions = partitions.size();
    if (keyBytes == null) {
        int nextValue = nextValue(topic);
        List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
        if (availablePartitions.size() > 0) {
            int part = Utils.toPositive(nextValue) % availablePartitions.size();
            return availablePartitions.get(part).partition();
        } else {
            // no partitions are available, give a non-available partition
            return Utils.toPositive(nextValue) % numPartitions;
        }
    } else {
        // hash the keyBytes to choose a partition
        return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
    }
}
带回调函数的API

回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是RecordMetadata 和 Exception,如果 Exception 为 null,说明消息发送成功,如果Exception 不为 null,说明消息发送失败。

  • 注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。
/**
 * @Date 2020/8/19 15:48
 * @Version 10.21
 * @Author DuanChaojie
 */
public class CallbackProducer {
    public static void main(String[] args) {

        // 创建配置信息
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop:9092");

        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");


        // 创建生产者对象
        KafkaProducer<String,String> producer = new KafkaProducer<>(properties);

        // 发送数据
        for (int i = 0; i < 10; i++) {
            producer.send(new ProducerRecord<>("bigdata", "atguigu====" + i), (metadata, exception) -> {
                if (exception == null) {
                    System.out.println(metadata.partition() + "-----------"+ metadata.offset());
                }else {
                    exception.printStackTrace();
                }
            });
        }

        producer.close();

    }
}

1.3 同步发送API

  • 同步发送的意思就是,一条消息发送之后,会阻塞当前线程,直至返回 ack。
  • 由于 send 方法返回的是一个 Future 对象,根据 Futrue 对象的特点,我们也可以实现同步发送的效果,只需在调用 Future 对象的 get 方法即可。
  • 7. Kafka Producer和Consumer API_第2张图片

2. Consumer API

  1. Consumer 消费数据时的可靠性是很容易保证的,因为数据在 Kafka 中是持久化的,故不用担心数据丢失问题。
  2. 由于 consumer 在消费过程中可能会出现断电宕机等故障,consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。
  3. 所以 offset 的维护是 Consumer 消费数据是必须考虑的问题。

2.1 自动提交 offset

引入依赖

        <dependency>
            <groupId>org.apache.kafkagroupId>
            <artifactId>kafka-clientsartifactId>
            <version>0.11.0.0version>
        dependency>

代码实现

为了使我们能够专注于自己的业务逻辑,Kafka 提供了自动提交 offset 的功能。自动提交 offset 的相关参数:

  • enable.auto.commit :是否开启自动提交 offset 功能
  • auto.commit.interval.ms :自动提交offset的时间间隔
/**
 * @Date 2020/8/20 18:44
 * @Version 10.21
 * @Author DuanChaojie
 */
public class MyConsumer {
    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop:9092");

        // 开启自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
        // 自动提交的延时
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");
        // key和value的反序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");

        // 消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"bigdata");

        // 创建消费者
        KafkaConsumer consumer = new KafkaConsumer<>(properties);

        // 订阅主题
        consumer.subscribe(Arrays.asList("bigdata"));

        while (true){
            // timeout,维护长轮询--防止空转
            // 每条数据都要封装成一个 ConsumerRecord 对象
            ConsumerRecords<String,String> consumerRecords = consumer.poll(100);
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord.key() + "------" + consumerRecord.value());
            }
        }
    }
}

2.2 手动提交 offset

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

手动提交 offset 的方法有两种:

  • 分别是 commitSync(同步提交)和 commitAsync(异步提交)。
  • 两者的相同点是,都会将次 本次 poll 的一批数据最高的偏移量提交;
  • 不同点是,commitSync 阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而 commitAsync 则没有失败重试机制,故有可能提交失败。
同步提交 offset

由于同步提交 offset 有失败重试机制,故更加可靠

//关闭自动提交 offset
properties.put("enable.auto.commit", "false");
// ....
while( true ){
    // timeout,维护长轮询--防止空转
    // 每条数据都要封装成一个 ConsumerRecord 对象
    ConsumerRecords<String,String> consumerRecords = consumer.poll(100);
    for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
        System.out.println(consumerRecord.key() + "------" + consumerRecord.value());
    }
   //同步提交,当前线程会阻塞直到 offset 提交成功
	consumer.commitSync();  
}
异步提交 offset

虽然同步提交 offset 更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会收到很大的影响。因此更多的情况下,会选用异步提交 offset 的方式。

//关闭自动提交 offset
properties.put("enable.auto.commit", "false");
// ....
while( true ){
    // timeout,维护长轮询--防止空转
    // 每条数据都要封装成一个 ConsumerRecord 对象
    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("Commit  failed  for"  + offsets);
            }
        }
    });
}

数据漏消费和重复消费分析

  • 无论是同步提交还是异步提交 offset,都有可能会造成数据的漏消费或者重复消费。先提交 offset 后消费,有可能造成数据的漏消费;而先消费后提交 offset,有可能会造成数据的重复消费。
自定义存储 offset
  1. Kafka 0.9 版本之前,offset 存储在 zookeeper,0.9 版本及之后,默认将 offset 存储在 Kafka的一个内置的 topic 中。
  2. 除此之外,Kafka 还可以选择自定义存储 offset。offset 的维护是相当繁琐的,因为需要考虑到消费者的 Rebalace。
  3. 当有新的消费者加入消费者组、已有的消费者推出消费者组或者所订阅的主题的分区发生变化,就会触发到分区的重新分配,重新分配的过程叫做 Rebalance。消费者发生 Rebalance 之后,每个消费者消费的分区就会发生变化。因此消费者要首先获取到自己被重新分配到的分区,并且定位到每个分区最近提交的 offset 位置继续消费。
  4. 要实现自定义存储 offset,需要借助 ConsumerRebalanceListener,以下为示例代码,其中提交和获取 offset 的方法,需要根据所选的 offset 存储系统自行实现。

/**
 * @Date 2020/8/20 23:26
 * @Version 10.21
 * @Author DuanChaojie
 */
public class MyOffset {
    private static Map<TopicPartition, Long> currentOffset = new HashMap<>();

    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop:9092");

        // 开启自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
        // 自动提交的延时
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");
        // key和value的反序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");

        // 消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"bigdata");

        // 创建消费者
        KafkaConsumer consumer = new KafkaConsumer<>(properties);

        // 订阅主题
        consumer.subscribe(Arrays.asList("bigdata"), new ConsumerRebalanceListener() {
            // 该方法会在 Rebalance 之前调用
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
                commitOffset(currentOffset);
            }
            
            // 该方法会在 Rebalance 之后调用
            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
                currentOffset.clear();
                for (TopicPartition partition : partitions) {
                    //定位到最近提交的 offset 位置继续消费
                    consumer.seek(partition, getOffset(partition));

                }
            }

        });

        while (true){
            // timeout,维护长轮询--防止空转
            ConsumerRecords<String,String> consumerRecords = consumer.poll(100);
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord.key() + "------" + consumerRecord.value());
            }
            // 异步提交
            commitOffset(currentOffset);
        }

    }

    private static void commitOffset(Map<TopicPartition, Long> currentOffset) {

    }

    private static long getOffset(TopicPartition partition) {
        return 0;
    }
}

你可能感兴趣的:(kafka)