Kafka学习

126 阅读8分钟

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

Tags · apache/kafka (github.com)

拉取镜像
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命令进入容器内部

如果要更改容器内的配置文件:

  1. 将容器内的文件拷贝到主机
  2. 将拷贝到主机的文件更改后替换回容器内部
  3. 或者启动容器时通过环境变量改变默认的文件配置

将advertisedlisteners的ip更改为你的公网ip,更多详情请参考其他文章

listeners=PLAINTEXT://192.168.0.213:9092
advertised.listeners=PLAINTEXT://x.x.x.x:9092

kafka可视化管理工具

EFAK (kafka-eagle.org)

Kafka-King现代化GUI: github.com/Bronya0/Kaf…

Topic

使用kafka的第一件事就是创建个主题Topic,主题类似于文件夹,主题内部数据称作事件Event,相当于文件夹内部的文件

  • Topic内部有很多分区Partition
  • Partition内部存放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>其内部的参数如下

image-20241110211720016.png

image-20241110212111862.png

非阻塞式获取

通过对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

image-20241110214543884.png

可选的序列化器如下:

image-20241110214838410.png

消费者

消费者主要@KafkaListener注解标记方法,方法内的参数即该注解会自动把监听到的消息注入到方法参数里

其中该注解较重要的两个参数分别是

  • topics-->监听的topic,是string数组,也可以使用另外两个定义topic的参数topicPatterntopicPartitions
  • 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();
  }
}

image-20241111171026031.png

重置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.ProducerInterceptor

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