pom
xml
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.19</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
kafka客户端管理器代码
java
/**
* kafka管理client
* @author
* @create 2023-05-31 22:06
*/
public class KafkaTopicDML {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Properties properties = new Properties();
properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,"127.0.0.1:9092");
// properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,"127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
KafkaAdminClient client = (KafkaAdminClient) KafkaAdminClient.create(properties);
//创建topic信息
/*
*主题,分区,副本因子
*/
CreateTopicsResult createTopicsResult = client.createTopics(Arrays.asList(new NewTopic("topic02", 3, (short) 3)));
createTopicsResult.all().get();//异步创建改成同步创建
/**
* 打印kafka信息
*/
//主题列表
ListTopicsResult topicsResult = client.listTopics();
Set<String> names = topicsResult.names().get();
for (String name : names) {
System.out.println(name);
}
//删除topic
DeleteTopicsResult deleteTopics = client.deleteTopics(Arrays.asList("topic02"));
deleteTopics.all().get();//异步变同步
//查看topic详细信息
// DescribeTopicsResult describeTopicsResult = client.describeTopics(Arrays.asList("topic02"));
// Map<String, TopicDescription> topicDescriptionMap = describeTopicsResult.all().get();//异步变同步
// Set<Map.Entry<String, TopicDescription>> entries = topicDescriptionMap.entrySet();
// for (Map.Entry<String, TopicDescription> entry : entries) {
// System.out.println(entry.getKey());
// System.out.println(entry.getValue());
// System.out.println("=============");
// }
//关闭KafkaAdminClient
client.close();
}
}
生产者
java
public static void main(String[] args) {
Properties prop = new Properties();
prop.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"localhost:9092");
prop.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
prop.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class);
KafkaProducer<String, String> producer = new KafkaProducer<>(prop);
try {
//构建消息
ProducerRecord<String, String> record = new ProducerRecord<>("my-topic","my-key","yuyang");
// ProducerRecord<String, String> record = new ProducerRecord<String, String>("my-topic",1,"my-key","yuyang");//指定分区
//发送消息
producer.send(record);
log.info("message is sent");
// System.out.println("message is sent");
} catch (Exception e) {
e.printStackTrace();
} finally {
producer.close();
}
}
消费者
java
public static void main(String[] args) {
Properties prop = new Properties();
prop.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"localhost:9092");
prop.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
prop.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
prop.put(ConsumerConfig.GROUP_ID_CONFIG,"group01");//消费者组
//2.创建Topic消费者
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(prop);
//3.调用消费者拉取消息 订阅主题
consumer.subscribe(Collections.singletonList("my-topic"));
//3.订阅topic开头的消息队列
// consumer.subscribe(Pattern.compile("^topic.*$"));
// List<TopicPartition> partitions = Arrays.asList(new TopicPartition("topic", 0));
// consumer.assign(partitions);
// //指定消费分区
// consumer.seekToBeginning(partitions);//从头开始消费
consumer.seek(new TopicPartition("my-topic",0),1);//从头开始消费
while (true) {
//每隔1秒拉取一次消息
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
if (!consumerRecords.isEmpty()) {
Iterator<ConsumerRecord<String, String>> iterator = consumerRecords.iterator();
while (iterator.hasNext()) {
//获取一个消息
ConsumerRecord<String, String> record = iterator.next();
String topic = record.topic();
System.out.println("topic = " + topic);
int partition = record.partition();
System.out.println("partition = " + partition);
long offset = record.offset();
System.out.println("offset = " + offset);
String key = record.key();
System.out.println("key = " + key);
String value = record.value();
System.out.println("value = " + value);
long timestamp = record.timestamp();
System.out.println("timestamp = " + timestamp);
}
}
}
}
消费者自定义分区
java
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(prop);
List<TopicPartition> partitions = Arrays.asList(new TopicPartition("topic", 0));
consumer.assign(partitions);
//指定消费分区的位置
consumer.seekToBeginning(partitions);//从头开始消费
//设置消费分区指定位置
// consumer.seek(new TopicPartition("topic01",0),1);//从偏移量 开始消费
生产者分区策略
java
//自定义分区策略
prop.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, UserDefinePartitioner.class);
自定义分区类
java
public class UserDefinePartitioner implements Partitioner {
private AtomicInteger counter = new AtomicInteger(0);
/**
* 返回分区号
* @param topic
* @param key
* @param keybytes
* @param value
* @param valuebytes
* @param cluster
* @return
*/
@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 (keybytes == null) {
int addIncrement = counter.getAndIncrement();
return (addIncrement&Integer.MAX_VALUE)%numPartitions;
}else {
return Utils.toPositive(Utils.murmur2(keybytes))%numPartitions;
}
}
@Override
public void close() {
System.out.println("close");
}
@Override
public void configure(Map<String, ?> map) {
System.out.println("configure");
}
}
生产者自定义拦截器
java
prop.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, UserDefineInterceptor.class.getName());
自定义的拦截器类
java
public class UserDefineInterceptor implements ProducerInterceptor {
@Override
public ProducerRecord onSend(ProducerRecord producerRecord) {
return new ProducerRecord(producerRecord.topic(),producerRecord.key(),producerRecord.value()+":===");
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
System.out.println("metadata = " + metadata);
System.out.println("exception = " + exception);
}
@Override
public void close() {
System.out.println("close");
}
@Override
public void configure(Map<String, ?> map) {
System.out.println("configure");
}
}
拦截器作用:
生产者发送消息时,对数据做一些修饰,装饰;确定消息的生命周期;获取消息的元信息
高级API
offset自动控制
java
prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");//(最早)自动将偏移量重置为最早偏移量
prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"laster");//(最近)自动将偏移量重置为最新的偏移量
prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"none");//如果未找到消费者组的先前偏移量,则向消费者抛出异常
Kafka消费者在消费数据时默认会定期提交消费的偏移量,保证所有的消息至少可以被消费者消费一次,看通过以下两个参数配置
enable.auto.commit=true 默认 //配置offset自动提交
auto.commit.interval.ms=5000 默认
如果用户需要自己管理offset的自动提交,可以关闭offset的自动提交,手动管理offset提交的偏移量,手动提交的offset偏移量永远都要比本次消费的偏移量+1,因为提交的offset是kafka消费者下一次抓取数据的位置
java
//配置offset自动提交时间 5s一次 自动提交offset
prop.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,5000);
prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
手动提交
java
Properties prop = new Properties();
prop.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"localhost:9092");
prop.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
prop.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
prop.put(ConsumerConfig.GROUP_ID_CONFIG,"group01");
//./bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic topic01 --partitions 3l--replication-factor 3
prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");//从最早的位置
//关闭 offset偏移量自动提交
prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
//2.创建Topic消费者
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(prop);
//3.订阅topic开头的消息队列
consumer.subscribe(Arrays.asList("my-topic"));
while (true) {
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
if (!consumerRecords.isEmpty()) {
Iterator<ConsumerRecord<String, String>> iterator = consumerRecords.iterator();
//记录分区的消费元数据信息
Map<TopicPartition, OffsetAndMetadata> offsetMap = new HashMap<>();
while (iterator.hasNext()) {
//获取一个消息
ConsumerRecord<String, String> record = iterator.next();
String topic = record.topic();
System.out.println("topic = " + topic);
int partition = record.partition();
System.out.println("partition = " + partition);
long offset = record.offset();
System.out.println("offset = " + offset);
String key = record.key();
System.out.println("key = " + key);
String value = record.value();
System.out.println("value = " + value);
long timestamp = record.timestamp();
System.out.println("timestamp = " + timestamp);
//记录消费分区的偏移量元数据
offsetMap.put(new TopicPartition(topic,partition),new OffsetAndMetadata(offset));
//consumer同步提交 提交消费者偏移量
// consumer.commitSync(offsetMap);
//consumer异步提交 提交消费者偏移量
consumer.commitAsync(offsetMap, new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsetMap, Exception e) {
System.out.println("offsetMap = " + offsetMap + " ,Exception" + e);
}
});
}
}
}
ACKS & Retries
Kafka生产者在发送完一个消息后,要求Broker在规定的世界内ACK应答,如果没在规定时间内应答,Kafka生产者会尝试n此重发消息。
ACKS 默认1
acks=1 Leader会将record(消息记录)写到本地日志中,但不会等待所有的Follower完全确认的情况下做出响应。这种情况下,如果Leader在确认记录后立即失效,但在Follower复制记录之前失败,则记录将丢失。
acks=0 生产者根本不会等待服务器的确认,该记录立即添加到套接字缓冲区中并视为已发送。这种情况下,不保证服务器已收到该记录(应用于日志)
acks=-1 Leader将订单全部副本同步确认记录,这保证了只要有有一个同步副本处于活动状态,该记录就不会丢失。这个是最有力的保证。这等效于acks=all
如果生产者在规定时间内,没有得到Leader的ack应答,Kafka可以开启reties机制。
request.timeout.ms = 30000 默认
retries = 2147483647 默认
java
prop.put(ProducerConfig.ACKS_CONFIG,-1);
//重试次数, 不包含第一次发送
prop.put(ProducerConfig.RETRIES_CONFIG,3);
//将检测超时时间设置为1ms
prop.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,1);
幂等写
幂等特性:不多不少,不丢不重
什么是幂等:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。幂等又称为exactly once
Kafka在0.11.0.0版本支持增加了对幂等的支持。幂等是针对生产者角度的特性。幂等可以保证上生产者发送的消息,不会丢失,而且不会重复。实现幂等的关键点就是服务端可以区分请求是否重复,过滤掉重复的请求。要区分请求是否重复的有两点:
唯一标识:要想区分请求是否重复,请求中就得有唯一标识。例如支付请求中,订单号就是唯一标识
记录下已处理过的请求标识:光有唯一标识还不够,还需要记录下那些请求是已经处理过的,这样当收到新的请求时,用新请求中的标识和处理记录进行比较,如果处理记录中有相同的标识,说明是重复记录,拒绝掉。
要停止多次处理消息,必须仅将其持久化到Kafka Topic中仅仅一次。在初始化期间,kafka会给生产者生成一个唯一的ID称为Producer ID或PID。
PID和序列号与消息捆绑在一起,然后发送给Broker。由于序列号从零开始并且单调递增,因此,仅当消息的序列号比该PID / TopicPartition对中最后提交的消息正好大1时,Broker才会接受该消息。如果不是这种情况,则Broker认定是生产者重新发送该消息。
enable.idempotence= false 默认
注意:在使用幂等性的时候,必须开启retries=true和acks=all
java
//设置kafka acks和retries
prop.put(ProducerConfig.ACKS_CONFIG,-1);
//不包含第一次发送,如果尝试发送3次;失败,则系统放弃发送
prop.put(ProducerConfig.RETRIES_CONFIG,3);
//检测超时的时间设置为1ms
prop.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,1);
//开启kafka的幂等性
prop.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
//让记录严格有序
//有多于n个记录未被应答,客户端会被阻塞
prop.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION,1);
一个是唯一标识,一个是开启kafka幂等的特性
kafka事务
kafka的幂等性,只能保证一条记录在分区发送的原子性,但要保证多条记录(多分区)之间的完整性,就需要开启kafka的事务操作了。
在Kafka0.11.0.0除了引入的幂等性的概念,同时也引入了事务的概念。通常kafka的事务分为生产者事务Only、消费者&生产者事务。默认消费者消费的消息级别是read_uncommited数据,这有可能读取到事务失败的数据,所有再开启生产者事务之后,需要用户设置消费者的事务隔离级别
。
isolation.level = read_uncommitted 默认
该选项有两个值read_committed|read_uncommitted,如果开始事务控制,消费端必须将事务的隔离级别设置为read_committed
开启的生产者事务时,只需要指定transactional.id
属性即可,一旦开启了事务,默认生产者就已经开启了幂等。但要求transactional.id的取值必须是唯一的,同一时刻只能有一个transactional.id存储在,其他的将会被关闭。
生产者
java
//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());
//配置kafka批处理大小
props.put(ProducerConfig.BATCH_SIZE_CONFIG,1024);
//等待5ms如果batch中数量不足1024大小,也会发送
props.put(ProducerConfig.LINGER_MS_CONFIG,5);
//开启kafka的重试机制和幂等性
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
props.put(ProducerConfig.ACKS_CONFIG,-1);
props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,10000);//请求超时
//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();
消费者
java