大数据6_05_Kafka API操作

5 Kafka API

5.1 Producer API

5.1.1 消息发送流程

Kafka的Producer发送消息采用异步发送

ack只保证数据丢不丢和重复不重复的问题,并不会考虑同步还是异步发送。

producer发送一批消息,发送给leader后,不用等接收到ack;就可以发下一批消息。

同步发送是:

producer发送一批消息,发送给leader后,需要ISR内的所有follower接收到,并producer接收到ack,再发送下一批消息。会阻塞当前的线程。

涉及到两个线程:main线程、sender线程,一个共享变量RecordAccumulator。

main线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker

image-20201103234154720

5.1.2 异步发送API

步骤1:导入导入kafkajar包。

    <dependencies>
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>2.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.12.0</version>
        </dependency>
    </dependencies>

步骤2:添加log4j配置文件,文件名为log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="error" strict="true" name="XMLConfig">
    <Appenders>
        <!-- 类型名为Console,名称为必须属性 -->
        <Appender type="Console" name="STDOUT">
            <!-- 布局为PatternLayout的方式,
            输出样式为[INFO] [2018-01-22 17:34:01][org.test.Console]I'm here -->
            <Layout type="PatternLayout"
                    pattern="[%p] [%d{yyyy-MM-dd HH:mm:ss}][%c{10}]%m%n" />
        </Appender>

    </Appenders>

    <Loggers>
        <!-- 可加性为false -->
        <Logger name="test" level="info" additivity="false">
            <AppenderRef ref="STDOUT" />
        </Logger>

        <!-- root loggerConfig设置 -->
        <Root level="info">
            <AppenderRef ref="STDOUT" />
        </Root>
    </Loggers>

</Configuration>

步骤3:编写代码

  • KafkaProducer:创建生产者对象,用来发送数据。
  • ProducerConfig:获取所需的一系列配置参数。
  • ProducerRecord:每条数据都要封装成一个ProducerRecord对象。

不带回调函数的API

public class MyProducer2 {
    public static void main(String[] args) {
        //1 创建配置文件对象
        Properties properties = new Properties();
        //1.1 连接Kafka集群
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
        //1.2 设置key序列化的类型
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        //1.3 设置value序列化的类型
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        
        //1.4 设置ack模式,默认是-1(all)
        properties.put(ProducerConfig.ACKS_CONFIG, "all");
        //1.5 设置重试次数
        properties.put(ProducerConfig.RETRIES_CONFIG, 1);
        //1.6 设置批次大小
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
        //1.7 等待时间
        properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);
        //1.8 缓冲区大小
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);

        //2 创建生产者对象
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
        //3 生产数据
        for (int i = 0; i < 10; i++) {
            producer.send(new ProducerRecord<String, String>("WEI", Integer.toString(i), Integer.toString(i)));
        }
        //4 关闭生产者
        producer.close();

    }
}

带回调函数的API

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

  • 注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试
public class MyProducer {
    public static void main(String[] args) {
        //1 创建配置文件对象
        Properties properties = new Properties();
        //1.1 Kafka集群,broker-list
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
        //1.2 设置key-value序列化的类型
        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");
        
        //2 创建Kafka的生产者,需要传一个配置文件对象
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
        //3 生产数据
        for (int i = 0; i < 10; i++) {
            //3.1 发送数据,需要new一个ProducerRecord对象;同时创建一个回调函数,可以改写为lambda表达式形式!
            producer.send(new ProducerRecord<>("WEI","sun--" + i), new Callback() {
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    if (exception == null){
                        System.out.println(metadata.partition() + "--" + metadata.offset());
                    }else {
                        exception.printStackTrace();
                    }
                }
            });
        }
        //4 关闭资源
        producer.close();
    }
}
5.1.2 同步发送API

同步发送的意思是,一条消息发送后会阻塞当前线程,直至返回ack后,这样效率就慢!相比异步发送,不管受不受到ack就可以发送下一条消息。

send()方法返回的是一个Future对象,调用Future对象的get()方法就可以实现同步发送。

public class MySyncProducer {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1 创建配置文件对象
        Properties properties = new Properties();
        //1.1 kafka集群
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102: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");

        //2 创建kafka的producer
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
        
        //3 生产数据
        for (int i = 0; i < 10; i++) {
            producer.send(new ProducerRecord<>("WEI", "ji--" + i)).get();
        }
        //4 关闭producer
        producer.close();
    }
}
5.1.3 分区器

默认分区器:

image-20201104194452818

自定义分区器:

public class MyPartitioner implements Partitioner {
    /*
     * @Description //TODO 自定义分区的逻辑
     * @Param [topic, key, keyBytes, value, valueBytes, cluster]
     * @return int
     **/
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        Random random = new Random();
        int i = random.nextInt(2);
        return i;
    }

    @Override
    public void close() {

    }

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

    }
}

然后在Producer内设置properties.put()

        //指定自定义分区
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, "com.codejiwei.producer.MyPartitioner");

5.2 Consumer API

因为数据在Kafka中是持久化的所以不用担心数据丢失问题,但是如果consumer挂掉了,offset没有更新的话,下次消费就还会重复消费。所以consumer需要考虑offset的维护。

普通消费者
  • 消费和生产都用BOOTSTRAP_SERVERS_CONFIG, “hadoop102:9092”

  • 自己定义的consumer必须要声明自己是哪个组的,在命令行中没有声明是哪个组的是因为配置文件内

  • 需要消费者需要将磁盘内的序列化数据反序列化读出。

  • 消费者订阅主题,可以订阅多个也可以订阅一个,所以subscribe()内放的是一个Collection

①自动提交offset

默认自动提交;默认auto.commit.interval.ms = 5000ms

image-20201104201530116

如果我enable.auto.commit=false,那么offset一直都没有提交,上一次还是从上一个offset开始消费。

public class MyConsumer {
    public static void main(String[] args) {
        //1 创建properties对此昂
        Properties properties = new Properties();
        //kafka集群
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
        //开启自动提交(默认就是自动提交)
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
        //自动提交延时(默认是5秒)
        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, "jiwei");
        //2 创建kafka消费者对象
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);

        //3 订阅主题
        //可以订阅多个主题,如果是一个:
//        consumer.subscribe(Arrays.asList("WEI"));
        consumer.subscribe(Collections.singletonList("WEI"));
        //可以订阅多个主题,如果是多个:
//        consumer.subscribe(Arrays.asList("WEI", "AA"));

        while (true) {
            //4 获取数据(批量获取)
            ConsumerRecords<String, String> poll = consumer.poll(100);

            //解析并打印consumerRecords
            for (ConsumerRecord<String, String> record : poll) {
                //key也是要存的到kafka的
                System.out.println(record.key() + "--" + record.value());
            }
        }
        // 关闭消费者连接(死循环了就不用关闭了,如果要关就kill)
    }
}
②重置Offset
auto.offset.reset = earliest | latest | none |
# earliest是从头开始,但注意不是0,因为最小的不一定是0
# latest(默认)从当前最大的开始

消费者offset重置的只有在两个特定的场景下才会生效:

  • 消费者组第一次消费的时候。
  • 当前的offset过期了。

java的配置文件中添加:

        //重置消费者的offset
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

注意要换一个组!才能消费到从头开始的数据。(前提要auto.offset.reset = earliest)

③手动提交offset

虽然自动提交offset很便利,但是它是基于auto.commit.interval.ms时间来提交的。

手动提交offset有两种方式:

​ 两种方式都会将本次poll的一批数据最高的offset提交。

  • commitSync(同步提交):一直提交,如果提交不了就重试,直到提交成功,提交失败会重试(也可能会提交失败)
  • commitAsync(异步提交):没有失败重试,可能提交失败。

虽然同步提交可靠性更高,但是效率低;更多的情况下会选用异步提高offset的方法。

同步提交offset

public class MyConsumer2 {
    public static void main(String[] args) {
        //1 创建properties对象
        Properties properties = new Properties();
        // 连接到kafka集群
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
        // 关闭自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        // 添加到组:ji1中
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "ji1");
        // 设置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");

        //2 创建consumer
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);

        //3 订阅主题
        List topics = new ArrayList();
        topics.add("WEI");
        consumer.subscribe(topics);

        //4 消费主题
        while (true){
            ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord.topic() + "--"+consumerRecord.partition()+"--"+consumerRecord.offset());
            }
            // 同步提交
            consumer.commitSync();
        }
    }
}

异步提交offset(默认就是异步提交)

public class MyASyncConsumer {
    public static void main(String[] args) {
        //1 创建properties对象
        Properties properties = new Properties();
        //kafka集群
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
        //设置消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "code");
        //关闭自动提交offset
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        //设置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");

        //2 创建kafka的consumer对象
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);

        //3 订阅主题
        consumer.subscribe(Collections.singletonList("WEI"));
        //4 消费订阅的数据
        while (true){
            //每隔0.1秒拉取数据,注意拉取是成批拉取的。
            ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord.topic() + "--" + consumerRecord.partition() + "--" + consumerRecord.value() + "--" + consumerRecord.offset());
            }
            //拉取完数据后要提交offset
            //异步提交
            consumer.commitAsync(new OffsetCommitCallback() {
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {       
                    //如果exception不为null,说明提交失败。
                    if (exception != null){
                        System.err.println("Commit failed for " + offsets);
                    }
                }
            });
        }
    }
}

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

5.3 自定义Interceptor

拦截器所在的位置:

拦截器链一定是在producer端,producer —> interceptors —> serializer —> partitioner

自定义拦截器需要实现Kafka提供的ProducerInterceptor接口。

onSend()和onAcknowledgement()是被循环调用的。

public class TimeInterceptor implements ProducerInterceptor<String, String> {
    @Override//对record的具体操作,比如给value加点东西,对record进行过滤等
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        return null;
    }
    @Override//根据返回ack的exception是否为null判断是否发送成功。可以用来统计发送成功数量
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
    }
    @Override//关闭资源或者输出结果的一些操作
    public void close() {
    }
    @Override//配置文件
    public void configure(Map<String, ?> configs) {
    }
}
①自定义拦截器1

给发送的数据添加一个时间戳

public class TimeInterceptor implements ProducerInterceptor<String, String> {
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        //1 取出数据
        String value = record.value();
        //2 因为record没有set方法,所以需要创建一个新的ProducerRecord对象
        ProducerRecord<String, String> record1 = new ProducerRecord<>("WEI", System.currentTimeMillis() + value);
        return record1;
    }
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
    }
    @Override
    public void close() {
    }
    @Override
    public void configure(Map<String, ?> configs) {
    }
}
②统计发送消息成功和发送失败消息数
public class CountInterceptor implements ProducerInterceptor<String, String> {
    int success;
    int error;
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {//虽然不需要对record操作,但是需要将record返回
        return record;
    }
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        if (exception == null){
            success++;
        }else {
            error++;
        }
    }
    @Override//为什么要在close中输出呢?因为上面的两个方法是被循环调用的,最后的结果应该在关闭时候
    public void close() {
        System.out.println("success--" + success);
        System.out.println("error--" + error);
    }
    @Override
    public void configure(Map<String, ?> configs) {
    }
}
③配置拦截器链
//        //设置一个拦截器
//        properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, "com.codejiwei.interceptor.TimeInterceptor");

        //设置拦截器链
        ArrayList<String> interceptors = new ArrayList<>();
        interceptors.add("com.codejiwei.interceptor.TimeInterceptor");
        interceptors.add("com.codejiwei.interceptor.CountInterceptor");
        properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

最佳第六六六人

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值