一、基本使用
依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
生产者
package com.xxx.test;
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;
/**
* @Author xxx
* @Date 2023/1/30 10:10
* @Description kafka生产者测试
* @Version 1.0
*/
public class MyProducer {
// 主题名(若不存在该主题,发送时会自动创建)
private final static String TOPIC_NAME = "my-topic";
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 集合,存放Kafka配置项
Properties props = new Properties();
// kafka的主机和端口,集群的话可写多个“主机:端口”,用逗号隔开(注意检查Linux的防火墙是否开启了)
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.190.132:9092");
// 如果生产者发送消息没有收到返回的ack,生产者会阻塞,阻塞到3s的时间,如果还没有收到消息,会进行重试,重试的次数3次。
// 对于ack来说,会有三个参数配置:
// ack=0 kafka集群不需要任何的broker收到消息,就立即返回ack给生产者,最容易丢消息的,效率是最高的
// ack=1 多副本之间的leader已经收到消息,并把消息写入到本地的log中,才会返回ack给生产者,性能和安全性是最均衡的
// ack=-1/all 里面有默认的配置min.insync.replicas=2(默认为1,推荐配置大于等于2),此时就需要leader和一个follower同步完后才会返回ack给生产者
// (此时集群中有2个broker已完成数据的接收)这种方式最安全,但性能最差
props.put(ProducerConfig.ACKS_CONFIG, "1");
// 发送间隔
props.put(ProducerConfig.RETRIES_CONFIG, 3);
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);
// 设置发送消息的本地缓冲区,如果设置了该缓冲区,消息会先发送到本地缓冲区,可以提高消息发送性能,默认值是33554432,即32MB
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
// kafka本地线程会从缓冲区取数据,批量发送到broker
// 设置批量发送消息的大小,默认值是16384,即16kb,就是说一个batch满了16kb就发送出去
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
// 发送的延迟
// 默认值是0,意思就是消息必须立即被发送,但这样会影响性能
// 一般设置10毫秒左右,就是说这个消息发送完后会进入本地的一个batch,如果10毫秒内,这个batch满了16kb就会随batch一起被发送出去
// 如果10毫秒内,batch没满,那么也必须把消息发送出去,不能让消息的发送延迟时间太长
props.put(ProducerConfig.LINGER_MS_CONFIG, 10);
// 将要发送的消息的键值对都进行序列化处理,将key和value由字符串序列化为字节数组
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 创建发消息的客户端
Producer<String, String> producer = new KafkaProducer<>(props);
// 发之前先封装消息(key的作用是决定往哪个分区发送)
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, "myKey", JSON.toJSONString("消息体内容888"));// 1 未指定分区,会通过hash计算key后,计算出往哪个分区发送
// ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, 0, "myKey", JSON.toJSONString("消息体内容2"));// 2 指定了分区“0”
// 发送消息(同步发送),为了保证消息被消费,同步的发送用的多一点
try {
// 同步发送(生产者非得等到kafka返回的ack,不然阻塞3秒*3次)
RecordMetadata metadata = producer.send(producerRecord).get();
// 输出元数据
System.out.println("同步发送消息返回的结果是:" + "主题:" + metadata.topic() + "\t分区:" + metadata.partition() + "\t偏移量:" + metadata.offset());
} catch (InterruptedException e) {
e.printStackTrace();
// 做一些事情,如记录日志
} finally {
// 做一些事情
System.out.println("finally do something");
}
// 发送消息(异步发送),不会阻塞,kafka收到消息后,会回调Callback函数
// producer.send(producerRecord, new Callback() {
// public void onCompletion(RecordMetadata metadata, Exception exception) {
// if (exception != null) {
// System.out.println("消息发送失败:" + exception.getStackTrace());
// }
// if (metadata != null) {
// System.out.println("异步发送消息返回的结果是:");
// }
// }
// });
}
}
消费者
package com.xxx.test;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.*;
/**
* @Author xxx
* @Date 2023/1/30 14:25
* @Description kafka消费者测试
* @Version 1.0
*/
public class MyConsumer {
// 主题名
private final static String TOPIC_NAME = "my-topic";
// 分组名
private final static String CONSUMER_GROUP_NAME = "my-group";
public static void main(String[] args) {
// 集合,存放Kafka配置项
Properties props = new Properties();
// kafka的主机和端口,集群的话可写多个“主机:端口”,用逗号隔开(注意检查Linux的防火墙是否开启了)
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.190.132:9092");
// 消费分组名
props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
// 消费者poll到消息后默认情况下,会自动向broker的_consumer_offsets主题提交当前主题-分区消费的偏移量。
// 自动提交会丢消息:因为如果消费者还没消费完下来的消息就自动提交了偏移量,那么此时消费者挂了,于是下一个消费者会从已提交的offset的
// 下一个位置开始消费消息。之前未被消费的消息就丢失掉了。
// 设置是否自动提交offset,默认是TRUE
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
// 若是自动提交,设置自动提交offset的间隔时间
// props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
// consumer给broker发送心跳的间隔时间
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);
// kafka如果超过10秒没有收到消费者的心跳,则会把消费者踢出消费组,进行rebalance,把分区分配给其他消费者
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);
// 设置一次poll最大拉取的条数,可以根据消费速度的快慢设置
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
// 如果两次poll的时间如果超出了30s的时间间隔,kafka会认为其消费能力过弱,将其踢出消费组。将分区分配给其他消费者。触发rebalance机制,重平衡
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
// 当消费主题的是一个新的消费组,或者指定offset的消费方式,offset不存在,那么应该如何消费
// latest(默认):只消费自己启动之后发送到主题的消息
// earliest:第一次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer,seekToBeginning(每次都从头开始消费)
// props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
// 将要接收的消息的键值对都进行序列化处理,将key和value由字符串序列化为字节数组
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 创建一个消费者的客户端
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 消费者订阅主题列表
consumer.subscribe(Arrays.asList(TOPIC_NAME));
// 也可以消费指定分区
// 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消费
// consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
// consumer.seek(new TopicPartition(TOPIC_NAME, 0), 3);
// 从指定时间开始消费
// List<PartitionInfo> topicPartitions = consumer.partitionsFor(TOPIC_NAME);
// // 从1小时前开始消费
// long fetchDataTime = System.currentTimeMillis() - 1000 * 60 * 60;
// Map<TopicPartition, Long> map = new HashMap<>();
// for (PartitionInfo par : topicPartitions) {
// map.put(new TopicPartition(TOPIC_NAME, par.partition()), fetchDataTime);
// }
// Map<TopicPartition, OffsetAndTimestamp> parMap = consumer.offsetsForTimes(map);
// for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : parMap.entrySet()) {
// TopicPartition key = entry.getKey();
// OffsetAndTimestamp value = entry.getValue();
// if (key == null || value == null) {
// continue;
// }
// Long offset = value.offset();
// System.out.println("partition-" + key.partition() + "\toffset-" + offset);
// // 根据消费里的timestamp确定offset
// if (value != null) {
// consumer.assign(Arrays.asList(key));
// consumer.seek(key, offset);
// }
// }
// 拉取消息的长轮询
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) {// 有消息
// 1 手动同步提交offset,会阻塞到成功(一般用同步提交,因为提交后一般没业务代码了)
consumer.commitSync();
// 2 手动异步提交offset,不会阻塞
// consumer.commitAsync(new OffsetCommitCallback() {
// @Override
// public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
// if (exception != null) {
// System.out.println("Commit failed for " + offsets);
// System.out.println("Commit failed exception: " + exception.getStackTrace());
// }
// }
// });
}
}
}
}
二、SpringBoot里的用法
依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
yml配置
server:
port: 8080
spring:
kafka:
bootstrap-servers: 192.168.190.132:9092
producer: #生产者(实际中一般生产者和消费者不会同时出现)
retries: 3 #设置大于0的值,则客户端会将发送失败的记录重新发送
batch-size: 16384
buffer-memory: 33554432
acks: 1
#进行消息key和value的序列化
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer: #消费者(实际中一般生产者和消费者不会同时出现)
group-id: default-group
enable-auto-commit: false
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
max-poll-records: 500
listener:
# 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交 RECORD
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交 BATCH
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交 TIME
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交 COUNT
# TIME | COUNT 有一个条件满足时提交 COUNT_TIME
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,手动调用Acknowledgment,acknowledge()后提交 MANUAL
# 手动调用Acknowledgment.acknowledge()后立即提交,一般使用这种 MANUAL_IMMEDIATE
ack-mode: MANUAL_IMMEDIATE
生产者
package com.xxx.kafkademo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author xxx
* @Date 2023/1/30 16:46
* @Description
* @Version 1.0
*/
@RestController
@RequestMapping("/msg")
public class MyProducer {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
private static final String TOPIC_NAME = "my-topic";
@RequestMapping("/send")
public String sendMessage() {
kafkaTemplate.send(TOPIC_NAME, 0, "myKey001", "msg...");
return "success";
}
}
消费者
简单配置
package com.xxx.kafkademo.consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
/**
* @Author yangpan
* @Date 2023/1/30 16:58
* @Description
* @Version 1.0
*/
@Component
public class MyConsumer {
@KafkaListener(topics = "my-topic", groupId = "MyGroup")
public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
String value = record.value();
System.out.println(value);
System.out.println(record);
// 手动提交offset
ack.acknowledge();
}
}
详细配置
package com.xxx.kafkademo.consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.annotation.PartitionOffset;
import org.springframework.kafka.annotation.TopicPartition;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
/**
* @Author yangpan
* @Date 2023/1/30 16:58
* @Description
* @Version 1.0
*/
@Component
public class MyConsumer {
@KafkaListener(groupId = "testGroup", topicPartitions = {
@TopicPartition(topic = "topic1", partitions = {"0", "1"}),
@TopicPartition(topic = "topic2", partitions = "0", partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))
}, concurrency = "3")// concurrency就是同组下的消费者个数,就是并发消费数,建议小于等于分区总数
public void listenGroup2(ConsumerRecord<String, String> record, Acknowledgment ack) {
String value = record.value();
System.out.println(value);
System.out.println(record);
// 手动提交offset
ack.acknowledge();
}
}