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
- 添加依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.7.1</version>
</dependency>
- 编写代码
- 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 则没有失败重试机制,故有可能提交失败。
- 同步提交 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();
}
}
}
- 异步提交 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
,其定义的方法包括:
-
configure(configs)
获取配置信息和初始化数据时调用。 -
onSend(ProducerRecord):
该方法封装于 KafkaProducer.send 方法中,即它运行在用户主线程中。在消息被序列化以及计算分区前调用该方法。用户可以在该方法中对消息做任何操作,但最好保证不要修改消息所属的 topic 和分区,否则会影响目标分区的计算。 -
onAcknowledgement(RecordMetadata, Exception):
该方法会在消息从 RecordAccumulator 成功发送到 Kafka Broker 之后,或者在发送失败时调用。并且通常都是在 producer 回调逻辑触发之前。onAcknowledgement 运行在producer 的 IO 线程中,因此不要在该方法中放入很复杂的逻辑,否则会拖慢 producer 的消息发送效率。 -
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发送消息的主要步骤
- 创建一个Properties对象,设置Producer需要的属性bootstrap.servers、value.serializer、key.serializer三个是必须设置的属性。
- 使用Properties对象创建KafkaProducer对象。
- 构造待发送的消息ProducerRecord,设置消息发送的主题topic,key和value。
- 调用KafkaProducer对象send方法,发送已构造的待发送消息。
- 关闭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主要有六个步骤
- 创建一个Properties对象,设置consumer需要的属性bootstrap.servers、value.deserializer、key.deserializer、group.id。
- 使用Properties对象创建KafkaConsumer对象。
- 使用KafkaConsumer对象订阅主题“Topic”。
- 循环使用KafkaConsumer对象的poll方法获取主题“Topic”中的封装消息ConsumerRecord。
- 处理获取到的ConsumerRecord。
- 关闭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号分区上
- 定义一个类实现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;
}
}
- 自定义分区(新)
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() {
}
}
- 在代码中调用
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();
}
}
- 测试
在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开发主流框架之一,使用该框架可以简化开发流程,去掉复杂的配置,提高开发效率和降低应用的维护成本。
- 新建项目
新建Maven项目kafka-springboot,项目完整结构如下所示
- 添加依赖
修改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>
- 修改配置文件
项目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
- 编码
生产者
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);
}
}
- 测试
启动工程,运行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();
}
}