Springboot 集成KafkaAdmin

基础知识点

(256条消息) 看完这篇Kafka,你也许就会了Kafka_心的步伐的博客-CSDN博客

需要注意消费者offset的配置,生产者ack

docker 安装kafka和zookeeper

记得修改ip地址

docker pull wurstmeister/kafka
docker pull wurstmeister/zookeeper 
# 首先需要启动zookeeper
docker run -it --name zookeeper -p 12181:2181 -d wurstmeister/zookeeper:latest

# 第一台
docker run -it --name kafka01 -p 19092:9092 -d -e KAFKA_BROKER_ID=0 -e KAFKA_ZOOKEEPER_CONNECT=192.168.16.131:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.16.131:19092 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest
# 第二台
docker run -it --name kafka02 -p 19093:9092 -d -e KAFKA_BROKER_ID=1 -e KAFKA_ZOOKEEPER_CONNECT=192.168.16.131:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.16.131:19093 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest
# 第三台
docker run -it --name kafka03 -p 19094:9092 -d -e KAFKA_BROKER_ID=2 -e KAFKA_ZOOKEEPER_CONNECT=192.168.16.131:12181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.16.131:19094 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:latest

Pom.xml

    
        org.springframework.boot
        spring-boot-starter-parent
        3.1.1
         
    


        
            org.springframework.kafka
            spring-kafka
            3.0.7
        

        
            com.alibaba
            fastjson
            1.2.78
        

Yml

server:
  port: 8088


kafkaserver:
  server: 192.168.16.131:19092,192.168.16.131:19093,192.168.16.131:19094
  topic: dume-topic
  parttition: 0
  parttition1: 1
  group-id: dume

配置Topic

package com.example.kafkademo.config;

import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.NewTopic;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaAdmin;

import java.util.HashMap;
import java.util.Map;

/**
 * @description 
 * @author: gdg
 * @Date: 2023/7/13
 */
@Configuration
public class KafkaTopicConfig {

    @Value("${kafkaserver.server}")
    private String boardServer;
    @Value("${kafkaserver.topic}")
    private String topic;

    /**
     * 定义一个KafkaAdmin的bean,可以自动检测集群中是否存在topic,不存在则创建
     */
    @Bean
    public KafkaAdmin kafkaAdmin() {
        Map configs = new HashMap<>();
        // 指定多个kafka集群多个地址,例如:192.168.2.11,9092,192.168.2.12:9092,192.168.2.13:9092
        configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, boardServer);
        return new KafkaAdmin(configs);
    }

    /**
     * 创建Topic
     * */
    @Bean
    public NewTopic newTopic(){
        // 创建topic,需要指定创建的topic的"名称"、"分区数"、"副本数量(副本数数目设置要小于Broker数量)"
        return new NewTopic(topic,3,(short) 1);
    }
}

设置分区规则

package com.example.kafkademo.config;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.InvalidRecordException;
import org.apache.kafka.common.PartitionInfo;

import java.util.List;
import java.util.Map;

/**
 * @description <自定义分区规则>
 * @author: gdg
 * @Date: 2023/7/13
 */
public class MyPartitioner  implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List partitions = cluster.partitionsForTopic(topic);
        //分区数
        int numPartitions = partitions.size();
        // 根据业务逻辑,自定义消息的分区规则
        // 在这个示例中,我们假设 key 是一个字符串类型,根据 key 的哈希值来决定分区
        if (key instanceof String) {
            String keyString = (String) key;
            int partition = Math.abs(keyString.hashCode() % numPartitions);
            return partition;
        }
        // 如果 key 不是字符串类型,则抛出异常
        throw new InvalidRecordException("Invalid key type. It should be a string.");
    }

    @Override
    public void close() {
        // 在需要清理资源时执行的逻辑

    }

    @Override
    public void configure(Map map) {
        // 在需要配置一些参数时执行的逻辑
    }
}

Producer配置

package com.example.kafkademo.config;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;

import java.util.HashMap;
import java.util.Map;

/**
 * @description <功能描述>
 * @author: gdg
 * @Date: 2023/7/13
 * 使用 @EnableKafka :
 * 1 可以使用 @KafkaListener 注解来标注方法,指定要监听的主题、消费者组等配置,从而实现接收和处理 Kafka 消息的功能
 * 2 会根据默认配置或者自定义配置进行初始化  从而简化了 Kafka 相关组件的配置和创建过程。
 *
 */

@Configuration
@EnableKafka
public class KafkaProducerConfig {

    @Value("${kafkaserver.server}")
    private String boardServer;

    public Map producerConfigs(){
        Map props = new HashMap<>();
        // 指定多个kafka集群多个地址
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, boardServer);

        // 重试次数,0为不启用重试机制
        props.put(ProducerConfig.RETRIES_CONFIG, 0);

        //同步到副本, 默认为1
        // acks=0 把消息发送到kafka就认为发送成功
        // acks=1 把消息发送到kafka leader分区,并且写入磁盘就认为发送成功
        // acks=all 把消息发送到kafka leader分区,并且leader分区的副本follower对消息进行了同步就任务发送成功
        props.put(ProducerConfig.ACKS_CONFIG, "1");
        // 生产者空间不足时,send()被阻塞的时间,默认60s
        props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 6000);
        // 控制批处理大小,单位为字节
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 4096);
        // 批量发送,延迟为1毫秒,启用该功能能有效减少生产者发送消息次数,从而提高并发量
        props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
        // 设置生产者发送缓冲区的大小的属性。 发送缓冲区:Kafka 生产者用来临时存储待发送消息的内存区域
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 40960);
        // 消息的最大字节大小,也就是说send的消息大小不能超过这个限制, 默认1048576(1MB)
        props.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG,1048576);
        // 键的序列化方式
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 值的序列化方式
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 压缩消息,支持四种类型,分别为:none、lz4、gzip、snappy,默认为none。
        // 消费者默认支持解压,所以压缩设置在生产者,消费者无需设置。
        //none:不进行压缩,消息以原始格式发送。
        //gzip:使用 GZIP 压缩算法进行压缩。
        //snappy:使用 Snappy 压缩算法进行压缩。
        //lz4:使用 LZ4 压缩算法进行压缩。
        //需要注意的是,压缩会增加消息的处理开销,因为在发送和接收消息时需要进行压缩和解压缩操作。
        props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"none");
        //设置分区规则
        props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class);
        return props;
    }

    /**
     * Producer 工厂配置
     * 可以自定义创建 Kafka 生产者
     * 设置生产者的各种属性,如 Kafka 服务器地址、序列化器、分区器、拦截器等
     */
    @Bean
    public ProducerFactory producerFactory(){
        return new DefaultKafkaProducerFactory<>(producerConfigs());
    }


    /**
     * Producer Template 配置
     */
    @Bean(name="kafkaTemplate")
    public KafkaTemplate kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }

}

Consumer配置

package com.example.kafkademo.config;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.InvalidRecordException;
import org.apache.kafka.common.PartitionInfo;

import java.util.List;
import java.util.Map;

/**
 * @description <自定义分区规则>
 * @author: gdg
 * @Date: 2023/7/13
 */
public class MyPartitioner  implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List partitions = cluster.partitionsForTopic(topic);
        //分区数
        int numPartitions = partitions.size();
        // 根据业务逻辑,自定义消息的分区规则
        // 在这个示例中,我们假设 key 是一个字符串类型,根据 key 的哈希值来决定分区
        if (key instanceof String) {
            String keyString = (String) key;
            int partition = Math.abs(keyString.hashCode() % numPartitions);
            return partition;
        }
        // 如果 key 不是字符串类型,则抛出异常
        throw new InvalidRecordException("Invalid key type. It should be a string.");
    }

    @Override
    public void close() {
        // 在需要清理资源时执行的逻辑

    }

    @Override
    public void configure(Map map) {
        // 在需要配置一些参数时执行的逻辑
    }
}

配置消费者监听器

配置同一个topic  , 监听不同分区

package com.example.kafkademo.manager;

import com.alibaba.fastjson.JSONObject;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.annotation.TopicPartition;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Optional;

/**
 * @description <功能描述>
 * @author: gdg
 * @Date: 2023/7/13
 */
@Component
public class KafkaComsumerManager {

    private static Logger logger = LoggerFactory.getLogger("adminLogger");


    private static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    /**
     * 指定topic
     * 指定消费分区parttition
     * topicPartitions : 由主题名和分区号组成的列表或集合。它用于在消费者端进行分区级别的操作和处理
     * containerFactory : 指定消息监听  用于消费主题中的消息
     * ${kafkaserver.parttition}  ${} 是一种属性占位符语法,用于引用配置文件中的属性值 会从 YAML 配置文件中获取对应的值。
     *
     * @param records
     */
    @KafkaListener(topicPartitions = {
            @TopicPartition(topic = "${kafkaserver.topic}",partitions = "${kafkaserver.parttition}" ),
            @TopicPartition(topic = "${kafkaserver.topic}",partitions = "${kafkaserver.parttition1}" ),
            },
            containerFactory = "kafkaListenerContainerFactory",
            groupId = "group-test-1")
    public void onMessage(List records) {
        logger.info("**********************************接收数量{}**************************************",records.size());
        for(ConsumerRecord record :records ){
            Optional kafkaMassage = Optional.ofNullable(record.value());
            if(kafkaMassage.isPresent()){
                try {
                    Long current = System.currentTimeMillis();
                    logger.info("**********************************kafka接收信息打印开始**************************************");
                    logger.info("kafka接收信息:"+'\t'+record.toString());
                    logger.info("kafka数据:"+'\t'+record.value());
                    logger.info("分区:"+ record.partition());
                    logger.info("偏移量:" + record.offset());
                    logger.info("报文时间:" + formatter.format(record.timestamp()));
                    logger.info("系统时间:" + formatter.format(current));
                    logger.info("**********************************kafka信息打印结束**************************************");
                    JSONObject value = JSONObject.parseObject(record.value().toString());

                } catch (Exception e) {
                    // TODO: handle exception
                    logger.error("********kafka接收数据出错:{}********",e.getMessage());
                }
            }


        }

    }

}

Controller调用

package com.example.kafkademo.produce;

import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * @description <功能描述>
 * @author: gdg
 * @Date: 2023/7/13
 */
@RestController
@RequestMapping("/pro")
public class ProducerController {
    private static Logger logger = LoggerFactory.getLogger("adminLogger");

    @Autowired
    @Qualifier("kafkaTemplate")
    private KafkaTemplate kafkaTemplate;
    @Value("${kafkaserver.topic}")
    private String topic;

    /**
     * producer 同步方式发送数据
     * get()方法
     *      1 会阻塞当前线程直到发送完成,并返回发送结果对象
     *      2 可以实现同步地发送消息并等待发送完成
     * get(10, TimeUnit.SECONDS)  设置10秒内发送完成否则将会抛出 TimeoutException 异常
     *
     * @param topic   topic名称
     * @param message producer发送的数据
     */
    @PostMapping("/syncSend")
    public void sendMessageSync()
            throws InterruptedException, ExecutionException, TimeoutException {
        for (int i = 0; i < 50; i++) {
            Map map = new HashMap<>();
            map.put("userId","1000"+i);
            map.put("userName","阿萨德"+i);
            String message = JSON.toJSONString(map);
            kafkaTemplate.send(topic, message).get(10, TimeUnit.SECONDS);
        }

    }


    @PostMapping(value = "/producerData")
    public void producerData() {
        for (int i = 0; i < 50; i++) {
            Map map = new HashMap<>();
            map.put("userId","1000"+i);
            map.put("userName","阿萨德"+i);
            String message = JSON.toJSONString(map);
            sendMessageAsync(topic,generateRandomString(10),message);
        }
    }
    private   String generateRandomString(int length) {
        SecureRandom secureRandom = new SecureRandom();
        byte[] randomBytes = new byte[length];
        secureRandom.nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
    }
    /**
     * producer 异步方式发送数据
     *
     * @param topic   topic名称
     * @param message producer发送的数据
     */
    private void sendMessageAsync(String topic,String key, String message) {
        logger.info("准备发送消息为:{}", message);
        //发送消息
        CompletableFuture> future = kafkaTemplate.send(topic,key,message);
        //处理发送后的操作
        future.handle((result, ex) -> {
            if (ex != null) {
                // 在这里处理异常情况
                logger.error("{} - 生产者 发送消息失败:{}", topic, ex.getMessage());
                // 返回一个默认的 SendResult 或其他你希望返回的默认值
                return null;
            } else {
                // 返回原始的 SendResult
                logger.info("{} - 生产者 发送消息成功:{}" , topic, result.getProducerRecord().value());
                return result;
            }
        });
    }
}

进入容器执行命令

cd  /opt/kafka/bin/

# 创建topic名称为first,3个分区,1个副本

./kafka-topics.sh --zookeeper 192.168.16.131:12181 --create --topic first --replication-factor 1 --partitions 3

查询主题命令

 kafka-topics.sh  --describe  --zookeeper 192.168.16.131:12181 --topic  dume-topic

---ReplicationFactor : 副本数

---PartitionCount : 分区数
Springboot 集成KafkaAdmin_第1张图片

 分区数据存储

        cd /opt/kafka_2.13-2.8.1/config/   查询 server.properties文件,确定分区存储路径 

Springboot 集成KafkaAdmin_第2张图片

查询分区数据存储位置

cd /kafka/kafka-logs-d3c8611dfd21/
 

Springboot 集成KafkaAdmin_第3张图片

Springboot 集成KafkaAdmin_第4张图片

 

 分区分配规则

分区的平均分散是通过分区的分配算法来实现的。Kafka 使用一种称为“分区分配策略”的算法,根据一定的规则将分区均匀地分配给可用的 Broker

常用的分区分配策略有以下几种:

  • Round Robin(轮询):按照顺序将分区分配给可用的 Broker,保持平衡。
  • Range(范围):根据分区的大小或其他属性将分区分配给 Broker,以保持平衡和优化数据分布。
  • Sticky(黏性):尽可能将同一分区分配给同一 Broker,以减少分区的迁移和维护开销。

注意; 分区的分配和平衡是在 Kafka 集群中动态进行的,当集群的 Broker 增加或减少时,Kafka 会自动进行分区的重新分配,以适应集群的变化和维持平衡状态

Rebalance

当有新的消费者加入消费者组、已有的消费者退出消费者组或者订阅的主体分区发生了变化,会触发分区的重新分配操作,重新分配的过程称为Rebalance

你可能感兴趣的:(spring,boot,kafka,后端)