第3章 Kafka API

3.1 Producer API

3.1.1 消息发送流程

Kafka 的 Producer 发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程:main 线程和 Sender 线程,以及一个线程共享变量:RecordAccumulator。main 线程将消息发送给 RecordAccumulator,Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka broker。
在这里插入图片描述

相关参数

  • batch.size:只有数据积累到 batch.size 之后,sender 才会发送数据。
  • linger.ms:如果数据迟迟未达到 batch.size,sender 等待 linger.time 之后就会发送数据。

3.1.2 异步发送API

  1. 添加依赖
<dependency>
	<groupId>org.apache.kafka</groupId>
	<artifactId>kafka-clients</artifactId>
	<version>2.7.1</version>
</dependency>
  1. 编写代码
  • KafkaProducer:生产者对象,用来发送数据
  • ProducerConfig:配置参数对象
  • ProducerRecord:每条数据都要封装成一个 ProducerRecord 对象

不带回调函数的API

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.Properties;

public class CustomerProducer1 {
    public static void main(String[] args) {
        Properties properties = new Properties();

        //kafka 集群,broker-list
        properties.put("bootstrap.servers", "lubin01:9092,lubin02:9092,lubin03:9092");

        properties.put("acks", "all");
        //重试次数
        properties.put("retries", 1);
        //批次大小
        properties.put("batch.size", 16384);
        //等待时间
        properties.put("linger.ms", 1);
        //RecordAccumulator 缓冲区大小
        properties.put("buffer.memory", 33554432);
        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<100;i++){
            producer.send(new ProducerRecord<String,String>("test",Integer.toString(i),Integer.toString(i)));
        }
        producer.close();
    }
}

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

注意:消息发送失败会自动重试,不需要在回调函数中手动重试。

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

import java.util.Properties;

public class CustomerProducer3 {
    public static void main(String[] args) {
        Properties properties = new Properties();

        //kafka 集群,broker-list
        properties.put("bootstrap.servers", "lubin01:9092,lubin02:9092,lubin03:9092");

        properties.put("acks", "all");
        //重试次数
        properties.put("retries", 1);
        //批次大小
        properties.put("batch.size", 16384);
        //等待时间
        properties.put("linger.ms", 1);
        //RecordAccumulator 缓冲区大小
        properties.put("buffer.memory", 33554432);
        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 < 500; i++) {
            producer.send(new ProducerRecord<String, String>("test", Integer.toString(i), Integer.toString(i)),
                    new Callback() {
                        @Override
                        public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                            if (e == null) {
                                System.out.println("success-> " + recordMetadata.offset());
                            } else {
                                e.printStackTrace();
                            }
                        }
                    });
        }
        producer.close();
    }
}

3.1.3 同步发送API

同步发送的意思就是,一条消息发送之后,会阻塞当前线程,直至返回 ack。

由于 send 方法返回的是一个 Future 对象,根据 Futrue 对象的特点,可以实现同步发送的效果,只需调用 Future 对象的 get 方发即可。

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

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

public class CustomerProducer2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Properties properties = new Properties();

        // broker-list
        properties.put("bootstrap.servers", "lubin01:9092,lubin02:9092,lubin03:9092");

        properties.put("acks", "all");
        //重试次数
        properties.put("retries", 1);
        //批次大小
        properties.put("batch.size", 16384);
        //等待时间
        properties.put("linger.ms", 1);
        //RecordAccumulator 缓冲区大小
        properties.put("buffer.memory", 33554432);
        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 < 100; i++) {
            producer.send(new ProducerRecord<String, String>("test",
                            Integer.toString(i),
                            Integer.toString(i))).get();
        }
        producer.close();
    }
}

3.2 Consumer API

Consumer 消费数据时的可靠性是很容易保证的,因为数据在 Kafka 中是持久化的,故不用担心数据丢失问题。

由于 consumer 在消费过程中可能会出现断电宕机等故障,consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。

所以 offset 的维护是 Consumer 消费数据是必须考虑的问题。

3.2.1 自动提交offset

  • KafkaConsumer:消费者对象,用来消费数据
  • ConsumerConfig:配置参数对象
  • ConsumerRecord:每条数据都要封装成一个 ConsumerRecord 对象

为了专注于自己的业务逻辑,Kafka 提供了自动提交 offset 的功能。自动提交 offset 的相关参数如下

  • enable.auto.commit:是否开启自动提交 offset 功能
  • auto.commit.interval.ms:自动提交 offset 的时间间隔
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

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

public class CustomConsumer {
    public static void main(String[] args){
        Properties props = new Properties();
        props.put("bootstrap.servers", "lubin01:9092,lubin02:9092,lubin03:9092");
        props.put("group.id", "test");
        props.put("enable.auto.commit", "true");
        props.put("auto.commit.interval.ms", "1000");
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        KafkaConsumer<String,String> consumer = new KafkaConsumer<String, String>(props);

        consumer.subscribe(Arrays.asList("test"));

        while(true){
            ConsumerRecords<String,String> records = consumer.poll(100);
            for(ConsumerRecord<String,String> record:records){
                System.out.printf("offset=%d, key=%s, value=%s%n",record.offset(),record.key(),record.value());
            }
        }
    }
}

3.2.2 手动提交offset

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

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

  1. 同步提交 offset
    由于同步提交 offset 有失败重试机制,故更加可靠,以下为同步提交 offset 的示例。
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

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

public class CustomConsumer {
    public static void main(String[] args){
        Properties props = new Properties();
        props.put("bootstrap.servers", "lubin01:9092,lubin02:9092,lubin03:9092");
        props.put("group.id", "test");
        props.put("enable.auto.commit", "false");
        props.put("key.deserializer",
                "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer",
                "org.apache.kafka.common.serialization.StringDeserializer");

        KafkaConsumer<String,String> consumer = new KafkaConsumer<String, String>(props);

        consumer.subscribe(Arrays.asList("test"));

        while(true){
            ConsumerRecords<String,String> records = consumer.poll(100);
            for(ConsumerRecord<String,String> record:records){
                System.out.printf("offset=%d, key=%s, value=%s%n",record.offset(),record.key(),record.value());
            }
            //同步提交,当前线程会阻塞直到 offset 提交成功
            consumer.commitSync();
        }
    }
}
  1. 异步提交 offset
    虽然同步提交 offset 更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会收到很大的影响。因此更多的情况下,会选用异步提交 offset 的方式,以下为异步提交 offset 的示例
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

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

public class CustomConsumer {
    public static void main(String[] args){
        Properties props = new Properties();
        props.put("bootstrap.servers", "lubin01:9092,lubin02:9092,lubin03:9092");
        props.put("group.id", "test");
        props.put("enable.auto.commit", "false");
        props.put("key.deserializer",
                "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer",
                "org.apache.kafka.common.serialization.StringDeserializer");

        KafkaConsumer<String,String> consumer = new KafkaConsumer<String, String>(props);

        consumer.subscribe(Arrays.asList("test"));

        while(true){
            ConsumerRecords<String,String> records = consumer.poll(100);
            for(ConsumerRecord<String,String> record:records){
                System.out.printf("offset=%d, key=%s, value=%s%n",record.offset(),record.key(),record.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,有可能会造成数据的重复消费。

3.2.3 自定义存储offset

Kafka 0.9 版本之前,offset 存储在 zookeeper,0.9 版本及之后,默认将 offset 存储在 Kafka的一个内置的 topic 中。除此之外,Kafka 还可以选择自定义存储 offset。

offset 的维护是相当繁琐的,因为需要考虑到消费者的 Rebalace。当有新的消费者加入消费者组、已有的消费者退出消费者组或者所订阅的主题的分区发生变化,就会触发到分区的重新分配,重新分配的过程叫做 Rebalance。

消费者发生 Rebalance 之后,每个消费者消费的分区就会发生变化。因此消费者要首先获取到自己被重新分配到的分区,并且定位到每个分区最近提交的 offset 位置继续消费。

要实现自定义存储 offset,需要借助 ConsumerRebalanceListener,以下为示例代码,其中提交和获取 offset 的方法,需要根据所选的 offset 存储系统自行实现。

import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
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.TopicPartition;

import java.util.*;

public class CustomConsumer2 {
    private static Map<TopicPartition, Long> currentOffset = new HashMap<>();

    public static void main(String[] args) {
        //创建配置信息
        Properties props = new Properties();

        //Kafka 集群
        props.put("bootstrap.servers", "lubin01:9092,lubin02:9092,lubin03:9092");

        //消费者组,只要 group.id 相同,就属于同一个消费者组
        props.put("group.id", "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> consumer = new
                KafkaConsumer<>(props);

        // 订阅消费主题
        consumer.subscribe(Arrays.asList("test"), new ConsumerRebalanceListener() {
            //该方法会在 Rebalance 之前调用
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> collection) {
                commitOffset(currentOffset);
            }

            //该方法会在 Rebalance 之后调用
            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> collection) {
                currentOffset.clear();
                for(TopicPartition partition: collection){
                    //定位到最近提交的 offset 位置继续消费
                    consumer.seek(partition,getOffset(partition));
                }
            }
        });

        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(100);
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("offset=%d, key=%s, value=%s%n", record.offset(), record.key(), record.value());
                currentOffset.put(new TopicPartition(record.topic(),record.partition()),record.offset());
            }
            //异步提交
            commitOffset(currentOffset);
        }
    }

    //获取某分区的最新 offset
    private static long getOffset(TopicPartition partition) {
        return 0;
    }
    //提交该消费者所有分区的 offset
    private static void commitOffset(Map<TopicPartition, Long> currentOffset) {
    }
}

3.3 自定义Interceptor

3.3.1 拦截器原理

Producer 拦截器(interceptor)是在 Kafka 0.10 版本被引入的,主要用于实现 client端的定制化逻辑。

对于 producer 而言,interceptor 使得用户在消息发送前以及 producer 回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。同时,producer 允许用户指定多个 interceptor按序作用于同一条消息从而形成一个拦截链(interceptor chain)。

Intercetpor 的实现接口是org.apache.kafka.clients.producer.ProducerInterceptor,其定义的方法包括:

  1. configure(configs)
    获取配置信息和初始化数据时调用。

  2. onSend(ProducerRecord):
    该方法封装于 KafkaProducer.send 方法中,即它运行在用户主线程中。在消息被序列化以及计算分区前调用该方法。用户可以在该方法中对消息做任何操作,但最好保证不要修改消息所属的 topic 和分区,否则会影响目标分区的计算。

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

  4. close:
    关闭 interceptor,主要用于执行一些资源清理工作

如前所述,interceptor 可能被运行在多个线程中,因此在具体实现时用户需要自行确保线程安全。另外倘若指定了多个 interceptor,则 producer 将按照指定顺序调用它们,并仅仅是捕获每个 interceptor 可能抛出的异常记录到错误日志中而非向上传递。这在使用过程中要特别留意。

3.3.2 拦截器案例

需求:实现一个简单的双 interceptor 组成的拦截链。第一个 interceptor 会在消息发送前将时间戳信息加到消息 value 的最前部;第二个 interceptor 会在消息发送后,更新成功发送消息数或失败发送消息数。
在这里插入图片描述

时间戳拦截器TimeInterceptor

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;

public class TimeInterceptor implements ProducerInterceptor<String, String> {
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) {
        // 创建一个新的 record,把时间戳写入消息体的最前部
        return new ProducerRecord<String, String>(producerRecord.topic(),
                producerRecord.partition(),
                producerRecord.timestamp(),
                producerRecord.key(),
                System.currentTimeMillis()+","+producerRecord.value().toString());
    }

    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
    }

    @Override
    public void close() {
    }

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

计数拦截器CounterInterceptor
统计发送消息成功和发送失败消息数,并在 producer 关闭时打印这两个计数器

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;

public class CounterInterceptor implements ProducerInterceptor<String, String> {

    private int errorCount = 0;
    private int successCount = 0;

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

    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
        // 统计成功和失败的次数
        if (e == null) {
            successCount++;
        } else {
            errorCount++;
        }
    }

    @Override
    public void close() {
        System.out.println("Successful send:"+successCount);
        System.out.println("Failed send:"+errorCount);
    }

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

Producer主程序InterceptorProducer

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

public class InterceptorProducer {

    public static void main(String[] args) {
        // 1 设置配置信息
        Properties props = new Properties();
        props.put("bootstrap.servers", "lubin01:9092,lubin02:9092,lubin03:9092");
        props.put("acks", "all");
        props.put("retries", 3);
        props.put("batch.size", 16384);
        props.put("linger.ms", 1);
        props.put("buffer.memory", 33554432);
        props.put("key.serializer",
                "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer",
                "org.apache.kafka.common.serialization.StringSerializer");

        // 2 构建拦截链
        List<String> interceptors = new ArrayList<>();

        interceptors.add("com.test.TimeInterceptor");
        interceptors.add("com.test.CounterInterceptor");

        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,interceptors);

        String topic = "test";

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

        // 3 发送消息
        for(int i =0;i<10;i++){
            ProducerRecord<String,String> record = new ProducerRecord<>(topic,"meassage"+i);
            producer.send(record);

        }

        // 4 一定要关闭 producer,这样才会调用 interceptor 的 close 方法
        producer.close();
    }
}

3.3.3 高级API和低级API优缺点

高级API优点

  • 高级API 写起来简单
  • 不需要自行去管理offset,系统自行管理。
  • 不需要管理分区,副本等情况,系统自动管理。
  • 消费者断线会自动根据上一次记录在zookeeper中的offset去接着获取数据(0.9.0.0版本之前,默认设置1分钟更新一下zookeeper中存的offset)
  • 可以使用group来区分对同一个topic 的不同程序访问分离开来(不同的group记录不同的offset,这样不同程序读取同一个topic才不会因为offset互相影响)

高级API缺点

  • 不能自行控制offset(对于某些特殊需求来说)
  • 不能细化控制,如分区、副本、zk等

低级 API 优点

  • 能够让开发者自己控制offset,想从哪里读取就从哪里读取。
  • 自行控制分区,对分区自定义进行负载均衡
  • 对zookeeper的依赖性降低(如:offset不一定要靠zk存储,自行存储offset即可,比如存在文件或者内存中)

低级API缺点
太过复杂,需要自行控制offset,连接哪个分区,找到分区leader 等。

3.4 实战案例

Kafka支持多种语言开发,该案例使用Java语言开发,一般都包含生产者(Producer)和消费者(Consumer)。

3.4.1 简单的生产者开发

新建项目
使用IDEA新建一个Maven项目。

添加依赖
在文件的project节点里面添加kafka依赖包和log4j2日志支持,内容如下

<dependencies>
    <!-- kafka -->
    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka_2.12</artifactId>
        <version>2.3.0</version>
    </dependency>

    <!--log4j2 日志 begin-->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.9.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.11.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.11.0</version>
    </dependency>
    <!--log4j2 日志 end-->
</dependencies>

在项目的src\main\resources目录下新建日志配置文件“log4j2.xml”,设置日志打印级别为“trace”方便调试,文件内容如下

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="trace">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

编码
在项目src\main\java目录下新建包“com.test.producer”,然后在包下新建测试类ProducerTest.java,内容如下

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.Properties;

public class ProducerTest {
    public static void main(String[] args) {
        Properties properties = new Properties();
        //配置kafka broker list的地址
        properties.put("bootstrap.servers", "es1:9092,es2:9092,es3:9092");
        //序列化 防止在转换的时候抛出异常
        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        properties.put("acks","-1");  //响应0不关心写入是否成功 1:leader成功 -1:都写入成功
        properties.put("retries",3);  //重试次数
        KafkaProducer<String,String> producer = new KafkaProducer<String, String>(properties);
        producer.send(new ProducerRecord<String, String>("test","1","first message"));
        producer.close();
    }
}

Producer发送消息的主要步骤

  1. 创建一个Properties对象,设置Producer需要的属性bootstrap.servers、value.serializer、key.serializer三个是必须设置的属性。
  2. 使用Properties对象创建KafkaProducer对象。
  3. 构造待发送的消息ProducerRecord,设置消息发送的主题topic,key和value。
  4. 调用KafkaProducer对象send方法,发送已构造的待发送消息。
  5. 关闭KafkaProducer释放资源。

过时API

import java.util.Properties;
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;

public class OldProducer {
	@SuppressWarnings("deprecation")
	public static void main(String[] args) {
		Properties properties = new Properties();
		properties.put("metadata.broker.list", "hadoop102:9092");
		properties.put("request.required.acks", "1");
		properties.put("serializer.class", "kafka.serializer.StringEncoder");
		
		Producer<Integer, String> producer = new Producer<Integer,String>(new ProducerConfig(properties));
		
		KeyedMessage<Integer, String> message = new KeyedMessage<Integer, String>("first", "hello world");
		producer.send(message );
	}
}

创建带回调函数的API
请见 Producer API 章节

测试
打开终端,使用Kafka自带脚本启动一个Consumer

bin/kafka-console-consumer.sh --bootstrap-server es1:9092,es2:9092,es3:9092 --topic test --from-beginning

运行程序ProducerTest.java,终端中会打印如下信息:

first message

发送到主题“test”中的消息成功被Consumer消费。

3.4.2 简单的消费者开发

修改配置
将项目中src\main\resource\log4j2.xml,日志打印级别设置为“info”,防止等待消费消息时一直打印日志。

编码
新建一个测试类ConsumerTest.java,代码如下:

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

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

public class ConsumerTest {
    public static void main(String[] args) {
        Properties properties = new Properties();
        //配置kafka broker list的地址
        properties.put("bootstrap.servers", "es1:9092,es2:9092,es3:9092");
        //序列化 防止在转换的时候抛出异常
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        //设置consumer  group
        properties.put("group.id","testgroup");
        properties.put("enable.auto.commit","true");
        properties.put("auto.commit.interval.ms","1000");
        properties.put("auto.offset.reset","earliest");

        KafkaConsumer<String,String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
        //订阅test主题
        kafkaConsumer.subscribe(Arrays.asList("test"));
        try {
            while (true) {
                ConsumerRecords<String, String> consumerRecords= kafkaConsumer.poll(1000);
                for(ConsumerRecord<String, String> record:consumerRecords){
                    System.out.println("offset="+record.offset()+", key="+record.key()+", value="+record.value());
                }
            }
        }finally {
            kafkaConsumer.close();
        }
    }
}

使用Consumer主要有六个步骤

  1. 创建一个Properties对象,设置consumer需要的属性bootstrap.servers、value.deserializer、key.deserializer、group.id。
  2. 使用Properties对象创建KafkaConsumer对象。
  3. 使用KafkaConsumer对象订阅主题“Topic”。
  4. 循环使用KafkaConsumer对象的poll方法获取主题“Topic”中的封装消息ConsumerRecord。
  5. 处理获取到的ConsumerRecord。
  6. 关闭KafkaConsumer。

测试
运行ConsumerTest.java,启动消费者等待接收消息。
然后运行ProducerTest.java,向Topic中发送消息。
ConsumerTest控制台中会打印相关信息

offset=2, key=1, value=message info

过时消费者API

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import kafka.consumer.Consumer;
import kafka.consumer.ConsumerConfig;
import kafka.consumer.ConsumerIterator;
import kafka.consumer.KafkaStream;
import kafka.javaapi.consumer.ConsumerConnector;

public class CustomConsumer {

	@SuppressWarnings("deprecation")
	public static void main(String[] args) {
		Properties properties = new Properties();
		
		properties.put("zookeeper.connect", "hadoop102:2181");
		properties.put("group.id", "g1");
		properties.put("zookeeper.session.timeout.ms", "500");
		properties.put("zookeeper.sync.time.ms", "250");
		properties.put("auto.commit.interval.ms", "1000");
		
		// 创建消费者连接器
		ConsumerConnector consumer = Consumer.createJavaConsumerConnector(new ConsumerConfig(properties));
		
		HashMap<String, Integer> topicCount = new HashMap<>();
		topicCount.put("first", 1);
		
		Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap = consumer.createMessageStreams(topicCount);
		
		KafkaStream<byte[], byte[]> stream = consumerMap.get("first").get(0);
		
		ConsumerIterator<byte[], byte[]> it = stream.iterator();
		
		while (it.hasNext()) {
			System.out.println(new String(it.next().message()));
		}
	}
}

3.4.3 自定义分区生产者

需求:将所有数据存储到topic的第0号分区上

  1. 定义一个类实现Partitioner接口,重写里面的方法(过时API)
import java.util.Map;
import kafka.producer.Partitioner;

public class CustomPartitioner implements Partitioner {

	public CustomPartitioner() {
		super();
	}

	@Override
	public int partition(Object key, int numPartitions) {
		// 控制分区
		return 0;
	}
}
  1. 自定义分区(新)
package com.atguigu.kafka;
import java.util.Map;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;

public class CustomPartitioner implements Partitioner {

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

	@Override
	public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 控制分区
		return 0;
	}

	@Override
	public void close() {
		
	}
}
  1. 在代码中调用
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;

public class PartitionerProducer {

	public static void main(String[] args) {
		
		Properties props = new Properties();
		// Kafka服务端的主机名和端口号
		props.put("bootstrap.servers", "hadoop103:9092");
		// 等待所有副本节点的应答
		props.put("acks", "all");
		// 消息发送最大尝试次数
		props.put("retries", 0);
		// 一批消息处理大小
		props.put("batch.size", 16384);
		// 增加服务端请求延时
		props.put("linger.ms", 1);
		// 发送缓存区内存大小
		props.put("buffer.memory", 33554432);
		// key序列化
		props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
		// value序列化
		props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
		// 自定义分区
		props.put("partitioner.class", "com.test.kafka.CustomPartitioner");

		Producer<String, String> producer = new KafkaProducer<>(props);
		producer.send(new ProducerRecord<String, String>("first", "1", "test"));

		producer.close();
	}
}
  1. 测试
    在hadoop102上监控/opt/module/kafka/logs/目录下first主题3个分区的log日志动态变化情况
[root@hadoop102 first-0]$ tail -f 00000000000000000000.log
[root@hadoop102 first-1]$ tail -f 00000000000000000000.log
[root@hadoop102 first-2]$ tail -f 00000000000000000000.log

发现数据都存储到指定的分区了。

3.4.4 SpringBoot集成Kafka

SpringBoot是现在Java开发主流框架之一,使用该框架可以简化开发流程,去掉复杂的配置,提高开发效率和降低应用的维护成本。

  1. 新建项目

新建Maven项目kafka-springboot,项目完整结构如下所示
在这里插入图片描述

  1. 添加依赖

修改pom.xml文件,添加SpringBoot和Kafka依赖,如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.oliver</groupId>
    <artifactId>kafka-springboot</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <version>2.2.8.RELEASE</version>
        </dependency>

        <!--log4j2 日志 begin-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.0</version>
        </dependency>
        <!--log4j2 日志 end-->
    </dependencies>
</project>
  1. 修改配置文件

项目src\main\resource\目录下新建log2j4.xml和application.properties文件,添加SpringBoot和Kafka配置信息,log2j4.xml文件参考上面的配置,application.properties如下:

server.port=8081
####################kafka server config #########################
spring.kafka.bootstrap-servers=es1:9092,es2:9092,es3:9092
##################producer config##############################
spring.kafka.producer.acks=1
spring.kafka.producer.batch-size=16384
spring.kafka.producer.retries=3
spring.kafka.producer.buffer-memory=33554432
##################consumer config##############################
spring.kafka.consumer.enable-auto-commit=true
spring.kafka.consumer.group-id=test_group
spring.kafka.consumer.auto-commit-interval=100
  1. 编码

生产者

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.ExecutionException;

@RestController
public class ProducerController {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    /***
     * 同步发送
     * @return
     */
    @RequestMapping("syncSend")
    public String syncSend() {
        for (int i = 0; i < 20; i++) {
            try {
                kafkaTemplate.send("test", "0", "message" + i).get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        return "success";
    }

    /***
     * 异步发送
     * @return
     */
    @RequestMapping("asyncSend")
    public String sendMessage() {
        for (int i = 0; i < 20; i++) {
            ListenableFuture<SendResult<String, String>> send = kafkaTemplate.send("test", "0", "message" + i);
            send.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {

                @Override
                public void onSuccess(SendResult<String, String> result) {
                    System.out.println("async send message success, partition=" +result.getRecordMetadata().partition());
                    System.out.println("async send message success,offest="+ result.getRecordMetadata().offset());
                }

                @Override
                public void onFailure(Throwable throwable) {
                    System.out.println("send message fail, "+throwable.getLocalizedMessage());
                }
            });
        }
        return "success";
    }
}

消费者

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Component
public class ConsumerService {
    @KafkaListener(id = "test", topics = "test")
    public void testListener(String test) {
        System.out.println("message="+test);
    }
}

应用入口

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}
  1. 测试

启动工程,运行App.java类。
测试同步发送
打开浏览器,输入地址:http://localhost:8081/syncSend

测试异步发送
打开浏览器,输入地址:http://localhost:8081/asyncSend

3.4.5 0.11.0.0版java api

添加依赖

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka_2.11</artifactId>
    <version>0.11.0.0</version>
</dependency>

Java Producer API

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
//ack方式,all会等所有的follower commit
props.put("acks", "all");
//失败是否重试,设置1会有可能产生重复数据
props.put("retries ", 1);
//整个producer可以用于buffer的内存大小
props.put("buffer.memory", 33554432);
//如果buffer没满,比如设为1,即消息发送会多1ms的延迟
props.put("linger.ms", 1);  
props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(props);
for(int i = 0; i < 100; i++)
    producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i)));
producer.close();

Java Consumer API

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
//不同ID 可以同时订阅消息
props.put("group.id", "test");
//是否自动commit
props.put("enable.auto.commit", "true");
//定时commit的周期
props.put("auto.commit.interval.ms", "1000");
//consumer活性超时时间
props.put("session.timeout.ms", "30000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("my-topic"));//订阅TOPIC
try {
    while(RUNNING) {//轮询
        ConsumerRecords<String, String> records =consumer.poll(Long.MAX_VALUE);//超时等待时间
        for (TopicPartition partition : records.partitions()) {
            List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
            for (ConsumerRecord<String, String> record : partitionRecords) {
                System.out.println(record.offset() + ": " + record.value());
            }
        }
    }
} finally {
  consumer.close();
}

手动提交offset

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "false"); //关闭自动commit
props.put("session.timeout.ms", "30000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
final int minBatchSize = 200;
List<ConsumerRecord<String, String>> buffer = new ArrayList<>();
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(100);
    for (ConsumerRecord<String, String> record : records) {
        buffer.add(record);
    }
    if (buffer.size() >= minBatchSize) {
        insertIntoDb(buffer); 
        consumer.commitSync(); //批量完成写入后,手工sync offset
        buffer.clear();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值