Java人最优秀的kafka笔记

✅ 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 https://www.kafkatool.com/ 原名 Kafka Tool
CMAK https://github.com/yahoo/CMAK 原名 Kafka Manager
EFAK https://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.kafkagroupId>
    <artifactId>spring-kafkaartifactId>
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

  1. 普通发送方法

    public void sendEvent() {  
        kafkaTemplate.send("quickstart-events", "hello=kafka");  
    }
    

    或:

    public void sendEvent() {  
        kafkaTemplate.send("quickstart-events", 0, System.currentTimeMillis(), "k2", "hello=kafka");  
    }
    
  2. 消息体构建方式

    public void sendEvent2() {  
        Message<String> message = MessageBuilder.withPayload("hello")  
               .setHeader(KafkaHeaders.TOPIC, "quickstart-events")  
               .build();  
        kafkaTemplate.send(message);  
    }
    
  3. 带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  
        );
    }
    
  4. 使用默认主题发送

    public void sendEvent5() {  
        kafkaTemplate.sendDefault(0, System.currentTimeMillis(), "k1", "hello");  
    }
    

区别:kafkaTemplate.send(...)kafkaTemplate.sendDefault(...)

  • 主题指定send()方法需要明确指定目标主题,而sendDefault()则使用配置中的默认主题。
  • 适用场景send()适合动态确定主题的场景,sendDefault()适合固定主题的场景。
    {

四、获取生产者消息发送结果

  1. 返回值类型send()sendDefault()方法返回CompletableFuture>
  2. 获取方式
    • 方式一:阻塞获取

      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的注入

  1. 通过@Resource注解注入

    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;
    @Resource
    private KafkaTemplate<String, Object> kafkaTemplate2;
    
  2. 通过@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;
    }
    

六、消息序列化

  1. 问题:直接发送对象消息会导致序列化异常。

  2. 解决方案

    • 使用JsonSerializer来将对象序列化为字节数组:
    value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
    
    • 使用ToStringSerializer将对象通过toString()转换为字节数组:
    value-serializer: org.springframework.kafka.support.serializer.ToStringSerializer
    

七、Kafka核心概念 - 副本(Replica)

  1. 作用:副本用于备份数据,防止节点故障时数据丢失。
  2. 分类
    • Leader Replica:主副本,生产者发送和消费者消费数据都来自该副本。
    • Follower Replica:从副本,实时同步Leader副本数据,当Leader故障时可能成为新的Leader副本。
  3. 注意事项:副本数量不能为0,不能超过节点数量,否则无法创建主题。

八、指定主题的分区和副本

  1. 方式一:命令行指定: 使用命令行工具创建主题时指定分区和副本:

    ./kafka-topics.sh --create --topic myTopic --partitions 3 --replication-factor 1 --bootstrap-server 127.0.0.1:9092
    
  2. 方式二:代码指定

    @Configuration
    public class KafkaConfig {
        @Bean
        public NewTopic newTopic() {
            return new NewTopic("heTopic", 5, (short) 1);
        }
    }
    

九、生产者配置相关代码

  1. 创建生产者工厂

    public ProducerFactory<String, ?> producerFactory() {
        return new DefaultKafkaProducerFactory<>(producerConfigs());
    }
    
  2. 创建KafkaTemplate

    public KafkaTemplate<String, ?> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }
    
  3. 生产者配置属性

    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;
    }
    

十、生产者发送消息的分区策略

  1. 默认分配策略:使用DefaultPartitioner,无键时随机分配分区;有键时根据键的哈希值对分区数取模分配。
  2. 轮询分配:在config中加入RoundRobinPartitioner.class或者RoundRobinPartitioner.class.getName()
  3. 自定义分区策略
    • 创建实现Partitioner接口的类,例如CustomerPartitioner
    • partition()方法中实现分区逻辑。

十一、拦截生产者发送的消息

  1. 拦截原理:实现ProducerInterceptor接口。

  2. 示例拦截器

    • onSend(ProducerRecord record):消息发送前调用,可处理消息或记录日志。
    • onAcknowledgement(RecordMetadata metadata, Exception exception):消息被确认接收时调用。
  3. 配置拦截器: 在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 record参数可以获取全部信息,即使加上@Payload也不会报错,但不规范。

反序列化问题与解决方案

在处理对象消息时,可能会遇到反序列化失败的问题,表现为JsonMappingException,提示类不在信任的包中。

解决方案

  1. 发送端:将对象转换为JSON字符串。
    String userJSON = JSONUtils.toJSON(user);
    
  2. 接收端:将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 records):在消息被消费者消费之前执行,可以对消息进行预处理。
  • onCommit(Map offsets):在消费者提交偏移量之后执行,可以用于日志记录或其他操作。
  • close():关闭拦截器时调用。
  • configure(Map configs):拦截器初始化时调用,主要用于读取配置。

我们主要关注onConsumeonCommit方法,closeconfigure方法可以根据需求选择性实现。

示例代码:

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. 总结
  1. 实现拦截器:实现ConsumerInterceptor接口,并重写onConsumeonCommit方法。
  2. 注册拦截器:通过ConsumerFactory的配置将拦截器类名添加到INTERCEPTOR_CLASSES_CONFIG中。
  3. 覆盖默认工厂:手动定义并覆盖消费者工厂和Kafka监听器容器工厂,确保它们包含自定义拦截器。
  4. 指定工厂名称:在事件消费者中通过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

    分配步骤

    1. 计算每个消费者应得的分区数
      • 分区总数(10) / 消费者数量(3) = 3 … 余 1。
      • 前 1 个消费者多分配 1 个分区。
      • 结果:
        • consumer1 分配到 4 个分区(p0-p3)。
        • consumer2consumer3 各分配到 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; // 假设结果为 39
      
      因此,myGroup 的 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.properties
    
    找到 log.dirs=/tmp/kafka-logs,将其修改为新的路径。
3.2 日志文件类型
  • 消息索引文件00000000000000000000.index
  • 消息数据文件00000000000000000000.log
  • 时间戳索引文件00000000000000000000.timeindex
  • 快照文件00000000000000000006.snapshot,用于故障恢复。
  • 领导者检查点文件leader-epoch-checkpoint,记录分区领导者的 Epoch 和起始偏移量。
  • 分区元数据文件partition.metadata,存储特定分区的元数据。

4. 总结
4.1 分区策略
  • Kafka 提供了多种分区分配策略(如 RangeAssignorRoundRobinAssignorStickyAssignor 等),可根据实际需求选择合适的策略。
  • 自定义分区策略可实现更灵活的分区分配逻辑。
4.2 Offset 管理
  • 消费者 Offset 信息存储在 __consumer_offsets Topic 中,而非分区文件本身。
  • 使用命令行工具 kafka-consumer-groups.sh 可以查看消费者组的 Offset 信息。
  • Offset 提交分为自动提交和手动提交两种方式。
4.3 数据存储
  • Kafka 将消息以日志文件的形式存储在指定目录中,包括消息索引文件、消息数据文件等。
  • 日志文件的命名规则为 -,便于管理和查询。

集群

你可能感兴趣的:(java,kafka,笔记,spring,spring,boot,idea)