依赖
<!--kafka依赖-->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
Producer api
package com.kafka.producer;
import org.apache.kafka.clients.producer.*;
import java.util.Arrays;
import java.util.Properties;
/**
* Producer发送数据是异步发送
* 涉及到两个线程main线程和sender线程、和一个线程贡献变量RecordAccumulator,线程共享变量存放待发送的数据。
* Producer发送消息也可以同步发送,实际上就是用producer.send返回的对象调用get方法,get方法会阻塞当前的main线程。
* 选择同步发送并将topic设置为一个分区,可以保证topic内数据绝对有序。 同步发送基本不用。
* 发送数据流程:
* main线程中Producer调用send发送数据,数据依次经过拦截器、序列化器、分区器后进入RecordAccumulator,
* sender线程主动来RecordAccumulator中取数据。
* “batch.size”:Producer发送消息是按批次发送的,每批次大小该参数来指定。
* “linger.ms”:数据量很小的时候可能很久都达不到批次大小,故可设置该参数来指定发送的时间间隔。
* 发送指的是写入到RecoderAccumulator缓存区。
* “acks”:kafka 的ack应答级别,可以时0、1、all(相当于-1) 。
* "partitioner.class":指定自定义分区器的绝对路径。
* "interceptor.classes":指定自定义拦截器,传入一个集合,先生效的拦截器写在集合前面。
* 生产者中有一个ProducerConfig类,消费者中有ConsumerConfig类,这两个类中都定义了很多常量,对应kafka的所有配置。
* 发送消息:
* producer.send(new ProducerRecord<>("topic名"[[,分区号],key],"消息")[,new Callback(){....}]);
* 代码最后需要关闭producer:producer.close();
*/
public class MyProducer {
public static void main(String[] args) {
//创建kafka生产者
Properties ps = new Properties();
ps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.149.131:9092");//Kafka服务端的主机名和端口号
ps.put(ProducerConfig.ACKS_CONFIG, "all"); // 等待所有副本节点的应答
ps.put(ProducerConfig.RETRIES_CONFIG, 3); // 消息发送最大尝试次数
ps.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); // 一批次发送消息的大小16K,这个发送是指写入RecoderAccumulator缓存区
ps.put(ProducerConfig.LINGER_MS_CONFIG, 1); // 即使1ms内未达到batch.size也会发送数据
ps.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); // producer端RecoderAccumulator缓存区内存大小
ps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); // key序列化
ps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");// value序列化
ps.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.kafka.partitioner.MyPartitioner");//自定义分区器
ps.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, //自定义拦截器,写前面的拦截器先生效
Arrays.asList("com.kafka.interceptor.AddTimestampInterceptor", "com.kafka.interceptor.CountValueInterceptor"));
//创建producer对象
KafkaProducer<String, String> producer = new KafkaProducer<>(ps);
//发送数据
for (int i = 0; i < 10; i++) {
/**
* 不带回调函数发送
* producer.send(new ProducerRecord<>("producer","生产数据---"+i));
*/
/**
* 带回调函数发送消息
* 发送成功返回元信息,通过元信息可以打印分区、offset等信息
* 发送失败返回异常信息
* ProducerRecord有很多重载的构造器,可以指定消息发送的分区、也可以指定消息的key
* ProducerRecord有三个常用构造器
* ProducerRecord(topic,value) 指定topic和消息
* ProducerRecord(topic,key,value) 指定topic、分区key和消息
* ProducerRecord(topic,partition,key,value) 指定topic、分区、分区key和消息(分区key不生效)
*/
producer.send(new ProducerRecord<>("prod_ucer","partition_1","生产数据----" + i), new Callback() {
@Override
public void onCompletion(RecordMetadata meta, Exception e) {
if(meta != null)
System.out.println("分区:"+meta.partition()+"-----offset:"+meta.offset());
else
e.printStackTrace();
}
});
}
//关流
producer.close();
}
}
Consumer api
package com.kafka.consumer;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;
/**
*
* 只有当找不到任何消费的offset且消费策略是earliest的时候,才会从最开始的地方消费。
* 找不到任何消费的offset的两种情况:1.使用了全新的消费者组。2.数据消费发生在7天前,已经清除了数据。
* 一个消费者组可以订阅多个不同的topic,但为了防止数据混乱,生产开发强制一个消费者组只能订阅一个topic。
* 所谓的offset自动提交,其实就是每隔一段时间自动将内存中的offset保存到名为“__consumer_offsets”的topic中,
* 消费者只有在刚启动时才会去该topic中获取offset。其他时刻直接通过内存中的offset读取数据。
* kafka的消费者拉取数据也是一批一批的拉取,拉取完了之后立马开始消费,
* 如果没有消费完就自动提交了offset,此时宕机重启,就丢数据了。
* 如果消费完了没有自动提交offset,此时提交宕机重启,就重复消费了。
* 所以自动提交时提交的时间间隔就很重要了。可是开发人员又很难把握这个时间间隔到底设置为多少。所以开发人员一般手动提交offset。
* 所谓的offset手动提交,其实就是在消费完成后或者消费开始前手动调用提交offset的方法将内存中的offset保存到名为“__consumer_offsets”的topic。
* 手动提交分为同步提交"consumer.commitAsync();"和异步提交"consumer.commitAsync(new OffsetCommitCallback() {...})",
* 同步提交会阻塞当前线程不断重试直到提交成功(用得少)。异步提交不会重试提交,效率更高。
* 如果先消费然后手动提交offset未完成,宕机重启可能会造成重复消费。
* 如果先手动提交offset然后消费未完成,宕机重启可能会造成数据丢失。
* 所以自动提交和手动提交都不靠谱,kafka提供了第三种提交offset的方式:即自定义存储offset。
* 自定义存储offset时需要考虑到消费者组rebalance后带来的消费者消费数据offset变化的问题。
*
*/
public class MyConsumer {
public static void main(String[] args) {
//设置属性
Properties ps = new Properties();
ps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.149.131:9092");//Kafka服务端的主机名和端口号
//ps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");//开启自动提交
//ps.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");//每一秒自动提交一次offset
ps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");//关闭自动提交
ps.put(ConsumerConfig.GROUP_ID_CONFIG,"my_consumer");//消费者组
ps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");//消费策略,默认是latest
ps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");//KEY反序列化
ps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");//VALUE反序列化
//创建consumer对象和订阅主题
KafkaConsumer<String,String> consumer = new KafkaConsumer<>(ps);
consumer.subscribe(Collections.singletonList("first_topic"));//生产强制订阅一个主题,否则可能消费数据混乱
//不停地拉取数据并消费数据,每次拉取的是一批数据,而不是一条
while(true){
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1).toMillis());//拉取数据并设置超时时间
consumerRecords.forEach(
recode ->{
System.out.println("topic:" + recode.topic() +
"partition:" + recode.partition() +
"offset:" + recode.offset());
}
);
//consumer.commitSync();//同步提交,会阻塞当前线程,不断重试提交直到成功为止
consumer.commitAsync(new OffsetCommitCallback() {//手动提交中的异步提交,带回调函数
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
if(e != null){
System.out.print("offset提交失败:");
e.printStackTrace();
}
}
});
}
//拉取和消费不停,所以无需关闭consumer对象
}
}
自定义拦截器
我们定义一条拦截器链,该拦截器链由两个拦截器组成。
第一个拦截器负责在消息的头部追加一个时间戳。
第二个拦截器负责统计发送给broker成功和失败的消息总数。
package com.kafka.interceptor;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.header.Headers;
import java.util.Map;
/**
* 本类是kafka自定义拦截器:用于在消息头部追加一个当前系统时间
* 拦截器:
* 用户在消息发送前以及 producer 回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。
* 多个拦截器作用于同一条消息从而形成一个拦截链。
* 拦截器可能运行在多个线程中,线程安全需要用户自己去保证。
*/
public class AddTimestampInterceptor implements ProducerInterceptor {
/**
*获取配置信息和初始化数据时调用
* @param map
*/
@Override
public void configure(Map<String, ?> map) {
}
/**
* 序列化及计算分区前调用,用户可以在该方法内对消息做任何更改。即使不更改请将消息原样返回。
* 运行在producer的主线程中。
* 由于ProducerRecord中所有属性均为final,无法更改,所以改变value后需要改回一个新的ProducerRecord对象
* @param producerRecord 消息记录
* @return
*/
@Override
public ProducerRecord onSend(ProducerRecord producerRecord) {
String topic = producerRecord.topic();
Integer partition = producerRecord.partition();
Headers headers = producerRecord.headers();
Object key = producerRecord.key();
Object value = producerRecord.value();
Long timestamp = producerRecord.timestamp();
return new ProducerRecord(topic,partition,timestamp,key,System.currentTimeMillis()+","+value,headers);
}
/**
* 在消息成功发送给broker或发送失败时调用
* 由于该方法在producer的io线程中调用,故不宜书写过重的逻辑,以免影响消息发送速度
* @param recordMetadata 消息发送成功返回消息的元信息
* @param e 消息发送失败返回异常
*/
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
}
/**
*关闭拦截器,做一些资源的清理工作
*/
@Override
public void close() {
}
}
package com.kafka.interceptor;
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;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 本类是kafka自定义拦截器:用于在统计发送给broker的消息成功和失败的输量
* 拦截器:
* 用户在消息发送前以及 producer 回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。
* 多个拦截器作用于同一条消息从而形成一个拦截链。
* 拦截器可能运行在多个线程中,线程安全需要用户自己去保证。
*/
public class CountValueInterceptor implements ProducerInterceptor {
/**
*使用java.util.concurrent.atomic中的AtomicInteger来线程安全地实现i++功能
*/
private static AtomicInteger successNum=new AtomicInteger(0);//记录发送成功的消息数量
private static AtomicInteger failNum=new AtomicInteger(0);//记录发送失败的消息数量
/**
*获取配置信息和初始化数据时调用
* @param map
*/
@Override
public void configure(Map<String, ?> map) {
}
/**
* 序列化及计算分区前调用,用户可以在该方法内对消息做任何更改。即使不更改请将消息原样返回。
* 由于ProducerRecord中所有属性均为final,无法更改,所以改变value后需要改回一个新的ProducerRecord对象
* @param producerRecord 消息记录
* @return 对消息不做修改,所以直接返回producerRecord
*/
@Override
public ProducerRecord onSend(ProducerRecord producerRecord) {
return producerRecord;
}
/**
* 在消息成功发送给broker或发送失败时调用
* 由于该方法在producer的io线程中调用,故不宜书写过重的逻辑,以免影响消息发送速度
* @param recordMetadata 消息发送成功返回消息的元信息
* @param e 消息发送失败返回异常
*/
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
if(recordMetadata != null)
successNum.getAndAdd(1); //加1
else
failNum.getAndAdd(1);//加1
}
/**
*关闭拦截器,做一些资源的清理工作
*/
@Override
public void close() {
System.out.println("发送成功的消息数量:" + successNum + "\n 发送失败的消息数量:" + failNum);
}
}
自定义分区器
package com.kafka.partitioner;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.clients.producer.internals.DefaultPartitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import java.util.List;
import java.util.Map;
/**
* kafka自定义分区器实现自定义分区
* key和分区是在发送消息时new ProducerRecord对象时指定。
* kafka默认的分区规则需要查看org.apache.kafka.clients.producer.internals.DefaultPartitioner的partition方法
*
*/
public class MyPartitioner implements Partitioner {
/**
* 定义分区规则的方法
* @param topic topic名
* @param key 键
* @param keyBytes
* @param value
* @param valueBytes
* @param cluster kafka集群对象
* @return 返回0,则数据进入0号分区
*/
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//这里通过主题名称得到该主题所有的分区信息
List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
int numPartitions = partitionInfos.size();
if("partition_0".equals(key.toString())){
return 0;
}else if("partition_1".equals(key.toString())){
return 1;
}else{//如果没有自定义的分区规则,则调用kafka内部的分区规则
return new DefaultPartitioner().partition(topic,key,keyBytes,value,valueBytes,cluster);
}
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}