1.Producer API
1.1Producer结构及发送消息流程
- Producer结构
Producer由main和sender两个线程,以及一个线程共享变量RecordAccumulator组成。
其中main线程有三个器:拦截器、序列化器、分区器。
如下图所示,其中topic属于kafka server。
- Producer消息发送流程
如上图所示,步骤如下:
①main线程将消息发送给RecordAccumulator;
②Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker
Sender线程多久从RecordAccumulator拉取一次呢?引入两个参数:
batch.size:只有RecordAccumulator中数据积累到batch.size之后,sender才会发送数据。
linger.ms:如果数据迟迟未达到batch.size,sender等待linger.time之后就会发送数据。
1.2异步发送API
先介绍同步发送和异步发送的区别:
同步发送的意思就是,一条消息发送之后,会阻塞当前线程,直至返回ack。
异步发送的意思是,一条消息发送之后,直接发送下一条消息,不等待ack。
其中异步发送又分为不带回调的API和带回调的API。
- 不带回调的API
①导入依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
②编写代码
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "hadoop102:9092");//kafka集群,broker-list
props.put("acks", "all");
props.put("retries", 1);//重试次数
props.put("batch.size", 16384);//批次大小
props.put("linger.ms", 1);//等待时间
props.put("buffer.memory", 33554432);//RecordAccumulator缓冲区大小
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++) {
//调用send发送消息
producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i)));
}
//关闭生产者
producer.close();
}
}
步骤如下:
①创建生产者
②调用send发送消息
③关闭生产者。
其中创建Producer参数必须设置的有3个:
bootstap.servers:kafka集群列表;
key.serializer:key的序列化器;
value.serializer:value的序列化器;
- 带回调的API
带回调的API也是调用Producer.send方法,多了一个参数--Callback对象,在onCompletion方法中定义callback的操作。
回调函数会在producer收到ack时调用,为异步调用,onCompletion方法有两个参数RecordMetadata和Exception,如果Exception为null,说明消息发送成功,如果Exception不为null,说明消息发送失败。
代码如下:
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "hadoop102:9092");//kafka集群,broker-list
props.put("acks", "all");//acks机制,all是-1
props.put("retries", 1);//重试次数
props.put("batch.size", 16384);//批次大小
props.put("linger.ms", 1);//等待时间
props.put("buffer.memory", 33554432);//RecordAccumulator缓冲区大小
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>("first", Integer.toString(i), Integer.toString(i)), new Callback() {
//回调函数,该方法会在Producer收到ack时调用,为异步调用
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
System.out.println("success->" + metadata.offset());
} else {
exception.printStackTrace();
}
}
});
}
producer.close();
}
}
1.3同步发送API
同步发送就是一条消息发送之后,会阻塞当前线程,直至返回ack。同步发送效率较低,用的不多。
其实现方式就是在Producer.send方法后加get(),因为Producer.send返回类型为Future,Future.get可以阻塞线程。
ps:java可选用ExecutorService实现线程池。
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;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Properties props = new Properties();
props.put("bootstrap.servers", "hadoop102:9092");//kafka集群,broker-list
props.put("acks", "all");
props.put("retries", 1);//重试次数
props.put("batch.size", 16384);//批次大小
props.put("linger.ms", 1);//等待时间
props.put("buffer.memory", 33554432);//RecordAccumulator缓冲区大小
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>("first", Integer.toString(i), Integer.toString(i))).get();
}
producer.close();
}
}
2.Consumer API
消费者最重要的API当然是消费消息,流程为①KafkaConsumer对象;②调用consumer.subscribe订阅;③调用consumer.poll拉取消息;④如果是手动提交offset,则调用consumer.commit接口。
Consumer技术含量最高的部分则是维护offset,Kafka数据在kafka中是持久化的,故不用担心数据丢失问题。但由于consumer在消费过程中可能会出现断电宕机等故障,consumer恢复后,需要从故障前的位置的继续消费,所以consumer需要实时记录自己消费到了哪个offset,以便故障恢复后继续消费。
维护offset方案有三种:
自动提交offset、手动提交offset和自定义存储offset。
- 自动提交offset
自动提交方式,offset保存在kafka内部topic,提交方式是定期提交。实现方式:enable.auto.commit设置为true,同时要设置auto.commit.interval.ms每隔多少毫秒提交一次。
因为是定期提交,如果消费之后没来得及提交offset就宕机,就会出现重复消费的现象。
- 手动提交offset
手动提交方式,offset也是保存在kafka内部topic,通过代码调用接口控制提交时机。实现方式:enable.auto.commit设置为false,代码中调用consumer.commitSync或consumer.commitASync实现。
多个分区的情况下,每个分区都有自己的offset,commitSync/commitASync就是封装了所有分区的offset维护。
- 自定义存储offset
自定义存储可以将offset保存在HBase或Redis中,自定义存储才能保证offset的Execatly-once,因为可以保证kafka数据消费和存储offset原子性。
2.1手动提交offset
- 代码实现
第一步:导入依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
第二步:编写代码
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", "hadoop102:9092");
props.put("group.id", "test");//消费者组,只要group.id相同,就属于同一个消费者组
props.put("enable.auto.commit", "false");//自动提交offset
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("first"));
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.commitSync();
}
}
代码步骤如下:
①创建KafkaConsumer对象;
②调用consumer.subscribe订阅;
③调用consumer.poll拉取消息;
④调用consumer.commitSync接口手动提交offset。
拉取的消息数据保存在ConsumerRecords对象中。
介绍几个参数:
group.id:消费者组id,比如先起1个consumer对象,所有分区都给这个consumer,这时又起一个相同group.id的consumer,会发生分区重新分配rebalance,这样就两个consumer对象共同消费。
auto.offset.reset :定义没有初始offset的情况下,该从哪读。earliest最早、latest最新(默认)。
同步提交和异步提交
手动提交offset的方法有两种:分别是commitSync(同步提交)和commitAsync(异步提交)。两者的相同点是,都会将本次poll的一批数据最高的偏移量提交;不同点是,commitSync会阻塞发送进行失败重试,一直到提交成功(如果由于不可恢复原因导致,也会提交失败);而commitAsync则没有失败重试机制,故有可能提交失败(因为异步提交如果重试,可能之前失败的覆盖最新成果的offset),一般使用异步。
- 可能存在的问题
先消费后提交offset,可能会出现消费成功但是提交失败,造成重复消费;
先提交offset后消费,可能会造成提交成功但是消费失败,造成数据丢失。
2.2自动提交offset
自动提交前面已介绍,最终的是两个参数设置:enable.auto.commit设置为true,auto.commit.interval.ms设置定期提交间隔。
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", "hadoop102: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<>(props);
consumer.subscribe(Arrays.asList("first"));
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.自定义拦截器Interceptor
3.1拦截器的作用
拦截器是Producer的特性,是在Kafka 0.10版本被引入的。
顾名思义,拦截器就是在Kakfa消息发送过程中,将消息拦截下来,根据业务需要对消息内容进行修改,然后再发送出去。
3.2拦截器的使用
- 使用步骤
拦截器是Producer的特性,所以自然是在Producer发送消息时使用,使用也很简单:①首先implements ProducerInterceptor定义自己的拦截器;②创建Producer时在Properties中设置拦截器列表。
- 4大方法
实现ProducerInterceptor需要实现4个方法:
(1)configure(configs)
获取配置信息和初始化数据时调用,用的不多。
(2)onSend((ProducerRecord)
发送之前调用,可以在该方法中对消息做任何操作,但最好保证不要修改消息所属的topic和分区,否则会影响目标分区的计算。
(3)onAcknowledgement(RecordMetadata, Exception)
在消息成功发送到Kafka Broker之后收到ack之前,或者在发送过程中失败时调用,不要在该方法中放入很重的逻辑,否则会拖慢producer的消息发送效率。
(4)close
关闭interceptor时调用,而在Producer关闭时才会关闭interceptor,所以Producer.close()时才会调用到,主要用于执行一些资源清理工作。
- 案例
实现一个简单的双interceptor组成的拦截链。第一个interceptor会在消息发送前将时间戳信息加到消息value的最前部;第二个interceptor会在消息发送后更新成功发送消息数或失败发送消息数。
时间戳拦截器:
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class TimeInterceptor implements ProducerInterceptor<String, String> {
@Override
public void configure(Map<String, ?> configs) {
}
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
// 创建一个新的record,把时间戳写入消息体的最前部
return new ProducerRecord(record.topic(), record.partition(), record.timestamp(), record.key(),
System.currentTimeMillis() + "," + record.value().toString());
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override
public void close() {
}
}
成功率统计拦截器:
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class CounterInterceptor implements ProducerInterceptor<String, String>{
private int errorCounter = 0;
private int successCounter = 0;
@Override
public void configure(Map<String, ?> configs) {
}
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
return record;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
// 统计成功和失败的次数
if (exception == null) {
successCounter++;
} else {
errorCounter++;
}
}
@Override
public void close() {
// 保存结果
System.out.println("Successful sent: " + successCounter);
System.out.println("Failed sent: " + errorCounter);
}
}
Producer主程序:
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
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;
public class InterceptorProducer {
public static void main(String[] args) throws Exception {
// 1 设置配置信息
Properties props = new Properties();
props.put("bootstrap.servers", "hadoop102:9092");
props.put("acks", "all");
props.put("retries", 0);
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.atguigu.kafka.interceptor.TimeInterceptor"); interceptors.add("com.atguigu.kafka.interceptor.CounterInterceptor");
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
String topic = "first";
Producer<String, String> producer = new KafkaProducer<>(props);
// 3 发送消息
for (int i = 0; i < 10; i++) {
ProducerRecord<String, String> record = new ProducerRecord<>(topic, "message" + i);
producer.send(record);
}
// 4 一定要关闭producer,这样才会调用interceptor的close方法
producer.close();
}
}