✅ Kafka 启动方式与环境搭建总结笔记
到官网下载,然后上传到虚拟机,不了解的建议先学linux,学习顺序很重要
一、启动方式概述
| 启动模式 | 描述 | 说明 |
|---|---|---|
| 基于 Zookeeper 启动 | 传统启动方式 | 需要依赖独立的 Zookeeper 服务(默认端口:2181) |
| 基于 KRaft 启动 | Kafka 自主共识机制 | Kafka 3.x 推荐方式,逐步替代 Zookeeper |
⚠ 两种方式只能选择一种使用,不能同时使用!
二、Kafka 启动与关闭命令
1️⃣ 使用 Zookeeper 模式启动 Kafka
下载kafka自带Zookeeper,如果发现没有,可能上传丢失包,重新下载上传
1. 启动 Zookeeper
cd bin/
./zookeeper-server-start.sh ../config/zookeeper.properties &
2. 启动 Kafka 服务
./kafka-server-start.sh ../config/server.properties &
3. 停止服务
./kafka-server-stop.sh ../config/server.properties
./zookeeper-server-stop.sh ../config/zookeeper.properties
🔍 查看进程/端口情况
ps -ef | grep zookeeper
netstat -nlpt
三、Zookeeper 单独部署说明(推荐独立部署)
1. 下载与启动
cd apache-zookeeper-3.8.3-bin/bin/
./zkServer.sh start
2. 配置 Zoo.cfg(复制样例配置)
cd apache-zookeeper-3.8.3-bin/conf
cp zoo_sample.cfg zoo.cfg
3. 修改端口避免冲突
# zoo.cfg 文件添加或修改
admin.serverPort=8888 # 默认8080易冲突,建议修改
四、KRaft 模式启动 Kafka(无 Zookeeper)
1️⃣ 生成集群 UUID
./kafka-storage.sh random-uuid
或查看 UUID:
./kafka-storage.sh info -C ../config/server.properties
2️⃣ 格式化元数据目录
./kafka-storage.sh format -t <集群UUID> -c ../config/kraft/server.properties
-
注意这里可以自定义uuid,但是上一次生产的uuid是保存了的,得先删除kraft-combined-logs里面的文件
-
可以看一下
。/kafka-storage.sh info -c ../config/kraft/server.properties
示例:
./kafka-storage.sh format -t MQqryG5apsSGGiJC0tR4ssDg -c ../config/kraft/server.properties
3️⃣ 启动 Kafka(KRaft)
./kafka-server-start.sh ../config/kraft/server.properties &
4️⃣ 停止服务
./kafka-server-stop.sh ../config/kraft/server.properties
五、Docker 启动 Kafka(快速方式)
1. 拉取镜像
docker pull apache/kafka:3.9.0
2. 启动容器
# 前台启动
docker run -p 9092:9092 apache/kafka:3.9.0
# 后台启动(推荐)
docker run -d -p 9092:9092 apache/kafka:3.9.0
六、Kafka 启动注意事项
- ✅ 必须安装 Java 8+
- ✅ 启动 Kafka 必须选择:Zookeeper or KRaft 二选一
- ✅ Zookeeper 默认占用 8080,建议在
zoo.cfg中改为其他端口(如 8888) - ✅ Kafka 3.x 推荐使用 KRaft,自带分布式共识机制,简化部署
✅ Kafka 基础操作
一、Topic 基本操作
✅ 1. 创建与管理 Topic(主题)
-
Topic 概念:类似文件夹,消息生产者将事件写入主题,消费者从主题中读取事件。
-
常用命令:
# 查看脚本说明 ./kafka-topics.sh # 创建主题 ./kafka-topics.sh --create --topic quickstart-events --bootstrap-server localhost:9092 # 列出所有主题 ./kafka-topics.sh --list --bootstrap-server localhost:9092 # 删除主题 ./kafka-topics.sh --delete --topic quickstart-events --bootstrap-server localhost:9092 # 查看主题详细信息 ./kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092 # 修改主题分区数 ./kafka-topics.sh --alter --topic quickstart-events --partitions 5 --bootstrap-server localhost:9092
二、生产与消费事件(Events)
✅ 2. 写入事件(Producer)
# 使用 Producer 向主题发送消息
./kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092
- 一次输入一条消息,按回车发送。
Ctrl+C退出。
✅ 3. 消费事件(Consumer)
# 从主题读取消息(包含历史消息)
./kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
⚠ 注意:“--from-beginning” 表示从最开始读取历史消息,不是只读最新消息!
三、Kafka Docker 部署连接问题解决
✅ 4. 外部环境无法连接 Kafka 的解决办法
- Kafka 默认监听
localhost:9092,Docker 启动后外部连接失败。 - 需修改 Kafka 的配置文件:
# server.properties配置修改项
listeners=PLAINTEXT://0.0.0.0:9092
advertised.listeners=PLAINTEXT://192.168.11.128:9092
0.0.0.0是保留地址,表示监听所有IP。
✅ 5. 修改 Kafka 容器配置文件流程:
# 进入Kafka容器
docker exec -it <container_id> /bin/bash
# 找到配置文件路径
cd /etc/kafka/docker
# 拷贝配置文件到主机
docker cp <container_id>:/etc/kafka/docker/server.properties ./
# 修改server.properties后挂载运行
docker run -d \
--volume /opt/kafka/docker:/mnt/shared/config \
-p 9092:9092 \
apache/kafka:3.9.0
四、Kafka 图形界面工具
✅ 6. 常见可视化管理工具
| 工具名称 | 官网链接 | 备注 |
|---|---|---|
| Offset Explorer | www.kafkatool.com/ | 原名 Kafka Tool |
| CMAK | github.com/yahoo/CMAK | 原名 Kafka Manager |
| EFAK | www.kafka-eagle.org/ | 原名 Kafka Eagle,由国人开发 |
五、CMAK(Kafka Manager)配置与使用
✅ CMAK 配置
-
修改
conf/application.conf:kafka-manager.zkhosts="192.168.11.128:2181" cmak.zkhosts="192.168.11.128:2181"
✅ 启动命令
cd bin
./cmak -Dconfig.file=../conf/application.conf -java-home /usr/local/jdk-11.0.22
✅ 访问地址
http://192.168.11.128:9000/
📌 注意:CMAK仅支持基于Zookeeper启动的Kafka集群!
六、EFAK(Kafka Eagle)配置与使用
✅ 下载与解压
wget https://github.com/smartloli/kafka-eagle-bin/archive/v3.0.1.tar.gz
tar -zxvf kafka-eagle-bin-3.0.1.tar.gz
cd kafka-eagle-bin-3.0.1
tar -zxvf efak-web-3.0.1-bin.tar.gz
cd efak-web-3.0.1
✅ 修改配置文件(conf/system-config.properties):
cluster1.zk.list=127.0.0.1:2181
efak.driver=com.mysql.cj.jdbc.Driver
efak.url=jdbc:mysql://127.0.0.1:3306/ke?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
efak.username=root
efak.password=123456
✅ 配置环境变量:
vi /etc/profile
# 添加以下内容
export KE_HOME=/usr/local/efak-web-3.0.1
export PATH=$KE_HOME/bin:$PATH
source /etc/profile
✅ 启动与访问
cd $KE_HOME/bin
./ke.sh start # 启动
./ke.sh stop/status # 管理命令
访问地址:http://192.168.11.128:8048/
登录账号:admin 密码:123456
仅支持zookeeper,说是支持kcraft,但其实没有
Spring Boot集成Kafka生产者相关开发
一、Spring-Kafka依赖
在pom.xml文件中添加如下依赖配置:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
二、Kafka配置
在application.yml配置文件中添加Kafka配置:
# kafka连接地址(ip+port)
kafka:
bootstrap-servers: 192.168.11.128:9092
# 配置消费者(有24个配置项,此处展示部分)
consumer:
auto-offset-reset: latest
template:
default-topic: default-topic
说明:
bootstrap-servers:指定Kafka的连接地址。consumer.auto-offset-reset:设置消费者的偏移量重置策略为latest,即从最新偏移量开始消费。template.default-topic:设置默认主题。
三、生产者发送消息方法
会自动创建topic,消费也会自动创建topic
-
普通发送方法:
public void sendEvent() { kafkaTemplate.send("quickstart-events", "hello=kafka"); }或:
public void sendEvent() { kafkaTemplate.send("quickstart-events", 0, System.currentTimeMillis(), "k2", "hello=kafka"); } -
消息体构建方式:
public void sendEvent2() { Message<String> message = MessageBuilder.withPayload("hello") .setHeader(KafkaHeaders.TOPIC, "quickstart-events") .build(); kafkaTemplate.send(message); } -
带Headers的发送:
public void sendEvent3() { Headers headers = new RecordHeaders(); headers.add("phone", "123".getBytes(StandardCharsets.UTF_8)); headers.add("email", "123".getBytes(StandardCharsets.UTF_8)); ProducerRecord<String, String> record = new ProducerRecord<>( "quickstart-events", 0, System.currentTimeMillis(), "k1", "hello-kafka", headers ); } -
使用默认主题发送:
public void sendEvent5() { kafkaTemplate.sendDefault(0, System.currentTimeMillis(), "k1", "hello"); }
区别:kafkaTemplate.send(...) 和 kafkaTemplate.sendDefault(...)
- 主题指定:
send()方法需要明确指定目标主题,而sendDefault()则使用配置中的默认主题。 - 适用场景:
send()适合动态确定主题的场景,sendDefault()适合固定主题的场景。 {
四、获取生产者消息发送结果
- 返回值类型:
send()和sendDefault()方法返回CompletableFuture<SendResult<K, V>>。 - 获取方式:
-
方式一:阻塞获取:
public void sendEvent6() { CompletableFuture<SendResult<String, String>> completableFuture = kafkaTemplate.send("qui", "hello"); try { SendResult<String, String> result = completableFuture.get(); if (result.getRecordMetadata() != null) { System.out.println(result.getRecordMetadata().topic()); } } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } -
方式二:回调获取:
public void sendEvent6() { CompletableFuture<SendResult<String, String>> completableFuture = kafkaTemplate.send("qui", "hello"); completableFuture.thenAccept(result -> { if (result != null && result.getRecordMetadata() != null) { System.out.println("✅ 发送成功!"); System.out.println("Topic: " + result.getRecordMetadata().topic()); } }); completableFuture.exceptionally(ex -> { System.err.println("❌ 发送失败!"); ex.printStackTrace(); return null; }); System.out.println("🚀 sendEvent6 方法调用结束(非阻塞发送)"); }
-
五、KafkaTemplate的注入
-
通过
@Resource注解注入:@Resource private KafkaTemplate<String, String> kafkaTemplate; @Resource private KafkaTemplate<String, Object> kafkaTemplate2; -
通过
@Bean注解创建:@Bean @ConditionalOnMissingBean(KafkaTemplate.class) public KafkaTemplate<?, ?> kafkaTemplate(ProducerFactory<Object, Object> kafkaProducerFactory, ProducerListener<Object, Object> kafkaProducerListener, ObjectProvider<RecordMessageConverter> messageConverter) { KafkaTemplate<Object, Object> kafkaTemplate = new KafkaTemplate<>(kafkaProducerFactory); messageConverter.ifUnique(kafkaTemplate::setMessageConverter); kafkaTemplate.setProducerListener(kafkaProducerListener); kafkaTemplate.setDefaultTopic(this.properties.getTemplate().getDefaultTopic()); return kafkaTemplate; }
六、消息序列化
-
问题:直接发送对象消息会导致序列化异常。
-
解决方案:
- 使用
JsonSerializer来将对象序列化为字节数组:
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer- 使用
ToStringSerializer将对象通过toString()转换为字节数组:
value-serializer: org.springframework.kafka.support.serializer.ToStringSerializer - 使用
七、Kafka核心概念 - 副本(Replica)
- 作用:副本用于备份数据,防止节点故障时数据丢失。
- 分类:
- Leader Replica:主副本,生产者发送和消费者消费数据都来自该副本。
- Follower Replica:从副本,实时同步Leader副本数据,当Leader故障时可能成为新的Leader副本。
- 注意事项:副本数量不能为0,不能超过节点数量,否则无法创建主题。
八、指定主题的分区和副本
-
方式一:命令行指定: 使用命令行工具创建主题时指定分区和副本:
./kafka-topics.sh --create --topic myTopic --partitions 3 --replication-factor 1 --bootstrap-server 127.0.0.1:9092 -
方式二:代码指定:
@Configuration public class KafkaConfig { @Bean public NewTopic newTopic() { return new NewTopic("heTopic", 5, (short) 1); } }
九、生产者配置相关代码
-
创建生产者工厂:
public ProducerFactory<String, ?> producerFactory() { return new DefaultKafkaProducerFactory<>(producerConfigs()); } -
创建KafkaTemplate:
public KafkaTemplate<String, ?> kafkaTemplate() { return new KafkaTemplate<>(producerFactory()); } -
生产者配置属性:
public Map<String, Object> producerConfigs() { Map<String, Object> props = new HashMap<>(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, keySerializer); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, valueSerializer); props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, RoundRobinPartitioner.class); return props; }
十、生产者发送消息的分区策略
- 默认分配策略:使用
DefaultPartitioner,无键时随机分配分区;有键时根据键的哈希值对分区数取模分配。 - 轮询分配:在config中加入
RoundRobinPartitioner.class或者RoundRobinPartitioner.class.getName() - 自定义分区策略:
- 创建实现
Partitioner接口的类,例如CustomerPartitioner。 - 在
partition()方法中实现分区逻辑。
- 创建实现
十一、拦截生产者发送的消息
-
拦截原理:实现
ProducerInterceptor<K, V>接口。 -
示例拦截器:
onSend(ProducerRecord record):消息发送前调用,可处理消息或记录日志。onAcknowledgement(RecordMetadata metadata, Exception exception):消息被确认接收时调用。
-
配置拦截器: 在
producerConfigs()方法中,通过ProducerConfig.INTERCEPTOR_CLASSES_CONFIG添加拦截器:props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, "com.example.kafka.interceptor.CustomerProducerInterceptor"); // CustomerProducerInterceptor.getName()
Kafka 消费者相关笔记
基本消费
消费者通过@KafkaListener注解监听特定主题的消息。基本用法如下:
@Component
public class EventConsumer {
@KafkaListener(topics = {"quickstart-events"}, concurrency="3" groupId = "hello-group")
public void onEvent(String message) {
System.out.println("读取到的" + message);
}
}
使用@Payload和@Header
@Payload:表示参数接受的是消息体的内容。@Header:用于从请求头接收内容。
示例代码:
@KafkaListener(topics = {"quickstart-events"}, groupId = "hello-group")
public void onEvent(@Payload String message,
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
@Header(KafkaHeaders.RECEIVED_PARTITION) String partition) {
System.out.println("读取到的" + message + ", topic: " + topic + ", partition: " + partition);
}
使用ConsumerRecord<String,String> record参数可以获取全部信息,即使加上@Payload也不会报错,但不规范。
反序列化问题与解决方案
在处理对象消息时,可能会遇到反序列化失败的问题,表现为JsonMappingException,提示类不在信任的包中。
解决方案:
- 发送端:将对象转换为JSON字符串。
String userJSON = JSONUtils.toJSON(user); - 接收端:将JSON字符串转换回对象。
User user = JSONUtils.toBean(userJSON, User.class);
工具类JSONUtils:
package com.bjpowernode.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JSONUtils {
private static final ObjectMapper OBJECTMAPPER = new ObjectMapper();
public static String toJSON(Object object) {
try {
return OBJECTMAPPER.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public static <T> T toBean(String json, Class<T> clazz) {
try {
return OBJECTMAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
读取占位符配置
可以通过YML文件中的占位符来动态配置监听的主题和组名:
kafka:
topic:
name: helloTopic
consumer:
name: helloGroup
@Component
public class EventConsumer {
@KafkaListener(topics = {"${kafka.topic.name}"}, groupId = "${kafka.consumer.name}")
public void onEvent(String message) {
System.out.println("读取到的" + message);
}
}
手动确认消息
默认情况下,Kafka自动确认消息。若希望手动确认消息,需配置手动确认模式,并在代码中调用ack.acknowledge():
配置:
kafka:
listener:
ack-mode: manual
代码示例:
@KafkaListener(topics = {"quickstart-events"}, groupId = "hello-group")
public void onEvent(@Payload String message,
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
@Header(KafkaHeaders.RECEIVED_PARTITION) String partition,
ConsumerRecord<String,String> record,
Acknowledgment ack) {
System.out.println("读取到的" + message + ", topic: " + topic + ", partition: " + partition);
System.out.println("读取到的" + record.toString());
ack.acknowledge(); // 手动确认消息
}
监听指定分区
可以监听指定分区的消息,包括初始偏移量设置:
@KafkaListener(groupId = "${kafka.consumer.name}",
topicPartitions = {
@TopicPartition(
topic = "${kafka.topic.name}",
partitions={"0","1","2"},
partitionOffsets = {
@PartitionOffset(partition = "3",initialOffset = "2"),
@PartitionOffset(partition = "4",initialOffset = "2")
}
)
})
public void onEvent(@Payload String message,
@Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
@Header(KafkaHeaders.RECEIVED_PARTITION) String partition,
ConsumerRecord<String,String> record,
Acknowledgment ack) {
System.out.println("读取到的" + message + ", topic: " + topic + ", partition: " + partition);
System.out.println("读取到的" + record.toString());
ack.acknowledge();
}
批量接收消息
批量接收消息需要配置max-poll-records并修改监听器方法签名:
spring:
kafka:
listener:
type: batch
consumer:
max-poll-records: 20
@Component
public class EventConsumer {
@KafkaListener(topics = {"${kafka.topic.name}"}, groupId = "${kafka.consumer.name}")
public void onEvent(List<ConsumerRecord<String,String>> records) {
for (ConsumerRecord<String, String> record : records) {
System.out.println("读取到的" + record.value());
}
}
}
消费拦截器
1. 实现ConsumerInterceptor接口
消费拦截器需要实现Kafka的ConsumerInterceptor接口。该接口提供了以下方法:
onConsume(ConsumerRecords<K, V> records):在消息被消费者消费之前执行,可以对消息进行预处理。onCommit(Map<TopicPartition, OffsetAndMetadata> offsets):在消费者提交偏移量之后执行,可以用于日志记录或其他操作。close():关闭拦截器时调用。configure(Map<String, ?> configs):拦截器初始化时调用,主要用于读取配置。
我们主要关注onConsume和onCommit方法,close和configure方法可以根据需求选择性实现。
示例代码:
package com.example.kafka.interceptor;
import org.apache.kafka.clients.consumer.ConsumerInterceptor;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.common.TopicPartition;
import java.util.Map;
public class CustomConsumerInterceptor implements ConsumerInterceptor<String, String> {
@Override
public ConsumerRecords<String, String> onConsume(ConsumerRecords<String, String> records) {
System.out.println("Intercepting consumed records...");
for (ConsumerRecord<String, String> record : records) {
System.out.println("Consumed Record: " + record.key() + " - " + record.value());
}
return records; // 返回修改后的记录(或原样返回)
}
@Override
public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets) {
System.out.println("Committing offsets: " + offsets);
}
@Override
public void close() {
System.out.println("Closing interceptor...");
}
@Override
public void configure(Map<String, ?> configs) {
System.out.println("Configuring interceptor with configs: " + configs);
}
}
2. 注册消费拦截器
消费拦截器需要通过消费者的配置进行注册。可以通过ConsumerFactory的配置将拦截器添加到消费者的属性中。
配置拦截器
props.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, CustomConsumerInterceptor.class.getName());
完整配置示例:
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class KafkaConfig {
private String bootstrapServers = "localhost:9092";
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "test-group");
// 注册自定义拦截器
props.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, CustomConsumerInterceptor.class.getName());
return new DefaultKafkaConsumerFactory<>(props);
}
}
3. 覆盖默认工厂
Spring Boot默认会注入一个消费者工厂和Kafka监听器容器工厂(根据yml文件),但它们不包含自定义拦截器。因此,我们需要手动覆盖这些默认的bean。
覆盖消费者工厂
如果未使用@Configuration和@Bean注解,默认的消费者工厂仍然会被注入,但它不会包含拦截器。因此,必须显式地定义并覆盖消费者工厂。
覆盖Kafka监听器容器工厂
同样,默认的Kafka监听器容器工厂也需要覆盖,以确保它使用我们自定义的消费者工厂。
完整代码:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
@Configuration
public class KafkaListenerConfig {
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory(
ConsumerFactory<String, String> consumerFactory) {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory); // 使用自定义消费者工厂
return factory;
}
}
4. 验证工厂注入情况
启动应用后,可以通过ApplicationContext查看当前注入的消费者工厂和Kafka监听器容器工厂。
示例代码:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import java.util.Map;
@SpringBootApplication
public class KafkaBaseApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(KafkaBaseApplication.class, args);
// 获取所有类型为ConsumerFactory的bean
Map<String, ConsumerFactory> consumerFactories = context.getBeansOfType(ConsumerFactory.class);
consumerFactories.forEach((k, v) -> {
System.out.println("ConsumerFactory Bean Name: " + k + ", Bean Instance: " + v);
});
// 获取所有类型为KafkaListenerContainerFactory的bean
Map<String, KafkaListenerContainerFactory> listenerFactories = context.getBeansOfType(KafkaListenerContainerFactory.class);
listenerFactories.forEach((k, v) -> {
System.out.println("KafkaListenerContainerFactory Bean Name: " + k + ", Bean Instance: " + v);
});
}
}
- 这里的消费者工厂会被覆盖,监听器不会被覆盖,会有两个,一个是我们自己的,一个是默认的
5. 指定消费者工厂
在事件消费者中,需要明确指定使用我们自定义的Kafka监听器容器工厂。
示例代码:
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class EventConsumer {
@KafkaListener(topics = "${kafka.topic.name}", groupId = "${kafka.consumer.group-id}",
containerFactory = "kafkaListenerContainerFactory")
public void onEvent(List<ConsumerRecord<String, String>> records) {
for (ConsumerRecord<String, String> record : records) {
System.out.println("Received message: Key = " + record.key() + ", Value = " + record.value());
}
}
}
6. 总结
- 实现拦截器:实现
ConsumerInterceptor接口,并重写onConsume和onCommit方法。 - 注册拦截器:通过
ConsumerFactory的配置将拦截器类名添加到INTERCEPTOR_CLASSES_CONFIG中。 - 覆盖默认工厂:手动定义并覆盖消费者工厂和Kafka监听器容器工厂,确保它们包含自定义拦截器。
- 指定工厂名称:在事件消费者中通过
containerFactory属性指定自定义的Kafka监听器容器工厂。
消息转发
使用@SendTo注解实现消息转发:
@Component
@SendTo(value = "bt")
public class EventConsumer {
@KafkaListener(topics = {"at"}, groupId = "ag", containerFactory = "ourKafkaListenerContainerFactory")
public String onEvent(ConsumerRecord<String,String> record) {
System.out.println("读取到的" + record.value());
return record.value();
}
}
另一个消费者监听转发后的消息:
@Component
public class EventConsumer {
@KafkaListener(topics = {"bt"}, groupId = "bg", containerFactory = "ourKafkaListenerContainerFactory")
public void onEvent(ConsumerRecord<String,String> record) {
System.out.println("读取到的" + record.value());
}
}
Kafka 消费者消费信息的分区策略与 Offset 管理
1. 消费者的分区分配策略
Kafka 提供了多种分区分配策略,用于在消费者组内将主题的分区均匀地分配给消费者。默认的分区分配策略是 RangeAssignor,但也可以选择其他策略或自定义策略。
1.1 默认分区策略:RangeAssignor
- 原理:根据分区数量和消费者数量,按范围分配分区。
- 示例:
- 主题:
myTopic,有 10 个分区(p0 - p9)。 - 消费者组:包含 3 个消费者:
consumer1,consumer2,consumer3。
- 计算每个消费者应得的分区数:
- 分区总数(10) / 消费者数量(3) = 3 ... 余 1。
- 前 1 个消费者多分配 1 个分区。
- 结果:
consumer1分配到 4 个分区(p0-p3)。consumer2和consumer3各分配到 3 个分区(p4-p6 和 p7-p9)。
- 分区分配较为简单,可能导致某些消费者负载不均(如
consumer1负载稍重)。
- 主题:
1.2 其他分区策略
RoundRobinAssignor:- 原理:将所有分区按轮询方式分配给消费者。
- 优点:分配更加均匀,适合分区数量较多的场景。
StickyAssignor:- 原理:尽量保持消费者与分区之间的分配关系不变,减少不必要的分区重分配。
- 优点:适合动态扩缩容场景,避免频繁重新分配分区。
CooperativeStickyAssignor:- 原理:与
StickyAssignor类似,但支持协作式重新平衡,允许消费者在离开消费者组前通知协调器。 - 优点:进一步优化了分区分配的稳定性。
- 原理:与
1.3 自定义分区策略
如果需要更灵活的分区分配逻辑,可以实现自定义的 Partitioner 类。例如:
public class CustomPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if (keyBytes == null) {
return ThreadLocalRandom.current().nextInt(numPartitions);
} else {
return Math.abs(Utils.murmur2(keyBytes)) % numPartitions;
}
}
@Override
public void close() {}
@Override
public void configure(Map<String, ?> configs) {}
}
然后在生产者配置中指定该分区器:
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, CustomPartitioner.class.getName());
2. 消费者 Offset 的管理
2.1 Offset 的定义与作用
- 生产者 Offset:
- Kafka 内部为每条消息分配一个唯一的 Offset,表示消息在分区中的位置。
- 生产者发送消息时,Offset 由 Kafka 自动生成并递增。
- 消费者 Offset:
- 消费者 Offset 用于记录消费者已读取到的位置,确保消费者可以从上次消费的位置继续读取消息。
- 每个消费者组中的消费者独立维护自己的 Offset。
2.2 Offset 的存储
- 存储位置:消费者的 Offset 信息并不存储在分区文件本身,而是保存在一个特殊的 Topic 中,名为
__consumer_offsets。 - 分区规则:
__consumer_offsets默认有 50 个分区。- 每个消费者组的 Offset 信息会根据以下公式分配到某个分区: [ \text{partition} = \text{Math.abs}(\text{"groupid"}.hashCode()) % 50 ]
- 示例:
因此,String groupid = "myGroup"; int partition = Math.abs(groupid.hashCode()) % 50; // 假设结果为 39myGroup的 Offset 信息会保存在__consumer_offsets的第 39 个分区中。
2.3 Offset 的提交
- 自动提交:消费者可以配置为自动提交 Offset,默认时间间隔为 5 秒。
- 手动提交:通过调用
commitSync()或commitAsync()方法手动提交 Offset。 - 更新条件:消费者 Offset 需要在消费消息后提交确认(ACK),否则 Offset 不会更新。
2.4 Offset 的查看
- Zookeeper 插件限制:
- 在 IDEA 中安装 Zookeeper Manager 插件只能查看元数据,无法直接查看消费者组的 Offset。
- 命令行工具:
- 使用
kafka-consumer-groups.sh命令查看消费者组的 Offset 信息:kafka-consumer-groups.sh --bootstrap-server 127.0.0.1:9092 --group myGroup --describe
- 使用
3. Kafka 数据存储与日志文件
3.1 存储路径
- 默认路径:Kafka 的所有事件(消息、数据)都存储在
/tmp/kafka-logs目录中。 - 修改路径:可以通过编辑 Kafka 配置文件
server.properties修改日志存储路径:找到cd /usr/local/kafka_2.13-3.7.0/config vim server.propertieslog.dirs=/tmp/kafka-logs,将其修改为新的路径。
3.2 日志文件类型
- 消息索引文件:
00000000000000000000.index - 消息数据文件:
00000000000000000000.log - 时间戳索引文件:
00000000000000000000.timeindex - 快照文件:
00000000000000000006.snapshot,用于故障恢复。 - 领导者检查点文件:
leader-epoch-checkpoint,记录分区领导者的 Epoch 和起始偏移量。 - 分区元数据文件:
partition.metadata,存储特定分区的元数据。
4. 总结
4.1 分区策略
- Kafka 提供了多种分区分配策略(如
RangeAssignor、RoundRobinAssignor、StickyAssignor等),可根据实际需求选择合适的策略。 - 自定义分区策略可实现更灵活的分区分配逻辑。
4.2 Offset 管理
- 消费者 Offset 信息存储在
__consumer_offsetsTopic 中,而非分区文件本身。 - 使用命令行工具
kafka-consumer-groups.sh可以查看消费者组的 Offset 信息。 - Offset 提交分为自动提交和手动提交两种方式。
4.3 数据存储
- Kafka 将消息以日志文件的形式存储在指定目录中,包括消息索引文件、消息数据文件等。
- 日志文件的命名规则为
<topic_name>-<partition_id>,便于管理和查询。