Topic基本的dml操作,如何创建,删除,查询一个topic含有哪些东西
package com.dml;
import org.apache.kafka.clients.admin.*;
import org.apache.kafka.common.KafkaFuture;
import java.util.*;
import java.util.concurrent.ExecutionException;
public class TopicDMLDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//配置连接参数
Properties props = new Properties();
/*props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,
"CenTos:9092");*/
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,
"CentOSA:9092,CentOSB:9092,CentOSC:9092");
//创建kafka客户端连接
KafkaAdminClient adminClient= (KafkaAdminClient) KafkaAdminClient.create(props);
//客户端删除主题
DeleteTopicsResult deleteTopicsResult=adminClient.deleteTopics(Arrays.asList("topic01"));//异步删除
deleteTopicsResult.all().get();//加上这段 上面的异步变为同步
//创建Topics
List<NewTopic> newTopics = Arrays.asList(new NewTopic("topic08", 2, (short) 3));
CreateTopicsResult createTopicsResult= adminClient.createTopics(newTopics);//异步创建topic
createTopicsResult.all().get();//加上上面这段 才是同步创建topic
//查询topics
KafkaFuture<Set<String>> nameFutures = adminClient.listTopics().names();
for (String name : nameFutures.get()) {
System.out.println(name);
}
//查看Topic详情
DescribeTopicsResult describeTopics =
adminClient.describeTopics(Arrays.asList("topic03"));
Map<String, TopicDescription> tdm = describeTopics.all().get();
for (Map.Entry<String, TopicDescription> entry : tdm.entrySet()) {
System.out.println(entry.getKey()+"\t"+entry.getValue());
}
adminClient.close();
}
}
创建生产者模拟发送消息,发送消息的时候需要指定消息所属的topic和消息k,v.因为创建topic的时候指定了分区数量,所以会根据消息的key指定消息所属的分区。发送消息有两种方式,即同步和异步。
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class KafkaProducerDemo {
public static void main(String[] args) throws InterruptedException {
//1.创建链接参数
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
//2.创建生产者
KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);
//3.模拟发送消息
for(Integer i=0;i< 10;i++){
Thread.sleep(100);
ProducerRecord<String, String> record = new ProducerRecord<>("topic03", "key" + i, "value" + i);
producer.send(record);//同步发送
//异步发送
producer.send(new ProducerRecord<String, String>("topic03",
"value" + i + Integer.toString(i)), new Callback() {
//回调函数, 该方法会在 Producer 收到 ack 时调用,为异步调用
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
System.out.println(metadata.partition() + " - " + metadata.offset());
} else {
exception.printStackTrace();
}
}
});
}
producer.close();
}
}
创建消费者,指定消费者组,消费者所属的组。指定消费者消费的topic,在这里,消费者具体消费哪一个分区的内容是根据消费者的分区分配策略指定的。
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 java.time.Duration;
import java.util.Iterator;
import java.util.Properties;
import java.util.regex.Pattern;
public class KafkaConsumerDemo {
public static void main(String[] args) {
//1.创建Kafka链接参数
Properties props=new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG,"group01");//如果不指定消费者组 启动会报错
//2.创建Topic消费者
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
//3.订阅topic开头的消息队列 指定当前消费者组消费的消息主题topic
consumer.subscribe(Pattern.compile("^topic.*$"));
while (true){
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));//隔多久去取一次数据 这里每隔一秒
Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();//对取出来的数据进行获取迭代器
while (recordIterator.hasNext()){//获取下一个 返回true表示有下一个 返回false表示没有
ConsumerRecord<String, String> record = recordIterator.next();//去到下一个消息
//分别获取消息的key value offset partition
String key = record.key();
String value = record.value();
long offset = record.offset();
int partition = record.partition();
System.out.println("key:"+key+",value:"+value+",partition:"+partition+",offset:"+offset);
}
}
}
}//ConsumerCoordinator消费者协调器负责将topic的分区分发给消费者
//同一组内的消费者对偏移量的消费是一种均分分区的概念
//同一个GROUP_ID_CONFIG消费组的消费者对同一个topic的消息partition是均分的,一个group01的所有消费者和三个group02的所有消费者消费的topic1的partition总数是一样的,区别是一个全部消费一个均分
//消费者实现分区内部有序
//同一个消费者组的成员有宕机的会重新分配这个消费者组的topic分区
在这里没有指定消费者所属的group,每一个消费者消费topic的哪个分区手动指定
也可以指定消费者从分区的哪个位置开始消费
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.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;
public class KafkaConsumerDemo_2 {
public static void main(String[] args) {
//1.创建Kafka链接参数
Properties props=new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
//props.put(ConsumerConfig.GROUP_ID_CONFIG,"group01");
//2.创建Topic消费者
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
//订阅相关的topics,手动指定消费分区,失去分组管理特性 没有组的概念也不会做负载均衡 consumer的分区分配策略失效了
List<TopicPartition> partitions=Arrays.asList(new TopicPartition("topic03",2));
//消息的负载均衡策略:每一条消息被生产者产生之后会根据是否有key来计算应该位于所属topic的哪个分区,没有key就轮询
consumer.assign(partitions);
//手动指定消费分区位置
consumer.seekToBeginning(partitions);//手动指定消费者消费分区从哪个位置开始 这个方法是从0开始消费
//consumer.seek(new TopicPartition("topic03",0),1); //从topic03 的第0个分区 的第1位置开始消费
//手动指定消费者消费的分区
//3.订阅topic开头的消息队列
// consumer.subscribe(Pattern.compile("^topic.*$"));
while (true){
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();
while (recordIterator.hasNext()){
ConsumerRecord<String, String> record = recordIterator.next();
String key = record.key();
String value = record.value();
long offset = record.offset();
int partition = record.partition();
System.out.println("key:"+key+",value:"+value+",partition:"+partition+",offset:"+offset);
}
}
}
}//ConsumerCoordinator消费者协调器负责将topic的分区分发给消费者
//同一组内的消费者对偏移量的消费是一种均分分区的概念
//同一个GROUP_ID_CONFIG消费组的消费者对同一个topic的消息partition是均分的,一个group01的所有消费者和三个group02的所有消费者消费的topic1的partition总数是一样的,区别是一个全部消费一个均分
//消费者实现分区内部有序
//同一个消费者组的成员有宕机的会重新分配这个消费者组的topic分区
//没有了组管理的概念 即使启动多个该方法 关闭任意一个 都不会影响其他的 如果有组的概念 就会将关闭的消费者消费的分区均衡到该组其他的消费者组下
指定生产者生产消息的时候,消息所属topic的分区
生产者生产消息
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class KafkaProducerDemo {
public static void main(String[] args) {
//1.创建链接参数
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
//配置消息的分区策略
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,UserDefinePartitioner.class.getName());
//2.创建生产者
KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);
//3.封账消息队列
for(Integer i=0;i< 15;i++){
ProducerRecord<String, String> record = new ProducerRecord<>("topic03", "value" + i);
producer.send(record);
}
producer.close();
}
}
UserDefinePartitioner实现Partitioner接口,重写分区方法
有key就用key和分区数取模,没有key就用实现的累加器和分区数取模
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.utils.Utils;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public class UserDefinePartitioner implements Partitioner {
private AtomicInteger atomicInteger=new AtomicInteger(0);
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
int numPartitions = cluster.partitionsForTopic(topic).size();//获取所有可用分区数
if(keyBytes==null || keyBytes.length==0){//没有key 就轮询
return atomicInteger.addAndGet(1) & Integer.MAX_VALUE% numPartitions;
} else {//有key就用key和分区数运算
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
//消息发送完毕关闭调用
@Override
public void close() {
System.out.println("close");
}
//初始化配置调用
@Override
public void configure(Map<String, ?> configs) {
System.out.println("configure");
}
}
发送消息的时候对发送的消息进行序列化;
消费消息的时候进行反序列化;
引入依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
创建User类实现Serializable接口
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Stack;
public class User implements Serializable {
private Integer id;
private String name;
private Date birthDay;
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", birthDay=" + birthDay +
'}';
}
public User(Integer id, String name, Date birthDay) {
this.id = id;
this.name = name;
this.birthDay = birthDay;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getBirthDay() {
return birthDay;
}
public void setBirthDay(Date birthDay) {
this.birthDay = birthDay;
}
}
实现Serializer接口创建序列化类
import org.apache.commons.lang3.SerializationUtils;
import org.apache.kafka.common.serialization.Serializer;
import java.io.Serializable;
import java.util.*;
public class ObjectSerializer implements Serializer<Object> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
System.out.println("configure");
}
//实现序列化
@Override
public byte[] serialize(String topic, Object data) {
return SerializationUtils.serialize((Serializable) data);
}
@Override
public void close() {
System.out.println("close");
}
}
实现Deserializer实现反序列化
import org.apache.commons.lang3.SerializationUtils;
import org.apache.kafka.common.serialization.Deserializer;
import java.util.Map;
public class ObjectDeserializer implements Deserializer<Object> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
System.out.println("configure");
}
@Override
public Object deserialize(String topic, byte[] data) {
return SerializationUtils.deserialize(data);
}
@Override
public void close() {
System.out.println("close");
}
}
生产者发送消息实现使用自定义序列化类
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,ObjectSerializer.class.getName());
public class KafkaProducerDemo {
public static void main(String[] args) {
//1.创建链接参数
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,ObjectSerializer.class.getName());
//2.创建生产者
KafkaProducer<String,User> producer=new KafkaProducer<String, User>(props);
//3.封账消息队列
for(Integer i=0;i< 10;i++){
ProducerRecord<String, User> record = new ProducerRecord<>("topic01", "key"+i,new User(i,"user"+i,new Date()));
producer.send(record);
}
producer.close();
}
}
消费者消费消息的时候指定value的反序列化
public class KafkaConsumerDemo {
public static void main(String[] args) {
//1.创建Kafka链接参数
Properties props=new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,ObjectDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG,"group01");
//2.创建Topic消费者
KafkaConsumer<String,User> consumer=new KafkaConsumer<String, User>(props);
//3.订阅topic开头的消息队列
consumer.subscribe(Pattern.compile("^topic.*$"));
while (true){
ConsumerRecords<String, User> consumerRecords = consumer.poll(Duration.ofSeconds(1));
Iterator<ConsumerRecord<String, User>> recordIterator = consumerRecords.iterator();
while (recordIterator.hasNext()){
ConsumerRecord<String, User> record = recordIterator.next();
String key = record.key();
User value = record.value();
long offset = record.offset();
int partition = record.partition();
System.out.println("key:"+key+",value:"+value+",partition:"+partition+",offset:"+offset);
}
}
}
}
实现ProducerInterceptor接口,在发送消息的时候做一些拦截修饰处理,以及发送消息之后的回调通知
public class UserDefineProducerInterceptor implements ProducerInterceptor {
@Override
public ProducerRecord onSend(ProducerRecord record) {
ProducerRecord wrapRecord = new ProducerRecord(record.topic(), record.key(), record.value());
wrapRecord.headers().add("user1","mashibing".getBytes());
return wrapRecord;
}
@Override //onSend之后的回调通知 成功或者失败都会通知
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
System.out.println("metadata:"+metadata+",exception:"+exception);
}
@Override
public void close() {
System.out.println("close");
}
@Override
public void configure(Map<String, ?> configs) {
System.out.println("configure");
}
}
生产者指定生产消息的拦截器策略
public class KafkaProducerDemo {
public static void main(String[] args) {
//1.创建链接参数
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,UserDefineProducerInterceptor.class.getName());
//2.创建生产者
KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);
//3.封账消息队列
for(Integer i=0;i< 10;i++){
ProducerRecord<String, String> record = new ProducerRecord<>("topic01", "key" + i, "error" + i);
producer.send(record);
}
producer.close();
}
}
Kafka消费者默认对于未订阅的topic的offset的时候,也就是系统并没有存储该消费者的消费分区的记录信息,默认Kafka消费者的默认首次消费策略:latest
auto.offset.reset=latest
earliest - 自动将偏移量重置为最早的偏移量
latest - 自动将偏移量重置为最新的偏移量 如果系统没有该消费者的偏移量,系统会读取该分区的最早偏移量
none - 如果未找到消费者组的先前偏移量,则向消费者抛出异常
Kafka消费者在消费数据的时候默认会定期的提交消费的偏移量,这样就可以保证所有的消息至少可以被消费者消费1次,用户可以通过以下两个参数配置:
enable.auto.commit = true 默认
auto.commit.interval.ms = 5000 默认 配置offset自动提交的时间间隔,每5秒中提交一次
如果用户需要自己管理offset的自动提交,可以关闭offset的自动提交,手动管理offset提交的偏移量,注意用户提交的offset偏移量永远都要比本次消费的偏移量+1,因为提交的offset是kafka消费者下一次抓取数据的位置。
消费者配置如下配置 可以改变自动提交offset的策略
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest");
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG 生效是在第一次访问kafka服务器的时候没有偏移量的时候应该怎么读取 后续再消费都是从上次的消费位置开始消费去读
手动提交配置
自动提交设置为false
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
每次提交的offset偏移量是下次消费的读取的起始位置,所以要加1.
消费一次数据提交一次偏移量
public class KafkaConsumerDemo_02 {
public static void main(String[] args) {
//1.创建Kafka链接参数
Properties props=new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG,"group09");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest");
//2.创建Topic消费者
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
//3.订阅topic开头的消息队列
consumer.subscribe(Arrays.asList("topic02"));
while (true){
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();
while (recordIterator.hasNext()){
ConsumerRecord<String, String> record = recordIterator.next();
String key = record.key();
String value = record.value();
long offset = record.offset();
int partition = record.partition();
//记录分区的消费元数据信息
Map<TopicPartition, OffsetAndMetadata> offsets=new HashMap<TopicPartition, OffsetAndMetadata>();
//记录消费分区的偏移量元数据 一定要在提交的时候offset加1
offsets.put(new TopicPartition(record.topic(),partition),new OffsetAndMetadata(offset+1));
//提交消费者偏移量
consumer.commitAsync(offsets, new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
System.out.println("完成:"+offset+"提交!"+"/exception"+exception);
}
});
System.out.println("key:"+key+",value:"+value+",partition:"+partition+",offset:"+offset);
}
}
}
}
Kafka生产者在发送完一个的消息之后,要求Broker在规定的额时间Ack应答答,如果没有在规定时间内应答,Kafka生产者会尝试n次重新发送消息。
acks=1 默认
acks=1 - Leader会将Record写到其本地日志中,但会在不等待所有Follower的完全确认的情况下做出响应。在这种情况下,如果Leader在确认记录后立即失败,但在Follower复制记录之前失败,则记录将丢失。
acks=0 - 生产者根本不会等待服务器的任何确认。该记录将立即添加到套接字缓冲区中并视为已发送。在这种情况下,不能保证服务器已收到记录。
acks=all - 这意味着Leader将等待全套同步副本确认记录。这保证了只要至少一个同步副本仍处于活动状态,记录就不会丢失。这是最有力的保证。这等效于acks = -1设置。
如果生产者在规定的时间内,并没有得到Kafka的Leader的Ack应答,Kafka可以开启reties机制。
发送消息以后指定时间内没有收到应答,会继续重发。会导致一个分区数据被发送多次
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class KafkaProducerDemo_01{
public static void main(String[] args) {
//1.创建链接参数
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,UserDefineProducerInterceptor.class.getName());
//设置请求超时时间 1毫秒内发送不成功即为失败
props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,1);
props.put(ProducerConfig.ACKS_CONFIG,"-1");
//重发次数 5 不包括第一次发送失败 尝试发送5次失败 系统放弃
props.put(ProducerConfig.RETRIES_CONFIG,5);
//2.创建生产者
KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);
//3.封账消息队列
ProducerRecord<String, String> record = new ProducerRecord<>("topic02", "ack", "testack");
producer.send(record);
producer.flush();
producer.close();
}
}//ack应答机制 保证消息一定会发送到leader 但是消费者可能会重复消费消息
HTTP/1.1中对幂等性的定义是:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.
Kafka在0.11.0.0版本支持增加了对幂等的支持。幂等是针对生产者角度的特性。幂等可以保证上生产者发送的消息,不会丢失,而且不会重复。实现幂等的关键点就是服务端可以区分请求是否重复,过滤掉重复的请求。要区分请求是否重复的有两点:
唯一标识:要想区分请求是否重复,请求中就得有唯一标识。例如支付请求中,订单号就是唯一标识
记录下已处理过的请求标识:光有唯一标识还不够,还需要记录下那些请求是已经处理过的,这样当收到新的请求时,用新请求中的标识和处理记录进行比较,如果处理记录中有相同的标识,说明是重复记录,拒绝掉。
幂等又称为exactly once。要停止多次处理消息,必须仅将其持久化到Kafka Topic中仅仅一次。在初始化期间,kafka会给生产者生成一个唯一的ID称为Producer ID或PID。
PID和序列号与消息捆绑在一起,然后发送给Broker。由于序列号从零开始并且单调递增,因此,仅当消息的序列号比该PID / TopicPartition对中最后提交的消息正好大1时,Broker才会接受该消息。如果不是这种情况,则Broker认定是生产者重新发送该消息。
enable.idempotence= false 默认
注意:在使用幂等性的时候,要求必须开启retries=true和acks=all
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class KafkaProducerDemo_02 {
public static void main(String[] args) {
//1.创建链接参数
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,UserDefineProducerInterceptor.class.getName());
props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,1);
props.put(ProducerConfig.ACKS_CONFIG,"-1");
props.put(ProducerConfig.RETRIES_CONFIG,3);
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);//开启kafka的幂等性
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION,1);//有一个不成功就阻塞
//2.创建生产者
KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);
//3.封账消息队列
for(Integer i=0;i< 1;i++){
ProducerRecord<String, String> record = new ProducerRecord<>("topic01", "key" + i, "value" + i);
producer.send(record);
}
producer.close();
}
}
//幂等性保证 不多不少 不丢不重
Kafka的幂等性,只能保证一条记录的在分区发送的原子性,但是如果要保证多条记录(多分区)之间的完整性,这个时候就需要开启kafk的事务操作。
在Kafka0.11.0.0除了引入的幂等性的概念,同时也引入了事务的概念。通常Kafka的事务分为 生产者事务Only、消费者&生产者事务。一般来说默认消费者消费的消息的级别是read_uncommited数据,这有可能读取到事务失败的数据,所有在开启生产者事务之后,需要用户设置消费者的事务隔离级别。
isolation.level = read_uncommitted 默认
该选项有两个值read_committed|read_uncommitted,如果开始事务控制,消费端必须将事务的隔离级别设置为read_committed
开启的生产者事务的时候,只需要指定transactional.id属性即可,一旦开启了事务,默认生产者就已经开启了幂等性。但是要求"transactional.id"的取值必须是唯一的,同一时刻只能有一个"transactional.id"存储在,其他的将会被关闭。
设置消费者事务隔离级别,可选项为读已提交和读未提交(产生脏数据,生产者事务失败了也会读到)
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 java.time.Duration;
import java.util.Iterator;
import java.util.Properties;
import java.util.regex.Pattern;
public class KafkaConsumerDemo {
public static void main(String[] args) {
//1.创建Kafka链接参数
Properties props=new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG,"group01");
//事务隔离级别 默认是读未提交 即使生产者发送数据 事务报错回滚了 该隔离级别也可以消费到
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");
//2.创建Topic消费者
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
//3.订阅topic开头的消息队列
consumer.subscribe(Pattern.compile("^topic.*$"));
while (true){
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();
while (recordIterator.hasNext()){
ConsumerRecord<String, String> record = recordIterator.next();
String key = record.key();
String value = record.value();
long offset = record.offset();
int partition = record.partition();
System.out.println("key:"+key+",value:"+value+",partition:"+partition+",offset:"+offset);
}
}
}
}
生产者开启事务配置
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 java.time.Duration;
import java.util.Iterator;
import java.util.Properties;
import java.util.regex.Pattern;
public class KafkaConsumerDemo {
public static void main(String[] args) {
//1.创建Kafka链接参数
Properties props=new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG,"group01");
//事务隔离级别 默认是读未提交 即使生产者发送数据 事务报错回滚了 该隔离级别也可以消费到
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");
//2.创建Topic消费者
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
//3.订阅topic开头的消息队列
consumer.subscribe(Pattern.compile("^topic.*$"));
while (true){
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();
while (recordIterator.hasNext()){
ConsumerRecord<String, String> record = recordIterator.next();
String key = record.key();
String value = record.value();
long offset = record.offset();
int partition = record.partition();
System.out.println("key:"+key+",value:"+value+",partition:"+partition+",offset:"+offset);
}
}
}
}
生产者事务和消费者隔离级别写在同一个方法
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.*;
public class KafkaProducerDemo01 {
public static void main(String[] args) {
//1.创建链接参数
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
//必须配置唯一事务ID
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction-id"+UUID.randomUUID().toString());
//2.创建生产者
KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);
producer.initTransactions();//初始化事务
try{
producer.beginTransaction();//开启事务
//3.封账消息队列
for(Integer i=0;i< 10;i++){
Thread.sleep(10000);
ProducerRecord<String, String> record = new ProducerRecord<>("topic01", "key" + i, "value" + i);
producer.send(record);
producer.flush();
}
producer.commitTransaction();//提交事务
}catch (Exception e){
producer.abortTransaction();//终止事务
}
producer.close();
}
}