Kafka的API
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.7.2</version>
</dependency>
Producer API
消息发送流程
Kafka的Producer发送消息采用异步发送的方式。在消息发送的过程中,涉及到了两个线程——main线程和Sender线程,以及一个线程共享变量——RecordAccumulator。 main线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker。
相关参数:
batch.size: 只有数据积累到batch.size之后,sender才会发送数据。
linger.ms: 如果数据迟迟未达到batch.size,sender等待linger.time之后就会发送数据。
-
编写代码
需要用到的类:
KafkaProducer:需要创建一个生产者对象,用来发送数据
ProducerConfig:获取所需的一系列配置参数
ProducerRecord:每条数据都要封装成一个ProducerRecord对象
最普通朴素的,不带回调函数的Producer,异步发送
package com.newbie.kafka.clients;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
/**
* @author yhj_newbie
* @create 2022-02-11 17:18
* @description
*/
public class JavaProducer {
public static void main(String[] args) {
Properties properties = new Properties();
//指定broker的地址
properties.put("bootstrap.servers", "Linux01:9092,Linux02:9092,Linux03:9092");
//指定写入数据key的序列化方式
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//指定写入数据value的序列化方式
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
//封装的发送数据
ProducerRecord<String, String> record1 = new ProducerRecord<String, String>("hellokafka", "hello kafka1");
kafkaProducer.send(record1);//不会立即发送,而是缓存在客户端中
ProducerRecord<String, String> record2 = new ProducerRecord<String, String>("hellokafka", "hello kafka2");
kafkaProducer.send(record2);
System.out.println(6666);
//最好在调用close之前,调用一次flush
kafkaProducer.flush();
//将kafkaProducer在close之前,会将缓存的数据flush到broker中
kafkaProducer.close();
}
}
注意:
-
kafkaProducer.send(),不会立即发送数据,而是缓存在客户端中。直到kafkaProducer.flush()或kafkaProducer.close()
Puducer的一些额外参数
//以下.setProperty,用.put替代亦可
//配置ACKS:数据同步应答
props.setProperty("acks", "1")
//配置重试次数
props.setProperty("retries", "100")
//设置压缩方式(不在kafka,而在客户端先压缩)
props.setProperty("compression", "snappy")
//设置客户端数据缓存的大小(从默认的32M改为100M)
props.setProperty("buffer.memory", 100 * 1024 * 1024 + "") //不能直接引数字,要的不是这串数字而是结果
//设置请求超时时间
props.setProperty("request.timeout.ms", 45000 + "")
//批次大小
props.put("batch.size", 16384);
//等待时间
props.put("linger.ms", 1);
带回调函数的自动提交Offset的Producer,异步发送
package com.newbie.kafka.clients;
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
/**
* @author yhj_newbie
* @create 2022-02-12 19:16
* @description
*/
public class test {
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)), 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();
}
}
-
回调函数会在producer收到ack时调用,为异步调用, 该方法有两个参数,分别是 RecordMetadata和Exception,如果Exception为null,说明消息发送成功,如果 Exception不为null,说明消息发送失败。
-
消息发送失败会自动重试,不需要我们在回调函数中手动重试。
Consumer API
Consumer消费数据时的可靠性是很容易保证的,因为数据在Kafka中是持久化的,不用担心数据丢失问题。
由于consumer在消费过程中可能会出现断电宕机等故障,consumer恢复后,需要从故障前的位置的继续消费,所以consumer需要实时记录自己消费到了哪个offset,以便故障恢复后继续消费,offset的维护是Consumer消费数据时必须考虑的问题。
编写代码
需要用到的类:
KafkaConsumer: 需要创建一个消费者对象,用来消费数据
ConsumerConfig: 获取所需的一系列配置参数
ConsuemrRecord: 每条数据都要封装成一个ConsumerRecord对象
为了使我们能够专注于自己的业务逻辑,Kafka提供了自动提交offset的功能。自动提交offset的相关参数:
enable.auto.commit: 是否开启自动提交offset功能
auto.commit.interval.ms: 自动提交offset的时间间隔
自动提交offset
最普通朴素的,自动提交offset的Consumer,长期监听版本,异步发送
package com.newbie.kafka.clients;
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.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Arrays;
/**
* @author yhj_newbie
* @create 2022-02-11 17:18
* @description
*/
public class JavaConsumer {
public static void main(String[] args) {
Properties properties = new Properties();
//指定kafka broker的地址
properties.setProperty("bootstrap.servers", "Linux01:9092,Linux02:9092,Linux03:9092");
//指定写入数据key的反序列化方式
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
//指定写入数据value的反序列化方式
properties.setProperty("value.deserializer", StringDeserializer.class.getName());
//指定消费者组id(必须的)
properties.setProperty("group.id", "g0001");
//指定从什么位置开始读取数据,默认是启动之后latest读数据
properties.setProperty("auto.offset.reset", "earliest");
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
//消费可以监听一到多个topic
// List<String> topics = Arrays.asList("test", "wordcount");
List<String> topics = Arrays.asList("hellokafka");
kafkaConsumer.subscribe(topics);
// //以下程序读完就退出了,我们希望持续监听数据
// ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(5));
//
// for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
// System.out.println(consumerRecord);
// }
while (true) {
//poll拉取数据等待的时长,五秒没数据就下一次拉取,有数据则拉取一个批次(有大小限制)
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(5));
//一次会拉取一个批次(0到多条数据)
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
System.out.println("88888888");
}
}
}
-
properties.setProperty("auto.offset.reset","earliest");
-
earliest:从头开始 相当于shell中的 --from-beginning,如果记录了偏移量(旧消费者组),则从偏移量开始读,新消费者组则从头读
-
latest:从消费者启动之后
-
默认是latest
-
-
List<String> topics = Arrays.asList("hellokafka"); 这里可以订阅不止一个topic
-
kafkaConsumer.poll(Duration.ofSeconds(5));
-
5秒内没数据拉取下一个批次,一个批次有0-n条数据
-
手动提交offset
props.setProperty("enable.auto.commit", "false")
虽然自动提交offset十分简介便利,但由于其是基于时间提交的, 开发人员难以把握 offset提交的时机。因此Kafka还提供了手动提交offset的API。
手动提交offset的方法有两种:分别是commitSync(同步提交)和commitAsync(异步提交)。
-
相同点:都会将本次poll的一批数据最高的偏移量提交;
-
不同点:commitSync阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而commitAsync新开一个单独线程,没有失败重试机制,故有可能提交失败。
普通手动提交offset的Consumer
package cn._51doit.kafka.clients
import java.time.Duration
import java.util
import java.util.Properties
import org.apache.kafka.clients.consumer.{ConsumerRecords, KafkaConsumer}
import org.apache.kafka.common.serialization.StringDeserializer
/**
* 消费者不自动提交偏移量,手动提交偏移量
*
*/
object ConsumerCommitOffsetDemo {
def main(args: Array[String]): Unit = {
// 1 配置参数
val props = new Properties()
//从哪些broker消费数据
props.setProperty("bootstrap.servers", "node-1.51doit.cn:9092,node-2.51doit.cn:9092,node-3.51doit.cn:9092")
// 反序列化的参数
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
props.setProperty("value.deserializer", classOf[StringDeserializer].getName)
// 指定group.id
props.setProperty("group.id", "g004")
// 指定消费的offset从哪里开始
//earliest:从头开始 --from-beginning
//latest:从消费者启动之后
props.setProperty("auto.offset.reset", "earliest") //[latest, earliest, none]
// 是否自动提交偏移量 offset
props.setProperty("enable.auto.commit", "false") // kafka自动维护偏移量 手动维护偏移量
//enable.auto.commit 5000
// 2 消费者的实例对象
val consumer: KafkaConsumer[String, String] = new KafkaConsumer[String, String](props)
// 订阅 参数类型 java的集合
val topic: util.List[String] = java.util.Arrays.asList("test")
// 3 订阅主题
consumer.subscribe(topic)
while (true) {
// 4 拉取数据
val msgs: ConsumerRecords[String, String] = consumer.poll(Duration.ofMillis(2000))
//新的方式
import scala.collection.JavaConverters._
for (cr <- msgs.asScala) {
println(cr)
}
if (!msgs.isEmpty) {//改进:有新的数据才提交偏移量
//提交偏移量
//异步提交偏移量,会启动一个单独的线程完成提交偏移量,主程序可以进行执行循环
consumer.commitAsync()
//同步提交,必须写入到kafka的特殊的topic中的偏移量成功后,才会进行下一次循环
//consumer.commitSync()
}
}
//consumer.close()
}
}
带回调函数的Consumer1(主体与上方代码块相同)
//在提交后,用回调函数判断成功与否执行另外一些操作
while (true) {
// 4 拉取数据
val msgs: ConsumerRecords[String, String] = consumer.poll(Duration.ofMillis(2000))
//新的方式
import scala.collection.JavaConverters._
for (cr <- msgs.asScala) {
println(cr)
}
if (!msgs.isEmpty) {
//提交偏移量
consumer.commitAsync(new OffsetCommitCallback {
override def onComplete(map: util.Map[TopicPartition, OffsetAndMetadata], e: Exception): Unit = {
//提交成功后可以做一些操作
if (e == null) {
println("偏移量提交成功")
} else if (e.isInstanceOf[CommitFailedException]) {
println("提交偏移量失败")
} else {
println("其他异常")
}
}
})
}
}
带回调函数的Consumer2(主体与上方代码块相同)
// 提交指定的偏移量到Kafka特殊的topic中,这里以取最大偏移量为例
// 偏移量由:group.id, topic, partition -> offset
while (true) {
// 4 拉取数据
val msgs: ConsumerRecords[String, String] = consumer.poll(Duration.ofMillis(2000))
//新的方式
import scala.collection.JavaConverters._
val consumerRecords: Iterable[ConsumerRecord[String, String]] = msgs.asScala
for (cr <- consumerRecords) {
println(cr)
}
if (!msgs.isEmpty) {
//(("test", 1), 21)
//(("test", 1), 22)
//(("test", 2), 15)
//(("test", 2), 13)
//(("test", 2), 14)
//(("test", 0), 17)
val offsets: Iterable[((String, Int), Long)] = consumerRecords.map(record => ((record.topic(), record.partition()), record.offset()))
//获取每个topic、每个分区,最大的偏移量
val maxOffsetInPartition: Iterable[((String, Int), Long)] = offsets.groupBy(_._1).map{
case(_, v) => {
v.maxBy(_._2)
}
}
//将数据整理成 : Map[TopicPartition, OffsetAndMetadata]
val offsetMap: Map[TopicPartition, OffsetAndMetadata] = maxOffsetInPartition.map(t => {
(new TopicPartition(t._1._1, t._1._2), new OffsetAndMetadata(t._2, null))
}).toMap
println(offsetMap.toBuffer)
//提交偏移量
consumer.commitSync(offsetMap.asJava)
}
}