kafka安装
3.7之前kafka依赖于zookeeper,所以安装kafka的同时,也需要安装zookeeper,下面主要通过docker拉取镜像安装
从3.7开始可以采用KRaft方式启动和搭建集群
拉取zookeeper
拉取镜像
docker pull zookeeper:3.4.14
创建容器
docker run -d --name zookeeperCOntainer -p 2181:2181 zookeeper:3.4.14
拉取kafka
kafka2.0
下面拉取的kafka镜像是2.3.1版本的,截至2024-04目前最新的版本是3.6.2
拉取镜像
docker pull wurstmeister/kafka:2.12-2.3.1
创建容器
docker run -d --name kafka \
--env KAFKA_ADVERTISED_HOST_NAME=10.114.15.245 \
--env KAFKA_ZOOKEEPER_CONNECT=10.114.15.245:2181 \ # zookeeper注册地址ip:port
--env KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://10.114.15.245:9092 \ # kafka注册地址,即注册给zookeeper的地址
--env KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \ #kafka监听的地址
--env KAFKA_HEAP_OPTS="-Xmx256M -Xms256M" \ #kafka的最大堆内存
--net=host wurstmeister/kafka:2.12-2.3.1
kafka3.7
kafka3.7开始可以采用
KRaft方式启动Kafka,可以不使用zookeeper
拉取镜像
docker pull apache/kafka:3.7.0
创建容器
docker run -d --name kafka -p 9092:9092 apache/kafka:3.7.0
server.properties
kafka的配置文件位于
/etc/kafka/docker/server.properties,通过docker命令进入容器内部如果要更改容器内的配置文件:
- 将容器内的文件拷贝到主机
- 将拷贝到主机的文件更改后替换回容器内部
- 或者启动容器时通过环境变量改变默认的文件配置
将advertisedlisteners的ip更改为你的公网ip,更多详情请参考其他文章
listeners=PLAINTEXT://192.168.0.213:9092 advertised.listeners=PLAINTEXT://x.x.x.x:9092
kafka可视化管理工具
Kafka-King现代化GUI: github.com/Bronya0/Kaf…
Topic
使用kafka的第一件事就是创建个主题
Topic,主题类似于文件夹,主题内部数据称作事件Event,相当于文件夹内部的文件
- Topic内部有很多分区
PartitionPartition内部存放Event时间默认创建的Topic只有一个分区
kafka快速入门
创建maven工程模块
引入依赖
注意依赖的客户端版本要与Kafka版本一致
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.3.1</version>
</dependency>
生产者代码
package kafka;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
public class kafkaTest {
public static void main(String[] args) {
//1.kafka配置信息
Properties properties = new Properties();
//kafka的连接地址
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
//发送失败,失败的重试次数
properties.put(ProducerConfig.RETRIES_CONFIG, 5);
//消息key的序列化器
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
//消息value的序列化器
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
//2.生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//封装发送的消息
ProducerRecord<String, String> record = new ProducerRecord<String, String>("itheima-topic", "100001", "hello kafka");
//3.发送消息
producer.send(record);
//4.关闭消息通道,必须关闭,否则消息发送不成功
producer.close();
}
}
消费者代码
package kafka;
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 java.time.Duration;
import java.util.Collections;
import java.util.Properties;
/**
* 消费者
*/
public class ConsumerQuickStart {
public static void main(String[] args) {
//1.添加kafka的配置信息
Properties properties = new Properties();
//kafka的连接地址
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
//消费者组
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "group2");
//消息的反序列化器
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
//也可以采用下面的写法获取序列化器的全路径
//properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
//2.消费者对象
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
//3.订阅主题
consumer.subscribe(Collections.singletonList("itheima-topic"));
//当前线程一直处于监听状态
while (true) {
//4.获取消息
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord.key());
System.out.println(consumerRecord.value());
}
}
}
}
卡号:CardabcdEZkZNP5aZZkDnLO3j5Q
SpringBoot整合Kafka
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
配置
spring:
application:
name: Kafka-demo-1
# kafka 连接地址(ip+post)
kafka:
bootstrap-servers: 10.114.15.245:9092
# 配置生产者
producer:
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
key-serializer: org.apache.kafka.common.serialization.StringSerializer
# 确认机制 all 1 0 -1 leader
acks: "1"
# 配置消费者
consumer:
# 偏移量重置设置-最早的偏移量的
auto-offset-reset: earliest
# 一次拉取的最大数量
max-poll-records: 20
enable-auto-commit: false # 关闭自动提交,手动提交偏移量
# 值的反序列化
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
template:
# 指定template默认发送方法(sendDefault)的默认topic
default-topic: "test-topic"
listener:
# 手动消息确认
ack-mode: manual
type: batch
生产者
生产者通过KafkaTemplate发送消息,其内置多种方法,跟RedisTemplate类似
demo
package com.tzf.kafka.producer;
import jakarta.annotation.Resource;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
@Component
public class TestProducer {
@Resource private KafkaTemplate<String, String> kafkaTemplate;
public void send() {
kafkaTemplate.send("test-topic", "{'topic':'test-topic','msg':'今天天气真好!'}");
}
/** 使用 MessageBuilder 构建消息 */
public void sendMessage() {
Message<String> message =
MessageBuilder.withPayload("晚上去外面吃饭").setHeader(KafkaHeaders.TOPIC, "test-topic").build();
kafkaTemplate.send(message);
}
/** 使用ProducerRecord 构建消息发送 **/
public void sendProducerRecord() {
Headers headers = new RecordHeaders();
headers.add("name", "tzf".getBytes());
headers.add("age", "17".getBytes());
ProducerRecord<String, String> record =
new ProducerRecord<>("test-topic", 0, System.currentTimeMillis(), "key", "value", headers);
kafkaTemplate.send(record);
}
}
发送结果
发送结果返回的是用
CompletableFuture<SendResult<K,V>>类,而具体的响应类型是SendResult<K,V>其内部的参数如下
非阻塞式获取
通过对
CompletableFuture注册回调函数对成功发送数据后做处理,可注册的如下:
- thenAccept()
- thenRun()
- thenApply()
public void send() {
CompletableFuture<SendResult<String, String>> result =
kafkaTemplate.send("test-topic", "{'topic':'test-topic','msg':'今天天气真好!'}");
result.thenAccept(
(item) -> {
RecordMetadata recordMetadata = item.getRecordMetadata();
if (recordMetadata != null) {
System.out.println("消息发送成功");
} else {
System.out.println("消息发送失败");
}
});
}
序列化配置
值序列化配置
默认使用的
StringSerializer,如果序列化对象为json可以使用JsonSerializer
可选的序列化器如下:
消费者
消费者主要
@KafkaListener注解标记方法,方法内的参数即该注解会自动把监听到的消息注入到方法参数里其中该注解较重要的两个参数分别是
- topics-->监听的topic,是string数组,也可以使用另外两个定义topic的参数
topicPattern和topicPartitions- groupId-->消费者id
demo
package com.tzf.kafka.consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class TestConsumer {
@KafkaListener(topics = "test-topic", groupId = "test-group")
public void receive(String event) {
log.info("receive event: {}", event);
}
}
获取消息
下面是在接受到消息时,用于获取消息内容的注解
@Payload-->获取消息体的内容@Header-->获取消息头等信息,列入topic,partition等@KafkaListener(topics = "test-topic", groupId = "test-group") public void receive(@Payload String event, @Header(value = KafkaHeaders.RECEIVED_TOPIC) String topic) { log.info("receive event: {},topic:{}", event, topic); }其中
@Header的value可以通过KafkaHeaders接口内部定义的常量头获取部分头名称
@Payload也可以用在ConsumserRecord上,该类内部包含所有的返回信息(key/value)@KafkaListener(topics = "test-topic", groupId = "test-group") public void receive( @Payload ConsumerRecord<String, String> record) { String event = record.value(); String topic = record.topic(); log.info("receive event: {},topic:{}", event, topic); }
手动确认消息
开启手动消息确认,默认是自动消息确认
ack通过在监听方法上注入
org.springframework.kafka.support.Acknowledgment再通过
acknowledge()方法手动确认消息已收到
spring:
kafka:
listener:
# 手动消息确认
ack-mode: manual
public class TestConsumer {
@KafkaListener(topics = "test-topic", groupId = "test-group")
public void receive(
@Payload ConsumerRecord<String, String> record, Acknowledgment acknowledgment) {
String event = record.value();
String topic = record.topic();
log.info("receive event: {},topic:{}", event, topic);
// 手动确认消息
acknowledgment.acknowledge();
}
}
重置offset
通过调整consumer的偏移量重置策略
- earliest-->最早的
- latest-->最晚的
- none-->消费者组如果没有偏移量,则抛出异常
- exception-->kafka不支持
当一个consumer已经消费过一个Event后,它的偏离量会被记录,此时要读取最早的消息,只能
手动重置偏移量或者创建新的消费者通过kafka提供的
sh脚本执行如下命令,脚本位于kafka的bin目录下bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --topic <topic-name> --group <group-name> --reset-offsets --to-earliest --execute # latest bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --topic <topic-name> --group <group-name> --reset-offsets --to-latest --execute
spring:
application:
name: Kafka-demo-1
# kafka 连接地址(ip+post)
kafka:
bootstrap-servers: 10.114.15.245:9092
# 配置生产者
# 配置消费者
consumer:
# 偏移量重置设置-最早的偏移量的
auto-offset-reset: earliest
指定topic,partition等消费
1、groupId:消费组ID;
2、topicPartitions:可配置更加详细的监听信息,可指定topic、parition、offset监听;
含义:监听topic的0号分区,同时监听topic的1号分区和2号分区里面offset从3开始的消息;
注意:topics和topicPartitions不能同时使用;
@KafkaListener(
groupId = "test-group",
topicPartitions = {
@TopicPartition(
topic = "test-topic",
partitions = {"0"},
partitionOffsets = {
@PartitionOffset(initialOffset = "3", partition = "1"),
@PartitionOffset(initialOffset = "3", partition = "2")
})
})
public void receive(
@Payload ConsumerRecord<String, String> record, Acknowledgment acknowledgment) {
String event = record.value();
String topic = record.topic();
log.info("receive event: {},topic:{}", event, topic);
// 手动确认消息
acknowledgment.acknowledge();
}
批量消费消息
通过设置监听方式为
batch,再设置消费者的单次消费数量;接收消息时再通过用List来接收;
# 设置批量消费
spring:
kafka:
listener:
type: batch
# 批量消费每次最多消费多少条消息
consumer:
max-poll-records: 100
@KafkaListener(groupId = "helloGroup", topics = "helloTopic")
public void onEvent(List<ConsumerRecord<String, String>> records) {
System.out.println("批量消费,records.size() = " + records.size() + ",records = " + records);
}
消息转发
消息转发就是应用A从TopicA接收到消息,经过处理后转发到TopicB,再由应用B监听接收该消息,即一个应用处理完成后将该消息转发至其他应用处理;
通过
@SendTo()注解指定发送到哪个topic中去
@KafkaListener(topics = {"topicA"})
@SendTo(value={"topicB"})
public String onEvent(ConsumerRecord<String, String> record) {
return record.value() + "-forward message";
}
自定义
分区自定义
分区主要是通过实现
org.apache.kafka.clients.producer.Partitioner接口,来自定义自己的分区策略值得注意的是分区的分区方法会执行两次,所以在实现轮询算法的时候会有问题
package org.apache.kafka.clients.producer; public interface Partitioner extends Configurable, Closeable { //**** }
拦截器自定义
拦截器主要是producer发送数据时执行的拦截器链,我们同样可以通过实现提供的接口,来实现自定义的拦截器,去做额外的业务操作
接口:
org.apache.kafka.clients.producer.ProducerInterceptorpackage org.apache.kafka.clients.producer; public interface ProducerInterceptor<K, V> extends Configurable, AutoCloseable { }
自定义Topic
我们可以通过
NewTopic创建spring类,交由spring管理package org.apache.kafka.clients.admin; public class NewTopic { }主要参数有如下三个
- topic-->topic名称
- numPartitions-->分区数量
- replicationFactor-->副本因子,不能超过节点数量
样例:
@Bean
public NewTopic customTopic() {
// 这里副本数量我们只能选择1个,因为是单节点的kafka集群
return new NewTopic("customTopic", 3, (short) 1);
}
自定义KafkaTemplate
当我们实现自定义分区和拦截器时,发现通过配置文件无法配置,这时我们需要通过创建新的
KafkaTemplate来注入我们实现的拦截器和分区等
@Configuration
public class KafkaConfig {
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>(16);
// 配置服务器地址
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
// 配置key序列化
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, keySerializer);
// 配置value序列化
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, valueSerializer);
// 配置自定义分区
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, CustomKafkaPartition.class);
// 配置producer拦截器
props.put(
ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, CustomKafkaProducerInterceptor.class.getName());
return props;
}
/**
* @description: 创建生产者工厂,注入配置属性
* @author 86187
* @return org.springframework.kafka.core.ProducerFactory<java.lang.String,java.lang.String>
*/
public ProducerFactory<String, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
/**
* @description: 根据自定义的配置创建kafkaTemplate,内部包含自定义分区和拦截器
* @author 86187
* @return org.springframework.kafka.core.KafkaTemplate<java.lang.String,java.lang.String>
*/
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
KafkaTemplate<String, String> stringStringKafkaTemplate = new KafkaTemplate<>(producerFactory());
// 默认分区在template上设置
stringStringKafkaTemplate.setDefaultTopic(defaultTopic);
return stringStringKafkaTemplate;
}
}