Kafka快速入门三:Kafka API使用

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实现。

多个分区的情况下,每个分区都有自己的offsetcommitSync/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();
	}
}

 

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值