5 Kafka API
5.1 Producer API
5.1.1 消息发送流程
Kafka的Producer发送消息采用异步发送。
ack只保证数据丢不丢和重复不重复的问题,并不会考虑同步还是异步发送。
producer发送一批消息,发送给leader后,不用等接收到ack;就可以发下一批消息。
而同步发送是:
producer发送一批消息,发送给leader后,需要ISR内的所有follower接收到,并producer接收到ack,再发送下一批消息。会阻塞当前的线程。
涉及到两个线程:main线程、sender线程,一个共享变量RecordAccumulator。
main线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker
5.1.2 异步发送API
步骤1:导入导入kafkajar包。
<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>
步骤2:添加log4j配置文件,文件名为log4j2.xml
<?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>
步骤3:编写代码
- KafkaProducer:创建生产者对象,用来发送数据。
- ProducerConfig:获取所需的一系列配置参数。
- ProducerRecord:每条数据都要封装成一个ProducerRecord对象。
不带回调函数的API
public class MyProducer2 {
public static void main(String[] args) {
//1 创建配置文件对象
Properties properties = new Properties();
//1.1 连接Kafka集群
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
//1.2 设置key序列化的类型
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
//1.3 设置value序列化的类型
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
//1.4 设置ack模式,默认是-1(all)
properties.put(ProducerConfig.ACKS_CONFIG, "all");
//1.5 设置重试次数
properties.put(ProducerConfig.RETRIES_CONFIG, 1);
//1.6 设置批次大小
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
//1.7 等待时间
properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);
//1.8 缓冲区大小
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
//2 创建生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
//3 生产数据
for (int i = 0; i < 10; i++) {
producer.send(new ProducerRecord<String, String>("WEI", Integer.toString(i), Integer.toString(i)));
}
//4 关闭生产者
producer.close();
}
}
带回调函数的API
回调函数会在producer收到ack时调用,为异步调用。有两个参数RecordMetadata和Exception,如果Exception为null,说明消息发送成功,如果Exception不为null,说明消息发送失败。
- 注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试
public class MyProducer {
public static void main(String[] args) {
//1 创建配置文件对象
Properties properties = new Properties();
//1.1 Kafka集群,broker-list
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
//1.2 设置key-value序列化的类型
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
//2 创建Kafka的生产者,需要传一个配置文件对象
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
//3 生产数据
for (int i = 0; i < 10; i++) {
//3.1 发送数据,需要new一个ProducerRecord对象;同时创建一个回调函数,可以改写为lambda表达式形式!
producer.send(new ProducerRecord<>("WEI","sun--" + i), new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null){
System.out.println(metadata.partition() + "--" + metadata.offset());
}else {
exception.printStackTrace();
}
}
});
}
//4 关闭资源
producer.close();
}
}
5.1.2 同步发送API
同步发送的意思是,一条消息发送后会阻塞当前线程,直至返回ack后,这样效率就慢!相比异步发送,不管受不受到ack就可以发送下一条消息。
send()方法返回的是一个Future对象,调用Future对象的get()方法就可以实现同步发送。
public class MySyncProducer {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1 创建配置文件对象
Properties properties = new Properties();
//1.1 kafka集群
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
//2 创建kafka的producer
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
//3 生产数据
for (int i = 0; i < 10; i++) {
producer.send(new ProducerRecord<>("WEI", "ji--" + i)).get();
}
//4 关闭producer
producer.close();
}
}
5.1.3 分区器
默认分区器:
自定义分区器:
public class MyPartitioner implements Partitioner {
/*
* @Description //TODO 自定义分区的逻辑
* @Param [topic, key, keyBytes, value, valueBytes, cluster]
* @return int
**/
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
Random random = new Random();
int i = random.nextInt(2);
return i;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
然后在Producer内设置properties.put()
//指定自定义分区
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, "com.codejiwei.producer.MyPartitioner");
5.2 Consumer API
因为数据在Kafka中是持久化的所以不用担心数据丢失问题,但是如果consumer挂掉了,offset没有更新的话,下次消费就还会重复消费。所以consumer需要考虑offset的维护。
普通消费者
消费和生产都用BOOTSTRAP_SERVERS_CONFIG, “hadoop102:9092”
自己定义的consumer必须要声明自己是哪个组的,在命令行中没有声明是哪个组的是因为配置文件内
需要消费者需要将磁盘内的序列化数据反序列化读出。
消费者订阅主题,可以订阅多个也可以订阅一个,所以subscribe()内放的是一个Collection
①自动提交offset
默认自动提交;默认auto.commit.interval.ms = 5000ms
如果我enable.auto.commit=false,那么offset一直都没有提交,上一次还是从上一个offset开始消费。
public class MyConsumer {
public static void main(String[] args) {
//1 创建properties对此昂
Properties properties = new Properties();
//kafka集群
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
//开启自动提交(默认就是自动提交)
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
//自动提交延时(默认是5秒)
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
//key-value的反序列化
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization" +
".StringDeserializer");
// properties.put(ConsumerConfig.GROUP_ID_CONFIG, "jiwei");
//2 创建kafka消费者对象
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
//3 订阅主题
//可以订阅多个主题,如果是一个:
// consumer.subscribe(Arrays.asList("WEI"));
consumer.subscribe(Collections.singletonList("WEI"));
//可以订阅多个主题,如果是多个:
// consumer.subscribe(Arrays.asList("WEI", "AA"));
while (true) {
//4 获取数据(批量获取)
ConsumerRecords<String, String> poll = consumer.poll(100);
//解析并打印consumerRecords
for (ConsumerRecord<String, String> record : poll) {
//key也是要存的到kafka的
System.out.println(record.key() + "--" + record.value());
}
}
// 关闭消费者连接(死循环了就不用关闭了,如果要关就kill)
}
}
②重置Offset
auto.offset.reset = earliest | latest | none |
# earliest是从头开始,但注意不是0,因为最小的不一定是0
# latest(默认)从当前最大的开始
消费者offset重置的只有在两个特定的场景下才会生效:
- 消费者组第一次消费的时候。
- 当前的offset过期了。
java的配置文件中添加:
//重置消费者的offset
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
注意要换一个组!才能消费到从头开始的数据。(前提要auto.offset.reset = earliest)
③手动提交offset
虽然自动提交offset很便利,但是它是基于auto.commit.interval.ms时间来提交的。
手动提交offset有两种方式:
两种方式都会将本次poll的一批数据最高的offset提交。
- commitSync(同步提交):一直提交,如果提交不了就重试,直到提交成功,提交失败会重试(也可能会提交失败)
- commitAsync(异步提交):没有失败重试,可能提交失败。
虽然同步提交可靠性更高,但是效率低;更多的情况下会选用异步提高offset的方法。
同步提交offset
public class MyConsumer2 {
public static void main(String[] args) {
//1 创建properties对象
Properties properties = new Properties();
// 连接到kafka集群
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
// 关闭自动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
// 添加到组:ji1中
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "ji1");
// 设置key-value反序列化的类型
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
//2 创建consumer
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
//3 订阅主题
List topics = new ArrayList();
topics.add("WEI");
consumer.subscribe(topics);
//4 消费主题
while (true){
ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord.topic() + "--"+consumerRecord.partition()+"--"+consumerRecord.offset());
}
// 同步提交
consumer.commitSync();
}
}
}
异步提交offset(默认就是异步提交)
public class MyASyncConsumer {
public static void main(String[] args) {
//1 创建properties对象
Properties properties = new Properties();
//kafka集群
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
//设置消费者组
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "code");
//关闭自动提交offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
//设置key-value反序列化类型
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
//2 创建kafka的consumer对象
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
//3 订阅主题
consumer.subscribe(Collections.singletonList("WEI"));
//4 消费订阅的数据
while (true){
//每隔0.1秒拉取数据,注意拉取是成批拉取的。
ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord.topic() + "--" + consumerRecord.partition() + "--" + consumerRecord.value() + "--" + consumerRecord.offset());
}
//拉取完数据后要提交offset
//异步提交
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
//如果exception不为null,说明提交失败。
if (exception != null){
System.err.println("Commit failed for " + offsets);
}
}
});
}
}
}
无论是同步消费还是异步消费,都有可能出现数据的漏消费或者重复消费,先提交offset后消费,可能数据漏消费;先消费后提交offset,可能数据重复消费。
5.3 自定义Interceptor
拦截器所在的位置:
拦截器链一定是在producer端,producer —> interceptors —> serializer —> partitioner
自定义拦截器需要实现Kafka提供的ProducerInterceptor接口。
onSend()和onAcknowledgement()是被循环调用的。
public class TimeInterceptor implements ProducerInterceptor<String, String> {
@Override//对record的具体操作,比如给value加点东西,对record进行过滤等
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
return null;
}
@Override//根据返回ack的exception是否为null判断是否发送成功。可以用来统计发送成功数量
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override//关闭资源或者输出结果的一些操作
public void close() {
}
@Override//配置文件
public void configure(Map<String, ?> configs) {
}
}
①自定义拦截器1
给发送的数据添加一个时间戳
public class TimeInterceptor implements ProducerInterceptor<String, String> {
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
//1 取出数据
String value = record.value();
//2 因为record没有set方法,所以需要创建一个新的ProducerRecord对象
ProducerRecord<String, String> record1 = new ProducerRecord<>("WEI", System.currentTimeMillis() + value);
return record1;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
②统计发送消息成功和发送失败消息数
public class CountInterceptor implements ProducerInterceptor<String, String> {
int success;
int error;
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {//虽然不需要对record操作,但是需要将record返回
return record;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
if (exception == null){
success++;
}else {
error++;
}
}
@Override//为什么要在close中输出呢?因为上面的两个方法是被循环调用的,最后的结果应该在关闭时候
public void close() {
System.out.println("success--" + success);
System.out.println("error--" + error);
}
@Override
public void configure(Map<String, ?> configs) {
}
}
③配置拦截器链
// //设置一个拦截器
// properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, "com.codejiwei.interceptor.TimeInterceptor");
//设置拦截器链
ArrayList<String> interceptors = new ArrayList<>();
interceptors.add("com.codejiwei.interceptor.TimeInterceptor");
interceptors.add("com.codejiwei.interceptor.CountInterceptor");
properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);