kafka02-Java客户端

kafka02-Java客户端

1.Kafka客户端-Java开发环境搭建

  1. 导入依赖
<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>
  1. 日志配置文件。
<?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>

2.Producer API

  1. 异步发送不带回调。
public static void producerMessage() {
    Properties props = new Properties();

    //kafka集群,对应生产者客户端命令 broker-list
    props.put("bootstrap.servers", "192.168.253.134:9092");
    props.put("acks", "all");

    //重试次数
    props.put("retries", 3);

    //批次大小
    props.put("batch.size", 16384);

    //等待时间
    props.put("linger.ms", 1);

    //RecordAccumulator缓冲区大小
    props.put("buffer.memory", 33554432);

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

    KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);

    for (int i = 0;i < 10;i++) {
        ProducerRecord<String, String> record = new ProducerRecord<>("offsetdemo01", "message" + i);

        kafkaProducer.send(record);
    }

    kafkaProducer.close();
}
  1. 异步发送带回调。
public static void producerMessageCallback() {
    Properties props = new Properties();
	...
    KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);

    for (int i = 0;i < 10;i++) {
        ProducerRecord<String, String> record = new ProducerRecord<>("offsetdemo01", "message####" + i);
        kafkaProducer.send(record, new Callback() {
            /**
                 * 消费发送成功exception为null,失败exception不为null。
                 * @param metadata
                 * @param exception
                 */
            @Override
            public void onCompletion(RecordMetadata metadata, Exception exception) {
                if (exception != null) {
                    System.out.println("消息发送失败!!!");
                }else {
                    System.out.println("消费发送成功," + metadata.topic() + " -- " +
                                       metadata.partition() + " -- " + metadata.offset());
                }
            }
        });
    }

    kafkaProducer.close();
}
  1. 同步发送。
public static void producerMessageFor() {
    Properties props = new Properties();
	...
    KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);

    for (int i = 0;i < 10;i++) {
        ProducerRecord<String, String> record = new ProducerRecord<>("offsetdemo01", "message####" + i);
        Future<RecordMetadata> send = kafkaProducer.send(record, new Callback() {
            /**
                 * 消费发送成功exception为null,失败exception不为null。
                 * @param metadata
                 * @param exception
                 */
            @Override
            public void onCompletion(RecordMetadata metadata, Exception exception) {
                if (exception != null) {
                    System.out.println("消息发送失败!!!");
                } else {
                    System.out.println("消费发送成功," + metadata.topic() + " -- " +
                                       metadata.partition() + " -- " + metadata.offset());
                }
            }
        });

        System.out.println("==== 消息发送 ====");
        try {
            // 使用Future.get()来阻塞发送消息的线程,指定消息发送成功并收到ACK后。
            send.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println("00000 消息发送成功 00000");
    }

    kafkaProducer.close();
}

4.Kafka自定义分区器

public class MyPartition implements Partitioner {

    /**
     *
     * @param topic 主题
     * @param key key
     * @param keyBytes key序列化后的字节数组
     * @param value 消息
     * @param valueBytes 消息序列化后的字节数组
     * @param cluster 集群对象
     * @return
     */
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        if (value instanceof String) {
            String s = (String) value;
            if (s.contains("1")) {
                return 0;
            } else {
                return 1;
            }
        }
        return 0;
    }

    @Override
    public void close() {

    }

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

    }
}

// 使用自定义分区器发送消息
public static void producerMessageCustomPartition() {
    Properties props = new Properties();
	...
    // 指定使用定义的分区器对象
    props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, "com.my.kafka.demo.MyPartition");

    KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);

    for (int i = 0;i < 10;i++) {
        ProducerRecord<String, String> record = null;
        if (i == 1 || i == 5) {
            record = new ProducerRecord<>("offsetdemo01", "message####" + i);
        } else {
            record = new ProducerRecord<>("offsetdemo01", UUID.randomUUID().toString());
        }
        kafkaProducer.send(record, new Callback() {
            /**
                 * 消费发送成功exception为null,失败exception不为null。
                 * @param metadata
                 * @param exception
                 */
            @Override
            public void onCompletion(RecordMetadata metadata, Exception exception) {
                if (exception != null) {
                    System.out.println("消息发送失败!!!");
                } else {
                    System.out.println("消费发送成功," + metadata.topic() + " -- " +
                                       metadata.partition() + " -- " + metadata.offset());
                }
            }
        });
    }

    kafkaProducer.close();
}

3.Consumer API

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

  2. 自动提交offset。

public static void consumerMessage() {
    Properties props = new Properties();

    props.put("bootstrap.servers", "192.168.253.134:9092");

    // 消费者组
    props.put("group.id", "consumer-test");
    // 自动提交offset
    props.put("enable.auto.commit", "true");
    // 自动提交的间隔时间
    props.put("auto.commit.interval.ms", "1000");

    // key和value的反序列化
    props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

    KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(props);
    List<String> partitioners = new ArrayList<>();
    partitioners.add("offsetdemo01");
    // 可以订阅没有的topic,订阅之后kafka会使用默认值创建该topic,一个分区,一个副本。
    //partitioners.add("hello01");

    kafkaConsumer.subscribe(partitioners);
    while (true) {
        // 每隔5秒钟拉取一次消息
        ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(5));

        for (ConsumerRecord<String, String> record : records) {
            System.out.println("消费消息==" + record.topic() + " --" + record.partition() + " -- "
                               + record.key() + " -- " + record.value());
        }
    }
}
  1. offset重置。
/**
     * Offset重置。
     * 当kafka找不到offset时会进行offset的重置。
     * kafka重置offset的两种情况。
     *  1 新加入的消费者组,没有消费记录,没有offset,会进行重置。
     *  2 offset,1-20,距离当前时间超过7天,kafka需要消费1-20的offset,这是找不到
     *  这些消息,就会进行offset的重置。消费7天前的已经被删除的消息。
     *
     *  设置offset的重置策略。auto.offset.reset
     *  earliest: automatically reset the offset to the earliest offset 最早
     *  latest: automatically reset the offset to the latest offset 最晚
     *  none 没有offset就会抛出异常
     */
public static void consumerMessageOffset() {
    Properties props = new Properties();

    props.put("bootstrap.servers", "192.168.253.134:9092");

    // 消费者组
    props.put("group.id", "consumer-test-offset");
    // 是否自动提交
    props.put("enable.auto.commit", "true");
    // 自动提交的间隔时间
    props.put("auto.commit.interval.ms", "1000");

    // 设置offset重置策略,设置为earliest,就可以消费到历史的消息。
    props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

    // key和value的反序列化
    props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

    KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(props);
    List<String> partitioners = new ArrayList<>();
    partitioners.add("offsetdemo01");
    // 可以订阅没有的topic,订阅之后kafka会使用默认值创建该topic,一个分区,一个副本。
    //partitioners.add("hello01");

    kafkaConsumer.subscribe(partitioners);
    while (true) {
        // 设置超时时间
        ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2));

        for (ConsumerRecord<String, String> record : records) {
            System.out.println("消费消息==" + record.topic() + " --" + record.partition() + " -- "
                               + record.key() + " -- " + record.value());
        }
    }
}
  1. 手动提交offset。

    1. 手动提交offset的方法有两种:commitSync同步提交和commitAsync异步提交。
    2. 两者的相同点是:都会将本次poll的一批数据最高的偏移量提交。
    3. 两者的不同点是:commitSync阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而commitAsync则没有失败重试机制,故有可能提交失败。
    4. 同步和异步提交offset。
    //异步提交Offset ,== {offsetdemo01-1=OffsetAndMetadata{offset=588, leaderEpoch=0, metadata=''}, offsetdemo01-0=OffsetAndMetadata{offset=987, leaderEpoch=0, metadata=''}}
    /**
         * 手动提交offset。
         * enable.auto.commit=false。
         * 消费者组第一次启动后,都会获取offset,然后进行消息的消费。这时候每次消费完成,
         * 如果没有进行消费的提交,会将offset维护在内存中。
         *
         * offset手动提交。
         *  同步提交,提交offset后再消费下一条消息。
         *  异步提交,提交offset时就可以消费下一条消息。
         */
    public static void consumerMessageCommitOffset() {
        Properties props = new Properties();
    
        props.put("bootstrap.servers", "192.168.253.134:9092");
    
        // 消费者组
        props.put("group.id", "consumer-test-offset");
        // 是否自动提交
        props.put("enable.auto.commit", "false");
    
        // key和value的反序列化
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(props);
        List<String> partitioners = new ArrayList<>();
        partitioners.add("offsetdemo01");
        kafkaConsumer.subscribe(partitioners);
        
        while (true) {
            // 拉取消息的间隔时间
            ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2));
    
            for (ConsumerRecord<String, String> record : records) {
                System.out.println("消费消息==" + record.topic() + " --" + record.partition() + " -- "
                                   + record.key() + " -- " + record.value());
            }
    
            System.out.println("00 开始提交offset 00");
            // 同步提交Offset,会阻塞当前线程,直到提交完成。
            //kafkaConsumer.commitSync();
    
            // 异步提交Offset
            kafkaConsumer.commitAsync(new OffsetCommitCallback() {
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                    System.out.println("异步提交Offset ,== " + offsets);
                }
            });
    
            System.out.println("AA Offset提交完成 AA");
        }
    }
    
  2. 数据漏消费和重复消费。无论是同步提交还是异步提交offset,都有可能会造成数据的漏消费或者重复消费。先提交offset后消费,有可能造成数据的漏消费;而先消费后提交offset,有可能会造成数据的重复消费。

5.Kafka自定义Interceptor拦截器

  1. Interceptor拦截器使得用户在消息发送前以及producer回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。
  2. 自定义拦截器。
/**
 * 自定义拦截器,在消息前加时间戳
 */
public class TimeInterceptor implements ProducerInterceptor<String, String> {

    /**
     * 消息前加时间戳。
     * 该方法封装进KafkaProducer.send方法中,即它运行在用户主线程中。
     * Producer确保在消息被序列化以及计算分区前调用该方法。
     * 用户可以在该方法中对消息做任何操作,但最好保证不要修改消息所属的topic和分区,
     * 否则会影响目标分区的计算。
     */
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        String value = record.value();

        String timeValue = System.currentTimeMillis() + " ->> " + value;

        ProducerRecord<String, String> newRecord = new ProducerRecord<>(record.topic(), record.partition(),
                record.key(), timeValue);
        return newRecord;
    }

    /**
     * 该方法会在消息从RecordAccumulator成功发送到Kafka Broker之后,
     * 或者在发送过程中失败时调用。并且通常都是在producer回调逻辑触发之前。
     * onAcknowledgement运行在producer的IO线程中,
     * 因此不要在该方法中放入很重的逻辑,否则会拖慢producer的消息发送效率。
     */
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {

    }

    /**
     * 关闭interceptor,主要用于执行一些资源清理工作
     */
    @Override
    public void close() {

    }

    /**
     * 获取配置信息和初始化数据时调用。
     * @param configs
     */
    @Override
    public void configure(Map<String, ?> configs) {

    }
}

/**
 * 定义拦截器,统计生产成功和失败的个数
 */
public class CountInterceptor implements ProducerInterceptor<String, String> {

    private int success;
    private int fail;

    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        return record;
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        if (exception != null) {
            fail++;
        } else {
            success++;
        }
    }

    @Override
    public void close() {
        System.out.println("消息生产成功 -> " + success);
        System.out.println("消息生产失败 -> " + fail);
    }

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

    }
}
  1. 生产者使用拦截器。
public class ProducerInterceptor {

    public static void main(String[] args) {
        Properties props = new Properties();

        //kafka集群,对应生产者客户端命令 broker-list
        props.put("bootstrap.servers", "192.168.253.134:9092");
        props.put("acks", "all");

        //重试次数
        props.put("retries", 3);

        //批次大小
        props.put("batch.size", 16384);

        //等待时间
        props.put("linger.ms", 1);

        //RecordAccumulator缓冲区大小
        props.put("buffer.memory", 33554432);

        // key和value的队列化方式
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        // 设置拦截器,拦截器的调用顺序就是添加到List的顺序
        List<String> interceptors = new ArrayList<>();
        interceptors.add("com.my.kafka.demo.incerptor.TimeInterceptor");
        interceptors.add("com.my.kafka.demo.incerptor.CountInterceptor");
        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);

        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);

        for (int i = 0;i < 10;i++) {
            ProducerRecord<String, String> record = new ProducerRecord<>("offsetdemo01", "message" + i);

            kafkaProducer.send(record);
        }

        kafkaProducer.close();
    }
}
posted @ 2022-03-06 10:17  行稳致远方  阅读(42)  评论(0)    收藏  举报