一、kafka的java客户端-生产者
1、引入依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
2、生产者的基本实现
package com.producer;
import com.alibaba.fastjson.JSON;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class MySimpleProducer {
private static final String TOPIC_NAME="my-replicated-topic";
public static void main(String[] args) throws ExecutionException, InterruptedException {
Properties properties=new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.111.1:9092,192.168.111.1:9093,192.168.111.1:9094");
//将发送的key从字符串转换为字节数组
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//把发送消息的value转换为字节数组
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
//发消息的客户端
Producer<String,String> producer=new KafkaProducer<>(properties);
ProducerRecord<String,String> producerRecord=new ProducerRecord<>(TOPIC_NAME,"mykey","hellokafka");
RecordMetadata recordMetadata = producer.send(producerRecord).get();
System.out.println("同步发送消息结果"+"topic-"+recordMetadata.topic()+"|partition-"+recordMetadata.partition()+"|offset"+recordMetadata.offset());
producer.close();
}
}
3、指定分区发送消息
ProducerRecord<String,String> producerRecord=new ProducerRecord<>(TOPIC_NAME,0,"mykey","hellokafka");
4、未指定分区,则会通过业务key的hash运算,算出消息发送往哪个分区
ProducerRecord<String,String> producerRecord=new ProducerRecord<>(TOPIC_NAME,"mykey","hellokafka");
5、生产者同步发送消息
如果生产者没有收到ack,生产者会阻塞,阻塞到3s的时间,如果还没有收到消息,会进行重试,重试次数3次。
RecordMetadata recordMetadata = producer.send(producerRecord).get();
System.out.println("同步发送消息结果" + "topic-" + recordMetadata.topic()
+ "|partition-" + recordMetadata.partition()
+ "|offset" + recordMetadata.offset());
6、生产者异步发送消息
异步发送生产者发送完消息后就可以执行之后的业务,broker在收到消息后异步调用生产者提供的callback回调方法。
producer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null) {
System.out.println("异步发送消息失败" + e.getStackTrace());
}
if (recordMetadata != null) {
System.out.println("异步发送消息结果" + "topic-" + recordMetadata.topic()
+ "| partition-" + recordMetadata.partition()
+ "| offset-" + recordMetadata.offset());
}
}
});
二、生产者中的配置
1、ack以及重试配置
在同步发送的前提下,生产者在获得集群的ack之前会一直阻塞。
ack有三个配置:
-
ack=0:kafka-cluser不需要任何broker收到的消息,就立即返回ack给生产者,最容易丢消息,效率是最高的。
-
如果设置为零,那么生产者根本不会等待来自服务器的任何确认。该记录将立即添加到套接字缓冲区并视为已发送。这种情况下retries不能保证服务端已经收到记录,配置不会生效(因为客户端一般不会知道任何故障)。为每个记录返回的偏移量将始终设置为-1。
-
-
ack=1(默认):多副本之间的leader已经收到消息,并把消息写入到本地log中,才会返回ack给生产者,性能和安全性是最均衡的。
-
这意味着领导者会将记录写入其本地日志,但会在不等待所有追随者的完全确认的情况下做出响应。在这种情况下,如果领导者在确认记录后立即失败,但在追随者复制它之前,则记录将丢失。
-
-
ack=all/1:里面有默认的配置min.insync.replicas=2(默认为 1,推荐配置大于 2),此时就需要leader和一个follower同步完后,才会返回ack给生产者(此时集群中有2个broker已经完全接收数据),这种方式最安全,性能最差。
-
这意味着领导者将等待完整的同步副本集来确认记录。这保证了只要至少一个同步副本保持活动状态,记录就不会丢失。这是最强的可用保证。这相当于 acks=-1 设置。
-
ack(没有收到ack,就进行重试)以及重试配置
properties.put(ProducerConfig.ACKS_CONFIG, 1);
/* 发送失败会进行重试,默认重试间隔为100ms,重试能保证消息发送的可靠性,
但是也可能造成消息重复发送,比如网络波动,所以需要在接收者那边进行幂等性校验*/
properties.put(ProducerConfig.RETRIES_CONFIG,3);
//重试时间间隔
properties.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG,300);
2、消息发送的缓冲区
- kafka默认会创建一个消息缓冲区,用来存放重要消息,缓冲区是32M
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
- kafka本地线程会去缓冲区中一次拉16KB的数据,发送到broker
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);
- 如果线程拉不到16k的数据,那么间隔10ms也会将已拉到的数据发送到broker
properties.put(ProducerConfig.LINGER_MS_CONFIG,10);
三、kafka的java客户端-消费者
1、消费者的基本实现
package com.producer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
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 org.apache.kafka.common.serialization.StringSerializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
public class MyConsumer {
private static final String CONSUMER_GROUP_NAME="testGroup";
private static final String TOPIC_NAME = "my-replicated-topic";
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.111.1:9092,192.168.111.1:9093,192.168.111.1:9094");
//消费者分组名
properties.put(ConsumerConfig.GROUP_ID_CONFIG,CONSUMER_GROUP_NAME);
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(properties);
consumer.subscribe(Arrays.asList(TOPIC_NAME));
while (true){
ConsumerRecords<String,String> records=consumer.poll(Duration.ofMillis(1000));
for(ConsumerRecord<String,String> record:records){
System.out.printf("收到的消息: partition = %d ,offset = %d ,key = %s ,value = %s%n",
record.partition(),record.offset(),record.key(),record.value());
}
}
}
}
2、关于消费者自动提交与手动提交offset
1、提交的内容
消费者无论是自动提交还是手动提交,都要把所属的消费组+消费的某个主题+消费的分区及消费的偏移量,这样的信息提交到集群的_consumer_offsets主题里面。
2、自动提交
消费poll消息下来以后就会提交offset
//是否自动提交偏移量
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");
//自动提交offset时间
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000")
注意:自动提交会丢消息。因为消费者在提交offset之后,有可能在还未消费之前宕机。
3、手动提交
需要把自动配置改为false
//是否自动提交偏移量
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");
- 手动同步提交
在消费完消息后调用同步提交的方法,当集群返回ack前一直阻塞,返回ack后表示提交成功,执行之后的逻辑。
while (true){
ConsumerRecords<String,String> records=consumer.poll(Duration.ofMillis(1000));
for(ConsumerRecord<String,String> record:records){
System.out.printf("收到的消息: partition = %d ,offset = %d ,key = %s ,value = %s%n",
record.partition(),record.offset(),record.key(),record.value());
}
//所有的消息已消费完
if(records.count()>0){
//手动提交offset 当前线程阻塞 知道提交offset成功
//一般使用同步提交
consumer.commitSync(); // 造成阻塞 等集群返回ack
}
}
- 手动异步提交
在消息消费完后提交,不需要等待集群返回ack,直接执行后续的逻辑,可以设置一个回调方法,供集群调用。
while (true){
ConsumerRecords<String,String> records=consumer.poll(Duration.ofMillis(1000));
for(ConsumerRecord<String,String> record:records){
System.out.printf("收到的消息: partition = %d ,offset = %d ,key = %s ,value = %s%n",
record.partition(),record.offset(),record.key(),record.value());
}
//所有的消息已消费完
if(records.count()>0){
//手动提交offset 当前线程阻塞 知道提交offset成功
//一般使用同步提交
//consumer.commitSync(); // 造成阻塞 等集群返回ack
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
if(e != null){
System.out.println("Commit failed for " + map);
System.out.println("Commit failed exception" + e.getStackTrace());
}
}
});
}
}
3、长轮询poll消息
- 默认情况下消费者一次会 poll 500 条消息
properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG,500);
- 代码中设置了长轮询的时间是 1000ms
while (true){
ConsumerRecords<String,String> records=consumer.poll(Duration.ofMillis(1000));
for(ConsumerRecord<String,String> record:records){
System.out.printf("收到的消息: partition = %d ,offset = %d ,key = %s ,value = %s%n",
record.partition(),record.offset(),record.key(),record.value());
}
}
意味着:
-
-
如果一次poll到500条 直接执行for循环
-
如果一次没有poll到500条。且时间在1s内,那么长轮询继续poll。要么到500条,要么到1s
-
如果多次poll 都没达到500条,且时间到了1s ,那么直接执行for循环。
-
-
如果两次poll的间隔超过30s,集群会认为该消费者的消费能力弱,会将该消费者踢出消费者组,由另一个消费者进行消费对应分区。(触发rebalance)会造成性能开销,可以通过设置参数,来调节一次所poll消息条数。
//一次最大拉取信息条数
properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG,500);
//两次poll之间时间间隔
properties.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG,30*1000);
4、消费者消费健康状态检查
消费者每隔1s向kafka集群发送一次心跳,如果kafka集群间隔10s未收到消费者心跳,将会把消费者踢出消费者组,触发该消费组的rebalance,将该分区交由其他消费者进行消费。
//consumer给broker发送心跳时间间隔
properties.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG,1000);
//kafka集群间隔10s未收到消费者心跳
properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG,10*1000);
5、指定分区和偏移量进行消费
- 指定分区消费
//指定分区消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
- 从头开始消费
//消息从头开始消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME,0)))
- 指定offset消费
//从指定offset开始消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seek(new TopicPartition(TOPIC_NAME,0),10);
- 指定时间开始消费
根据时间,去所有的partition中确定该时间对应的offset,然后去所有的partition中找到该offset之后的消息进行消费。
List<PartitionInfo> topicPartition = consumer.partitionsFor(TOPIC_NAME);
long date = new Date().getTime() - 1000 * 60 * 60;
Map<TopicPartition,Long> map=new HashMap<>();
for(PartitionInfo pa:topicPartition){
map.put(new TopicPartition(TOPIC_NAME,pa.partition()),date);
}
Map<TopicPartition,OffsetAndTimestamp> parMap=consumer.offsetsForTimes(map);
for(Map.Entry<TopicPartition,OffsetAndTimestamp> entry: parMap.entrySet()){
TopicPartition key=entry.getKey();
OffsetAndTimestamp timestamp=entry.getValue();
if(key==null||timestamp==null){
continue;
}
Long offset=timestamp.offset();
System.out.println("partition "+ key.partition()+"| offset"+offset);
System.out.println();
if(timestamp!=null){
consumer.assign(Arrays.asList(key));
consumer.seek(key,offset);
}
}
6、新消费组消费规则
新消费组中的消费者在启动之后,会从当前分区的最后一个offset+1开始消费新消息。可以通过配置,让新的消费组中的消费者第一次执行时从头开始消费,之后按照offset开始消费。
-
latest(默认):只消费自己启动之后的消息。
-
earliest:第一次从头开始消费,之后按照offset开始消费。
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");