Kafka入门第二课:Producer&Consumer api和自定义拦截器与分区器

依赖

            <!--kafka依赖-->
            <dependency>
                <groupId>org.apache.kafka</groupId>
                <artifactId>kafka-clients</artifactId>
                <version>0.11.0.0</version>
            </dependency>

Producer api 

package com.kafka.producer;

import org.apache.kafka.clients.producer.*;

import java.util.Arrays;
import java.util.Properties;

/**
 * Producer发送数据是异步发送
 *      涉及到两个线程main线程和sender线程、和一个线程贡献变量RecordAccumulator,线程共享变量存放待发送的数据。
 * Producer发送消息也可以同步发送,实际上就是用producer.send返回的对象调用get方法,get方法会阻塞当前的main线程。
 *      选择同步发送并将topic设置为一个分区,可以保证topic内数据绝对有序。 同步发送基本不用。
 * 发送数据流程:
 *      main线程中Producer调用send发送数据,数据依次经过拦截器、序列化器、分区器后进入RecordAccumulator,
 *      sender线程主动来RecordAccumulator中取数据。
 * “batch.size”:Producer发送消息是按批次发送的,每批次大小该参数来指定。
 *  “linger.ms”:数据量很小的时候可能很久都达不到批次大小,故可设置该参数来指定发送的时间间隔。
 * 发送指的是写入到RecoderAccumulator缓存区。
 * “acks”:kafka 的ack应答级别,可以时0、1、all(相当于-1) 。
 * "partitioner.class":指定自定义分区器的绝对路径。
 * "interceptor.classes":指定自定义拦截器,传入一个集合,先生效的拦截器写在集合前面。
 *  生产者中有一个ProducerConfig类,消费者中有ConsumerConfig类,这两个类中都定义了很多常量,对应kafka的所有配置。
 *  发送消息:
 *      producer.send(new ProducerRecord<>("topic名"[[,分区号],key],"消息")[,new Callback(){....}]);
 *  代码最后需要关闭producer:producer.close();
 */
public class MyProducer {
    public static void main(String[] args) {
        //创建kafka生产者
        Properties ps = new Properties();
            ps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.149.131:9092");//Kafka服务端的主机名和端口号
            ps.put(ProducerConfig.ACKS_CONFIG, "all");  // 等待所有副本节点的应答
            ps.put(ProducerConfig.RETRIES_CONFIG, 3);  // 消息发送最大尝试次数
            ps.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);  // 一批次发送消息的大小16K,这个发送是指写入RecoderAccumulator缓存区
            ps.put(ProducerConfig.LINGER_MS_CONFIG, 1);  // 即使1ms内未达到batch.size也会发送数据
            ps.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);  // producer端RecoderAccumulator缓存区内存大小
            ps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");  // key序列化
            ps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");// value序列化
            ps.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.kafka.partitioner.MyPartitioner");//自定义分区器
            ps.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, //自定义拦截器,写前面的拦截器先生效
                    Arrays.asList("com.kafka.interceptor.AddTimestampInterceptor", "com.kafka.interceptor.CountValueInterceptor"));

        //创建producer对象
        KafkaProducer<String, String> producer = new KafkaProducer<>(ps);

        //发送数据
        for (int i = 0; i < 10; i++) {
            /**
             *  不带回调函数发送
             *  producer.send(new ProducerRecord<>("producer","生产数据---"+i));
             */
            /**
             * 带回调函数发送消息
             *      发送成功返回元信息,通过元信息可以打印分区、offset等信息
             *      发送失败返回异常信息
             *  ProducerRecord有很多重载的构造器,可以指定消息发送的分区、也可以指定消息的key
             *  ProducerRecord有三个常用构造器
             *      ProducerRecord(topic,value)                 指定topic和消息
             *      ProducerRecord(topic,key,value)             指定topic、分区key和消息
             *      ProducerRecord(topic,partition,key,value)   指定topic、分区、分区key和消息(分区key不生效)
             */
            producer.send(new ProducerRecord<>("prod_ucer","partition_1","生产数据----" + i), new Callback() {
                @Override
                public void onCompletion(RecordMetadata meta, Exception e) {
                    if(meta != null)
                        System.out.println("分区:"+meta.partition()+"-----offset:"+meta.offset());
                    else
                        e.printStackTrace();
                }
            });
        }
        //关流
        producer.close();
    }
}

Consumer api

package com.kafka.consumer;

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

import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;

/**
 *
 * 只有当找不到任何消费的offset且消费策略是earliest的时候,才会从最开始的地方消费。
 *      找不到任何消费的offset的两种情况:1.使用了全新的消费者组。2.数据消费发生在7天前,已经清除了数据。
 * 一个消费者组可以订阅多个不同的topic,但为了防止数据混乱,生产开发强制一个消费者组只能订阅一个topic。
 * 所谓的offset自动提交,其实就是每隔一段时间自动将内存中的offset保存到名为“__consumer_offsets”的topic中,
 *      消费者只有在刚启动时才会去该topic中获取offset。其他时刻直接通过内存中的offset读取数据。
 * kafka的消费者拉取数据也是一批一批的拉取,拉取完了之后立马开始消费,
 *      如果没有消费完就自动提交了offset,此时宕机重启,就丢数据了。
 *      如果消费完了没有自动提交offset,此时提交宕机重启,就重复消费了。
 * 所以自动提交时提交的时间间隔就很重要了。可是开发人员又很难把握这个时间间隔到底设置为多少。所以开发人员一般手动提交offset。
 * 所谓的offset手动提交,其实就是在消费完成后或者消费开始前手动调用提交offset的方法将内存中的offset保存到名为“__consumer_offsets”的topic。
 *  手动提交分为同步提交"consumer.commitAsync();"和异步提交"consumer.commitAsync(new OffsetCommitCallback() {...})",
 *  同步提交会阻塞当前线程不断重试直到提交成功(用得少)。异步提交不会重试提交,效率更高。
 *      如果先消费然后手动提交offset未完成,宕机重启可能会造成重复消费。
 *      如果先手动提交offset然后消费未完成,宕机重启可能会造成数据丢失。
 * 所以自动提交和手动提交都不靠谱,kafka提供了第三种提交offset的方式:即自定义存储offset。
 * 自定义存储offset时需要考虑到消费者组rebalance后带来的消费者消费数据offset变化的问题。
 *
 */
public class MyConsumer {
    public static void main(String[] args) {
        //设置属性
        Properties ps = new Properties();
            ps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.149.131:9092");//Kafka服务端的主机名和端口号
            //ps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");//开启自动提交
            //ps.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");//每一秒自动提交一次offset
            ps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");//关闭自动提交
        ps.put(ConsumerConfig.GROUP_ID_CONFIG,"my_consumer");//消费者组
        ps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");//消费策略,默认是latest
        ps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");//KEY反序列化
        ps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");//VALUE反序列化

        //创建consumer对象和订阅主题
        KafkaConsumer<String,String> consumer = new KafkaConsumer<>(ps);
        consumer.subscribe(Collections.singletonList("first_topic"));//生产强制订阅一个主题,否则可能消费数据混乱

        //不停地拉取数据并消费数据,每次拉取的是一批数据,而不是一条
        while(true){
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1).toMillis());//拉取数据并设置超时时间
            consumerRecords.forEach(
                  recode ->{
                      System.out.println("topic:" + recode.topic() +
                              "partition:" + recode.partition() +
                              "offset:" + recode.offset());
                  }
            );
            //consumer.commitSync();//同步提交,会阻塞当前线程,不断重试提交直到成功为止
            consumer.commitAsync(new OffsetCommitCallback() {//手动提交中的异步提交,带回调函数
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
                    if(e != null){
                        System.out.print("offset提交失败:");
                        e.printStackTrace();
                    }
                }
            });
        }
        //拉取和消费不停,所以无需关闭consumer对象
    }
}

自定义拦截器

我们定义一条拦截器链,该拦截器链由两个拦截器组成。
第一个拦截器负责在消息的头部追加一个时间戳。
第二个拦截器负责统计发送给broker成功和失败的消息总数。  
package com.kafka.interceptor;

import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.header.Headers;

import java.util.Map;

/**
 * 本类是kafka自定义拦截器:用于在消息头部追加一个当前系统时间
 * 拦截器:
 *      用户在消息发送前以及 producer 回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。
 *      多个拦截器作用于同一条消息从而形成一个拦截链。
 *      拦截器可能运行在多个线程中,线程安全需要用户自己去保证。
 */
public class AddTimestampInterceptor implements ProducerInterceptor {
    /**
     *获取配置信息和初始化数据时调用
     * @param map
     */
    @Override
    public void configure(Map<String, ?> map) {

    }

    /**
     *  序列化及计算分区前调用,用户可以在该方法内对消息做任何更改。即使不更改请将消息原样返回。
     *  运行在producer的主线程中。
     *      由于ProducerRecord中所有属性均为final,无法更改,所以改变value后需要改回一个新的ProducerRecord对象
     * @param producerRecord 消息记录
     * @return
     */
    @Override
    public ProducerRecord onSend(ProducerRecord producerRecord) {
        String topic = producerRecord.topic();
        Integer partition = producerRecord.partition();
        Headers headers = producerRecord.headers();
        Object key = producerRecord.key();
        Object value = producerRecord.value();
        Long timestamp = producerRecord.timestamp();
        return new ProducerRecord(topic,partition,timestamp,key,System.currentTimeMillis()+","+value,headers);
    }

    /**
     * 在消息成功发送给broker或发送失败时调用
     * 由于该方法在producer的io线程中调用,故不宜书写过重的逻辑,以免影响消息发送速度
     * @param recordMetadata 消息发送成功返回消息的元信息
     * @param e  消息发送失败返回异常
     */
    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {

    }

    /**
     *关闭拦截器,做一些资源的清理工作
     */
    @Override
    public void close() {

    }

}
package com.kafka.interceptor;

import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 本类是kafka自定义拦截器:用于在统计发送给broker的消息成功和失败的输量
 * 拦截器:
 *      用户在消息发送前以及 producer 回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。
 *      多个拦截器作用于同一条消息从而形成一个拦截链。
 *      拦截器可能运行在多个线程中,线程安全需要用户自己去保证。
 */
public class CountValueInterceptor implements ProducerInterceptor {
    /**
     *使用java.util.concurrent.atomic中的AtomicInteger来线程安全地实现i++功能
     */
    private static AtomicInteger successNum=new AtomicInteger(0);//记录发送成功的消息数量
    private static AtomicInteger failNum=new AtomicInteger(0);//记录发送失败的消息数量
    /**
     *获取配置信息和初始化数据时调用
     * @param map
     */
    @Override
    public void configure(Map<String, ?> map) {

    }

    /**
     *  序列化及计算分区前调用,用户可以在该方法内对消息做任何更改。即使不更改请将消息原样返回。
     *      由于ProducerRecord中所有属性均为final,无法更改,所以改变value后需要改回一个新的ProducerRecord对象
     * @param producerRecord 消息记录
     * @return  对消息不做修改,所以直接返回producerRecord
     */
    @Override
    public ProducerRecord onSend(ProducerRecord producerRecord) {
        return producerRecord;
    }

    /**
     * 在消息成功发送给broker或发送失败时调用
     * 由于该方法在producer的io线程中调用,故不宜书写过重的逻辑,以免影响消息发送速度
     * @param recordMetadata 消息发送成功返回消息的元信息
     * @param e  消息发送失败返回异常
     */
    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
        if(recordMetadata != null)
            successNum.getAndAdd(1); //加1
        else
            failNum.getAndAdd(1);//加1
    }

    /**
     *关闭拦截器,做一些资源的清理工作
     */
    @Override
    public void close() {
        System.out.println("发送成功的消息数量:" + successNum + "\n 发送失败的消息数量:" + failNum);
    }

}

自定义分区器

package com.kafka.partitioner;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.clients.producer.internals.DefaultPartitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;

import java.util.List;
import java.util.Map;

/**
 * kafka自定义分区器实现自定义分区
 *      key和分区是在发送消息时new ProducerRecord对象时指定。
 * kafka默认的分区规则需要查看org.apache.kafka.clients.producer.internals.DefaultPartitioner的partition方法
 *
 */
public class MyPartitioner implements Partitioner {
    /**
     * 定义分区规则的方法
     * @param topic topic名
     * @param key 键
     * @param keyBytes
     * @param value
     * @param valueBytes
     * @param cluster kafka集群对象
     * @return  返回0,则数据进入0号分区
     */
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {

        //这里通过主题名称得到该主题所有的分区信息
        List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
        int numPartitions = partitionInfos.size();
        if("partition_0".equals(key.toString())){
            return 0;
        }else if("partition_1".equals(key.toString())){
            return 1;
        }else{//如果没有自定义的分区规则,则调用kafka内部的分区规则
            return new DefaultPartitioner().partition(topic,key,keyBytes,value,valueBytes,cluster);
       }
    }

    @Override
    public void close() {

    }

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

    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二百四十九先森

你的打赏是我努力的最大动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值