kafka详细学习笔记

一、准备材料

  1. 安装jdk1.8,配置环境变量
  2. 下载kafka:https://kafka.apache.org/downloads
  3. zookeeper下载:https://zookeeper.apache.org/releases.html#download
    3.1 解压:tar -xvf xxxx
    3.2 进入Zookeeper的目录apache-zookeeper-3.6.3-bin\conf,将“zoo_sample.cfg”重命名为“zoo.cfg”
    3.3 打开zoo.cfg,将 dataDir的值改为“dataDir=/opt/zookeeper/tmp” (自定义文件夹)
    3.4 启动:cd /opt/zookeeper/apache-zookeeper-3.6.3-bin/bin --> ./zkServer.sh start
  4. 将kafka解压,复制三份,并且修改配置文件,以kafka1为例:/opt/kafka/kafka1/config/server.properties
    #配置标识
    broker.id=12,3)
    #配置可删除
    delete.topic.enable=true
    #配置日志
    log.dirs=/opt/kafka/tmp/kafka1(2,3/kafka-logs
    #配置端口
    port=90929093,9094)
    #配置可被外部服务器访问
    listeners=PLAINTEXT://192.168.248.100:9092 
    advertised.listeners=PLAINTEXT://192.168.248.100:9092
    #配置清除数据日志文件策略
    log.cleanup.policy=delete
    log.retention.hours=168
    log.segment.bytes=1073741824
    
    3.6 启动: cd /opt/kafka/kafka1/bin -->./kafka-server-start.sh /opt/kafka/kafka1/config/server.properties
  5. ZooInspector监控下载(监控zookeeper):https://issues.apache.org/jira/secure/attachment/12436620/ZooInspector.zip
  6. offset explore下载(监控kafka):https://www.kafkatool.com/download.html
  7. 如果客户端和kafka不在同一台服务器,则需要kafka服务端的ip和hostname配置导本地的hosts中,否则代码访问会很慢!!

二、相关原理图

  1. 单个主题的单分区功能
    ​ ps1:创建主题,生产者往主题发送数据,消费者订阅主题,即可直接消费主题。
    在这里插入图片描述
  2. 单个主题的分区、集群功能
    ps1:创建主题可以设置多个分区,以提高吞吐效率,但同时会增大kafka服务器的压力
    ps2:生产者发送消息,默认轮询入分区,该策略可以配置。
    ps3:kafka集群后,会为每个分区选举一个leader节点。如三个分区,将会有三个leader节点。
    ps4:每个分区只会在各自leader节点进行吞吐,其他集群节点充当副本同步数据不参与吞吐。
    ps5:一个partition只能供一个消费者消费,以保证消费的顺序性。
    ps6:如果分区数大于消费者数量(同一个消费组),则一个消费者可以消费多个partition
    ps7:如果消费者数量大于分区数(同一个消费组),则多余的消费者将会空置,所以建议消费者数量不要超过partition数量。
    ps8:如果消费者挂了,则会触发rebalance机制,让其他消费者消费。
    在这里插入图片描述
  3. 偏移量的自动/手动提交
    ps1:自动提交会导致消息丢失–>消息poll下来后,操作失败,此时位移已经改变。
    ps2:自动提交会导致消息重复消费–>5s自动提交,消息poll后,应用在处理消息,3秒后kafka进行了重平衡,由于没有更新位移导致这部分消息重复消费。
    ps3:手动同步提交–>消费完调用同步提交方法,返回ack前线程阻塞,返回后才执行后面的逻辑
    ps4:手动异步提交–>消费完不需要返回ack访问就可以执行后面的逻辑。可以设置一个回调方法,监听异步回调的结果。
    在这里插入图片描述

三、相关概念

  1. 生产者:发送消息给broker指定主题的客户端
  2. 消费者:消费broker指定主题消息的客户端
  3. broker:消息中转站,即根据主题接收消息,分配消息
  4. topic:一个逻辑概念,kafka通过topic将消息进行分类,不同topic会被订阅该主题的消费者消费。
  5. pattiyion(分区):因为topic消费的消息多的时候会到T级别,所以需要进行数据分区,分段存储kafka的消息。同时提高读写的吞吐量。
  6. 数据相关文件:“E:\kafka\kafka\user\tmp\kafka-logs(自定义)”底下存放了以“主题-分区”命名的不同文件夹
    在这里插入图片描述
  7. __consumer_offsets:kafka自带50个分区的主题,用来记录各个“主题-分区-消费组”的偏移量。
    ps1:每个消费者消费都会将偏移量记录到__consumer_offsets,默认保存7天,到期删除。
    ps2:若某个主题的同一个消费组的消费者1挂了,消费者2上来,则需要在消费者1的基础上继续消费,此时就需要记住消费者1消费消息的偏移量。
    ps3:记录规则–>key是consumerGroupId+topic+分区号,value就是当前offsets
    ps4:提交分区公式–>hash(consumerGroupId)%(_consumer_offsets主题的分区数)
  8. 生产者同步发消息:生产者发送消息如果没有收到ack,则会阻塞3s,3s后还未收到则进行重试3次,重试间隔默认为100ms。
  9. 生产者异步发消息:生产者发送消息后就可以直接执行之后的业务,broker接到消息后会异步调用callback回调方法处理成功或者失败的操作。
  10. 消费者自动提交和手动提交:这个自动提交指的是偏移量自动提交到_consumer_offsets,间隔默认1s。这样子可能造成“消息丢失”和“消息重复消费”的风险。一般使用手动提交,提交的信息为:所属消费组+消费主题+消费分区+消费偏移量。
  11. 集群:kafka集群配置
  12. 副本:kafka集群存在的概念,即集群中的一个节点为leader节点,其他的都是副本节点
  13. controller:每个broker启动时会向zk创建一个临时序号节点,获得序号最小的那个broker会作为集群中的controller,用来管理集群中的所有分区和副本状态。如1. 某个分区的副本leader故障,由该控制器进行选举新的leader 2. 集群中的broker新增或者减少,controller会同步信息给其他broker。 3.集群中的分区增加或减少,broker会同步给其他broker
  14. rebalance机制:在消费者没有指名分区消费的前提下,当消费组里的消费者和分区的关系发生变化,就会触发rebalance机制。这个机制就是调整消费者消费哪个分区。分区也有三种:1.range,通过公式计算消费者消费哪个分区。 2.轮询,轮流消费。 3.stick,在消费者消费的原分区不变的基础上进行调整。
  15. HW和LEO
    LEO是某个副本消息的最后消息位置,HW是已完成同步的位置(副本数据同步)。消息写入broker时,当每个broker完成这条消息同步后,HW才会发生变化,并且在同步前,消费者无法消费这条消息,这样子可以防止leader挂掉后,消息丢失的情况。
    在这里插入图片描述

四、相关指令操作

  1. 启动:cd E:\kafka\kafka\user\kafka_2.12-2.8.1 --> .\bin\windows\kafka-server-start.bat .\config\server.properties(linux 可以加个”-daemon“,以守护进程方式启动)
  2. 进入目录:E:\kafka\kafka\user\kafka_2.12-2.8.1\bin\windows
  3. 创建主题: kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 10 --topic test
    ​ ps:脚本创建/指定zookeeper/一个副本/分区数/指定主题
  4. 查看有哪些主题: kafka-topics.bat --list --zookeeper localhost:2181
  5. 创建生产者: kafka-console-producer.bat --broker-list localhost:9092 --topic test
    ​ ps1:消息是有序的,存于 E:\kafka\kafka\user\tmp\kafka-logs\主题-分区(分区由创建主题时定)\00000000000000000000.log
    ​ ps2:消息消费完不会取消,不同消费组的其他消费者进来还可以继续消费。
  6. 创建消费者1(从最后一条消息的偏移量+1开始消费,即现发->现消费): kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test
  7. 创建消费者2(从头消费): kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test --from-beginning
  8. 创建同组的两个消费者测试:kafka-console-consumer.bat --bootstrap-server localhost:9092 --consumer-property group.id=testGroup --topic test
    ps1:单播消费者(将消费者纳入组中,一个消费组中只有一个消费者可以消费消息)
  9. 创建新的消费组测试:kafka-console-consumer.bat --bootstrap-server localhost:9092 --consumer-property group.id=testGroup1 --topic test
    ps1:多播消费者(即多个组,可以满足多个消费者消费,改变组名即可)
  10. 查看有哪些消费组:kafka-consumer-groups.bat --bootstrap-server localhost:9092 --list
  11. 查看消费组具体信息:kafka-consumer-groups.bat --bootstrap-server localhost:9092 --describe --group testGroup
    在这里插入图片描述
  12. 为主题创建2个分区,3个副本: kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 3 --partitions 2 --topic test
  13. 查看主题详情: kafka-topics.bat --describe --zookeeper localhost:2181 --topic test
    在这里插入图片描述
    ps1:replicas–>副本存在的broker节点
    ps2:leader–>副本才有,即需要一个领导节点,其他作为备份节点。读写操作都发生在leader节点,如果leader挂了,则从备份节点选一个当leader
    ps3:选举机制,在Replicas越靠前,且在isr中存在的broker,越优先成为leader
    ps4:isr–>可以同步的和已同步的broker节点,如果isr中的节点性能较差,会被踢出集合
  14. 删除主题:kafka-topics.bat --zookeeper localhost:2181 --delete --topic test_topic(有bug,请勿尝试。如果出错,需要关闭kafka,删除kafka的本地数据log文件,同时删除zookeeper上的相关主题,即可!)
  15. 创建集群生产者: kafka-console-producer.bat --broker-list localhost:9092,localhost:9093,localhost:9094 --topic test
  16. 创建集群消费者: kafka-console-consumer.bat --bootstrap-server localhost:9092,localhost:9093,localhost:9094 --consumer-property group.id=testGroup --topic test --from-beginning

五、kafka分区迁移

  1. kafka集群新增机制

    1.1、增加节点不需要重启,扩展分区或者新建主题时,分区会自动分配至新增节点上
    1.2、topic创建之后,已存在的主题分区副本永远不会自动改变,需要自己迁移

  2. 创建topics-to-move.json:

    {"topics":
    [
    {"topic":"testjq"}
    ],
    "version": 1
    }
    
  3. 执行脚本:./kafka-reassign-partitions.sh --zookeeper 192.168.6.160:2181 --topics-to-move-json-file topics-to-move.json --broker-list “2” --generate

  4. 将执行脚本后得到的两来哪配置保存下来,一份是当前分区(备份防止出错可以及时回滚),一份是建议分区

    current-topic-reassignment.json:

    {"version":1,"partitions":[{"topic":"testjq","partition":0,"replicas":[1],"log_dirs":["any"]},{"topic":"testjq","partition":1,"replicas":[1],"log_dirs":["any"]},{"topic":"testjq","partition":2,"replicas":[1],"log_dirs":["any"]},{"topic":"testjq","partition":3,"replicas":[2],"log_dirs":["any"]},{"topic":"testjq","partition":4,"replicas":[1],"log_dirs":["any"]},{"topic":"testjq","partition":5,"replicas":[2],"log_dirs":["any"]}]}
    

    proposed-topic-reassignment.json:

    {"version":1,"partitions":[{"topic":"testjq","partition":0,"replicas":[2],"log_dirs":["any"]},{"topic":"testjq","partition":1,"replicas":[2],"log_dirs":["any"]},{"topic":"testjq","partition":2,"replicas":[2],"log_dirs":["any"]},{"topic":"testjq","partition":3,"replicas":[2],"log_dirs":["any"]},{"topic":"testjq","partition":4,"replicas":[2],"log_dirs":["any"]},{"topic":"testjq","partition":5,"replicas":[2],"log_dirs":["any"]}]}
    
  5. 开始迁移:./kafka-reassign-partitions.sh --zookeeper 192.168.6.160:2181 --reassignment-json-file proposed-topic-reassignment.json --execute

  6. 判断是否完成:./kafka-reassign-partitions.sh --zookeeper 192.168.6.160:2181 --reassignment-json-file proposed-topic-reassignment.json --verify

六、代码实现-配置文件

server:
  port: 8081

spring:
  application:
    name: kafka
  kafka:
    bootstrap-servers: 127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094 # kafka集群信息
    producer: # 生产者配置
      retries: 3  # 重试次数,默认Integer.MAX_VALUE
      batch-size: 16384  # kafka会从缓冲区读取数据,批量发送给broker,批量发送消息的大小(默认16K)
      buffer-memory: 33554432  # 生产者内存缓存区大小(32M)。如果设置了该缓冲区,消息会先发送到缓冲区,可以提升发送性能。
      # 默认是1
      # 0:客户端发送了,认为是发送成功。这种容易丢失消息。这种但是效率最高
      # 1:客户端发送了,leader收到了并且写入了本地log,认为是发送成功。这种性能和效率是均衡的
      # all: 客户端发送了,分区leader收到了,其他副本follower也有了,认为是发送成功。最安全,但是性能最差。如果只开一台kafka,和“1”性能相等。
      acks: 1
      key-serializer: org.apache.kafka.common.serialization.StringSerializer # 指定消息key的编解码方式
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 指定消息体的编解码方式。这种是针对于对象传输
      properties:
        linger:
          ms: 10  # 如果kafka迟迟不发送消息(这里指的是缓冲区消息没堆积到指定数量),那么过了这个时间(单位:毫米)开始发
    consumer:
      group-id: defautGroup # 默认消费者组,即消费的时候不指定组的话,默认使用该组
      enable-auto-commit: false # 关闭自动提交。这个自动提交指的是偏移量自动提交,auto.commit.interval.ms为自动提交的时间间隔,默认1s。自动提交方虽然便,但是这样子可能造成“消息丢失”和“消息重复消费”的风险
      auto-offset-reset: earliest  #默认latest。earliest:从头开始消费。latest:从最新的开始消费,当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer # key反序列化(默认,可以不设置)
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer # value反序列化(默认,可以不设置)
      max-poll-records: 500 #一次性最大poll的条数。默认500
      heartbeat-interval: 1000 #consumer给broker发送心跳的间隔
    listener:
      concurrency: 1  #在侦听器容器中运行的线程数,多线程消费
      # RECORD:当每一条记录被消费者监听器(ListenerConsumer)处理之后提交
      # BATCH:当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
      # TIME:当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交
      # COUNT:当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交
      # COUNT_TIME:TIME | COUNT 有一个条件满足时提交
      # MANUAL:当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
      # MANUAL_IMMEDIATE:手动调用Acknowledgment.acknowledge()后立即提交,一般使用这种
      AckMode: MANUAL_IMMEDIATE
      poll-timeout: 10000  #轮训消费者时的超时时间,ms
      monitor-interval: 10000 #监控间隔时间
      ack-count: 5 #当ackMode为“COUNT”或“COUNT_TIME”时,偏移提交之间的记录数
      ack-time: 10000  #当ackMode为“TIME”或“COUNT_TIME”时,偏移提交之间的时间(以毫秒为单位)
      # 在spring-kafka中没有明确的配置对应,但是预留了一个properties属性,可以设置所有的kafka配置
      #    properties:
      #      session:
      #        timeout:
      #          ms: 10000  # kafka超过10s没收到消费者的心跳,将其剔除消费组,将分区给其他消费者
      max:
        poll:
          interval:
            ms: 30000 #kafka两次poll的时间超过30s的时间间隔,则kafka会认为改消费者能力弱,将其剔除消费组,将分区给其他消费者
#      enable:
#        idempotence: true #开启幂等性

七、代码实现-pom依赖

依赖最好跟服务端匹配,我这里部署的kafka版本是2.12-2.8.1

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>kafka</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath></relativePath>
    </parent>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- 覆盖 低版本  产生的错误 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        <!-- 热部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!--引入kafka依赖-->
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.12</artifactId>
            <version>2.8.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>2.8.1</version>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.28</version>
        </dependency>

    </dependencies>

</project>

八、代码实现-生产消费者

  1. 生产者
package com.kafka;

import lombok.NonNull;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.concurrent.ExecutionException;

/**
 * @author 天真热
 * @create 2022-03-24 15:42
 * @desc
 **/
@RestController
public class KafkaProducer {
    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;

    @RequestMapping("/send")
    public void send() throws ExecutionException, InterruptedException {
        //===============================同步发送一段消息===================================
        kafkaTemplate.send(Constants.TOPIC_NAME1, "同步 kafka").get();

        //===============================异步发送一段消息===============================
        //异步发送消息容易造成消息丢失,可设置监听
        ListenableFuture<SendResult<String, String>> sendYb = kafkaTemplate.send(Constants.TOPIC_NAME1, "异步 kafka");
        sendYb.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
            @Override
            public void onFailure(@NonNull Throwable throwable) {
                System.out.println("结果失败");
            }
            @Override
            public void onSuccess(SendResult<String, String> result) {
                ProducerRecord<String, String> producerRecord = result.getProducerRecord();
                System.out.println("结果成功");
            }
        });


        //===============================指定分区发送消息===============================
        //key可以不要,key的作用是在没有指定分区的情况下,计算出要往哪个分区发送数据。分区=hash(key)%(_consumer_offsets主题的分区数)
        //主题、分区、时间错、key、值
        kafkaTemplate.send(Constants.TOPIC_NAME2, 1, 100l, null, "指定分区1 kafka");

    }
}
  1. 消费者
package com.kafka;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.annotation.PartitionOffset;
import org.springframework.kafka.annotation.TopicPartition;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;

/**
 * @author 天真热
 * @create 2022-03-24 15:44
 * @desc
 **/
@Component
public class KafkaConsumer {

    /**
     * 消费者消费一个主题
     *
     * @param record
     * @param ack
     */
    //实际也是按批拿的数据,只不过对一批数据中的单条进行具体操作
    @KafkaListener(topics = Constants.TOPIC_NAME1, groupId = "group1")
    public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
        String value = record.value();
        System.out.println("group1:" + value);
        System.out.println(record.toString());
        //手动提交offset
        ack.acknowledge();
    }


    /**
     * 消费者消费两个主题
     *
     * @param record
     * @param ack
     */
    @KafkaListener(topics = {Constants.TOPIC_NAME1, Constants.TOPIC_NAME2}, groupId = "group2")
    public void listenGroup3(ConsumerRecord<String, String> record, Acknowledgment ack) {
        String value = record.value();
        System.out.println("group2:" + value);
        //手动提交offset
        ack.acknowledge();
    }


    /**
     * 1.消费者消费两个主题
     * 2.消费者消费主题1的0分区
     * 2.消费者消费主题2的0分区,1分区并且偏移量从2开始
     *
     * @param record
     * @param ack
     */
    @KafkaListener(id = "consumer1", groupId = "group3", topicPartitions = {
            @TopicPartition(topic = Constants.TOPIC_NAME1, partitions = {"0"}),
            @TopicPartition(topic = Constants.TOPIC_NAME2, partitions = "0",
                    partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "2")
            )
    }, concurrency = "2")
    public void listenGroup4(ConsumerRecord<String, String> record, Acknowledgment ack) {
        String value = record.value();
        System.out.println("group4:" + value);
        //手动提交offset
        ack.acknowledge();
    }


}
  1. 动态创建消费者
    原理:创建主题消费者监听器,但是不初始化导spring容器中。需要创建消费者时,创建个新的消费组并且将监听器绑定到消费组中,然后在spring容器初始化。
package com.authority;


import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerEndpointRegistry;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.MessageListenerContainer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * 动态创建消费者:
 * 1.设置监听器,动态创建消费者,注入监听器即可
 * 2.直接创建消费者、设置监听器
 *
 * @author 天真热
 * @create 2022-03-27 10:28
 * @desc
 **/
@RestController
@RequestMapping("consumer")
public class ConsumerController {
    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;

    /**
     * 应用程序上行文
     */
    @Autowired
    ApplicationContext context;


    /**
     * 监听器容器工厂
     */
    @Resource
    ConcurrentKafkaListenerContainerFactory<Object, Object> containerFactory;

    /**
     * 所有@KafkaListener这个注解所标注的方法都会被注册在这里面中
     */
    @Resource
    KafkaListenerEndpointRegistry registry;




    /**
     * 创建消费者分组
     */
    @GetMapping("/create")
    public void create() {
        //动态创建三个消费者分组
        String[] groupIds = {"group-0", "group-1", "group-2"};

        for (String groupId : groupIds) {
            // 初始化当前消费者分组的配置
            Map<String, Object> consumerProps = new HashMap<>();
            Utils.getProperties(consumerProps);
            consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
            // 设置监听器容器工厂
            containerFactory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(consumerProps));
            // 获取监听类实例
            context.getBean(MyListener.class);
        }
    }


    /**
     * 停止所有消费监听
     */
    @GetMapping("/stop")
    public void stop() {
        registry.getListenerContainers().forEach(container -> {
            System.out.println(container.getGroupId());
            System.out.println(container.getListenerId());
            container.stop();
        });
    }

    /**
     * 启动所有消费监听
     */
    @GetMapping("/start")
    public void start() {
        Collection<MessageListenerContainer> x = registry.getListenerContainers();
        registry.getListenerContainers().forEach(container -> {
            System.out.println(container.getGroupId());
            System.out.println(container.getListenerId());
            container.start();
        });
    }

    /**
     * 获取监听类实例
     */
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public MyListener listener() {
        return new MyListener();
    }
}
package com.authority;

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.kafka.support.KafkaHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Payload;

/**
 * @author 天真热
 * @create 2022-03-27 10:27
 * @desc
 **/
public class MyListener {
    @KafkaListener(topics = "TOPIC1")
    public void listen(@Payload String data, @Header(KafkaHeaders.GROUP_ID) String groupId, Acknowledgment ack) {
        //从上下文获取bean,然后执行数据库操作
        //private T service = (T) ApplicationContextGetBeanHelper.getBean("serviceImpl");

        System.out.println(groupId + ":" + data);
        //手动提交offset
        ack.acknowledge();
    }



}

九、代码实现-主题Api

1. 创建主题

/**
 * 创建主题
 */
public static void createTopic() {
    Properties prop = new Properties();
    prop.put("bootstrap.servers", "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
	// 创建kafka客户端连接
    AdminClient admin = AdminClient.create(prop);
	// 创建主题集合
    ArrayList<NewTopic> topics = new ArrayList<NewTopic>();
    // 声明主题  参数:主题名称、分区数、副本数
    NewTopic newTopic = new NewTopic("TOPIC2", 3, (short) 3);
    topics.add(newTopic);
	//客户端创建主题
    CreateTopicsResult result = admin.createTopics(topics);

    try {
        result.all().get();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

2. 删除主题

/**
 * 删除主题(kafka安装在window有bug,linux正常)
 */
public static void deleteTopic() {
    // 删除kafka主题
    Properties prop = new Properties();
    prop.put("bootstrap.servers", "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
    // 创建kafka客户端连接
    AdminClient client = AdminClient.create(prop);
    // 创建topic集合
    ArrayList<String> topics = new ArrayList<>();
    topics.add("test_topic");
    // 删除topic
    DeleteTopicsResult result = client.deleteTopics(topics);
    try {
        result.all().get();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

    // 创建zookeeper连接
    ZkClient zkClient = new ZkClient("127.0.0.1:2181", 30000, 30000);
    // 删除zookeeper主题
    zkClient.deleteRecursive(ZkUtils.getTopicPath("test_topic"));
}

3. AdminClient的其他API

//创建Topic:createTopics(Collection<NewTopic> newTopics)
//删除Topic:deleteTopics(Collection<String> topics)
//罗列所有Topic:listTopics()
//查询Topic:describeTopics(Collection<String> topicNames)
//查询集群信息:describeCluster()
//查询ACL信息:describeAcls(AclBindingFilter filter)
//创建ACL信息:createAcls(Collection<AclBinding> acls)
//删除ACL信息:deleteAcls(Collection<AclBindingFilter> filters)
//查询Topic、Broker等的所有配置项信息:describeConfigs(Collection<ConfigResource> resources)
//用于修改Topic、Broker等的配置项信息(该方法在新版本中被标记为已过期):alterConfigs(Map<ConfigResource, Config> configs)
//修改副本的日志目录:alterReplicaLogDirs(Map<TopicPartitionReplica, String> replicaAssignment)
//查询节点的日志目录信息:describeLogDirs(Collection<Integer> brokers)
//查询副本的日志目录信息:describeReplicaLogDirs(Collection<TopicPartitionReplica> replicas)
//增加分区:createPartitions(Map<String, NewPartitions> newPartitions)
//同样也是用于修改Topic、Broker等的配置项信息,但功能更多、更灵活,用于代替alterConfigs:incrementalAlterConfigs
//用于调整Topic的Partition数量,只能增加不能减少或删除,也就是说新设置的Partition数量必须大于等于之前的Partition数量:createPartitions

4. 获取主题列表(kafka)

/**
 * 查看主题列表
 */
public static void listTopic() {
    Properties prop = new Properties();
    prop.put("bootstrap.servers", "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
    // 创建kafka客户端连接
    AdminClient admin = AdminClient.create(prop);
	//获取主题列表
    ListTopicsResult result = admin.listTopics();
    KafkaFuture<Set<String>> future = result.names();

    try {
        System.out.println("==================Kafka Topics====================");
        future.get().forEach(name -> System.out.println(name));
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

}

5. 根据主题获取消费组

/**
 * 获取主题下的消费组
 *
 * @param brokerServers "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094"
 * @param topic "TOPIC1"
 * @return
 * @throws ExecutionException
 * @throws InterruptedException
 * @throws TimeoutException
 */
private static List<String> getGroupsForTopic(String brokerServers, String topic)
        throws ExecutionException, InterruptedException, TimeoutException {
    Properties props = new Properties();
    props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, brokerServers);

    try (AdminClient client = AdminClient.create(props)) {
        //获取所有消费组名称
        List<String> allGroups = client.listConsumerGroups().valid().get(10, TimeUnit.SECONDS).stream().map(ConsumerGroupListing::groupId).collect(Collectors.toList());
        //获取所有的消费组详情
        Map<String, ConsumerGroupDescription> allGroupDetails = client.describeConsumerGroups(allGroups).all().get(10, TimeUnit.SECONDS);
		 // 创建消费组集合
        final List<String> filteredGroups = new ArrayList<>();
        allGroupDetails.entrySet().forEach(entry -> {
            //获取组名称
            String groupId = entry.getKey();
            //获取组详情
            ConsumerGroupDescription description = entry.getValue();
            //是否属于给定的主题的消费组,如果是则加入集合
            boolean topicSubscribed = description.members().stream().map(MemberDescription::assignment)
                    .map(MemberAssignment::topicPartitions)
                    .map(tps -> tps.stream().map(TopicPartition::topic).collect(Collectors.toSet()))
                    .anyMatch(tps -> tps.contains(topic));
            if (topicSubscribed)
                filteredGroups.add(groupId);
        });
        return filteredGroups;
    }
}

6. 根据主题、分区、消费组获取当前偏移量

/**
     * 获取主题分组的偏移量
     * @param server "127.0.0.1:2181"
     * @param brokerServer "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094"
     * @param topic “TOPIC1”
     * @param group 'group1'
     * @return
     */
    public static Long getOffset(String server, String brokerServer, String topic, String group) {
        Properties props = new Properties();
        props.put("bootstrap.servers", brokerServer);
        props.put("group.id", group);
        props.put("key.deserializer", StringDeserializer.class.getName());
        props.put("value.deserializer", StringDeserializer.class.getName());
        // 创建kafka客户端连接
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
        consumer.subscribe(Arrays.asList(topic));

        Long commitedOffset = 0L;
        // 获取指定主题的分区列表
        List<PartitionInfo> partitionsFor = consumer.partitionsFor(topic);
        for (PartitionInfo partitionInfo : partitionsFor) {
            // 设置主题、分区
            TopicPartition tp = new TopicPartition(partitionInfo.topic(), partitionInfo.partition());
            // 获取主题、分区对应的提交offset的记录
            OffsetAndMetadata committed = consumer.committed(tp);
            //获取提交的offset
            commitedOffset += committed.offset();
        }

        return commitedOffset;
    }

7. 根据主题获取消息总数

/**
 * 主题的消息总量(各分区之和)
 *
 * @param brokerServer
 * @param topics
 * @return
 */
public static Map<String, Long> counts(String brokerServer, String topics) {
    //服务器或者主题为空,则直接返回null
    if (brokers == null || brokers.isEmpty() || topics == null || topics.isEmpty()) {
        return null;
    }
    Map<String, Long> map = Maps.newTreeMap();
    // 遍历主题
    // 获取主题对应的分区、副本、领导节点、选举节点等
    Map<Integer, PartitionMetadata> metadata = findLeader(brokers, topics);
    long size = 0L;
    for (Map.Entry<Integer, PartitionMetadata> entry : metadata.entrySet()) {
        // 分区数据
        int partition = entry.getKey();
        // leader节点
        String leadBroker = entry.getValue().leader().host();
        // 客户端,节点+分区
        String clientName = "Client_" + topics + "_" + partition;
        // 发送请求给每个分区的leader
        SimpleConsumer consumer = new SimpleConsumer(leadBroker, entry.getValue().leader().port(), 100000, 64 * 1024, clientName);
        // 获取分区底下的最大偏移量(数据总量)
        long readOffset = getLastOffset(consumer, topics, partition, kafka.api.OffsetRequest.LatestTime(), clientName);
        // 总数量叠加
        size += readOffset;
        consumer.close();
    }
    // 插入主题-->最大数量
    map.put(topics, size);
    return map;
}

8. 根据主题、消费组获取堆积消息数

堆积消息=消息总数-当前偏移量(具体可参考第6和第7点)

9. 根据主题、消费组获取指定时间偏移量

/**
     * 获取时间对应的时间戳
     *
     * @param time
     * @return
     */
    private static long getTime(String time) {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        Date date = null;
        try {
            date = format.parse(time);
            return date.getTime();
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return 0;
    }
public static Map<TopicPartition, OffsetAndTimestamp> getTimeOffsetMap(String server, String topic, String groupId, long time) {
    //相关配置信息
    Properties props = new Properties();
    props.put("bootstrap.servers", server);
    props.put("group.id", groupId);
    props.put("enable.auto.commit", "false");
    props.put("auto.commit.interval.ms", "1000");
    props.put("session.timeout.ms", "30000");
    props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    //获取kafka请求对象
    KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
    consumer.subscribe(Arrays.asList(topic));
    //获取主题分区数量
    Collection<PartitionInfo> partitionInfos = consumer.partitionsFor(topic);
    int partitionNum = partitionInfos.size();
    //遍历分区,将分区和时间信息存入map
    Map<TopicPartition, Long> partitionMap = new HashMap<>();
    for (int i = 0; i < partitionNum; i++) {
        TopicPartition topicPartition0 = new TopicPartition(topic, i);
        partitionMap.put(topicPartition0, time);
    }
    //获取分区和偏移量集合
    Map<TopicPartition, OffsetAndTimestamp> timeOffsetMap = consumer.offsetsForTimes(partitionMap);
    return timeOffsetMap;
}

10. 将主题、消费组的分区偏移量回滚至指定时间

public static void setOffset(String server, String topic, String groupId, long time) {
    //相关配置信息
    Properties props = new Properties();
    props.put("bootstrap.servers", server);
    props.put("group.id", groupId);
    props.put("enable.auto.commit", "false");
    props.put("auto.commit.interval.ms", "1000");
    props.put("session.timeout.ms", "30000");
    props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    //获取kafka请求对象
    KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
    consumer.subscribe(Arrays.asList(topic));
    //获取主题分区数量
    Collection<PartitionInfo> partitionInfos = consumer.partitionsFor(topic);
    int partitionNum = partitionInfos.size();
    //遍历分区,将分区和时间信息存入map
    Map<TopicPartition, Long> partitionMap = new HashMap<>();
    for (int i = 0; i < partitionNum; i++) {
        TopicPartition topicPartition0 = new TopicPartition(topic, i);
        partitionMap.put(topicPartition0, time);
    }
    //获取分区和偏移量集合
    Map<TopicPartition, OffsetAndTimestamp> timeOffsetMap = consumer.offsetsForTimes(partitionMap);
    //在调用seek方法的时候,需要先获得分区的信息,而分区的信息要通过poll方法来获得.
    // 如果调用seek方法时,没有分区信息,则会抛出IllegalStateException异常 No current assignment for partition xxxx.
    ConsumerRecords<String, String> poll = consumer.poll(0);
    //进行遍历、重新设置主题的各个分区偏移量
    Set<Map.Entry<TopicPartition, OffsetAndTimestamp>> entries = timeOffsetMap.entrySet();
    for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : entries) {
        if (entry.getValue() == null) {
            consumer.seek(new TopicPartition(entry.getKey().topic(), entry.getKey().partition()), 99);
            continue;
        }
        //设置偏移量,根据主题,分区来设置
        consumer.seek(new TopicPartition(entry.getKey().topic(), entry.getKey().partition()), entry.getValue().offset());
    }
    //提交
    consumer.commitSync();
    consumer.close();
}

11. 新增分区

 /**
     * 更新分区
     *
     * @param bootstrapServers
     * @param loginKey
     * @param topicName
     * @param partitions
     * @throws IOException
     * @throws ExecutionException
     * @throws InterruptedException
     */
    public static void createTopicPartitions(String bootstrapServers, String topicName, Integer partitions) throws IOException, ExecutionException, InterruptedException {
        AdminClient admin = getAdminClient(bootstrapServers);
        //构建Map
        Map<String, NewPartitions> newPartitions = new HashMap<>();
        //给Map存入Topic名字和想要增加到的partition数量。
        // 这里注意increaseTo,参数传入多少就是增加到多少,3就是增加到3,不是1+3=4
        newPartitions.put(topicName, NewPartitions.increaseTo(partitions));
        //拿到结果
        CreatePartitionsResult result = admin.createPartitions(newPartitions);
        //执行阻塞方法,等待结果完成
        result.all().get();
    }

十、相关问题解决优化

  1. 防止消息丢失
    1.1 发送方:ack设置为1 或者 all,这样子就可以保证至少有一个副本可以被写入数据。
    1.1 消费方:将自动提交改为手动提交
  2. 防止消息的重复消费
    如因为网络抖动,producer已经将消息已经发给kafka,但是producer没收到ack,导致重新发了一次,所以消费者收到了两条消息。
    解决:prodecer保留重发机制,防止消息丢失。可以在消费者端配置消息幂等性,如在数据库设置一个联合主键、或者使用redis锁,保证消息唯一性。
  3. 消息大量堆积:可以将消息直接poll后,存到另一个主题中。然后配置多个分区,消费组里配置多个消费者,一起消费。
  4. 消费者中途断掉重连:一方面,在同一组中,如果单个消费者挂掉,会自动分配给其他消费者。另一方面,如果就一个消费者,挂掉后,kafka会保存上次消费的offset位置,重连后会自动读取offset后面的数据。
  5. 线程(concurrency)和消费者的关系:这个参数来是设置每个@KafkaListener的并发个数。通俗的说,他指的应该是单个消费者的并发数,如果是3个消费者,线程为3,那其实并发数可以达到9。如果就3个分区,1个消费者的并发数就够了,而并不是指并发多少个消费者。
  6. 脏数据导致需要从某一时间点进行重新接数据:根据消费组、主题获取各个分区在指定时间的偏移量,然后重新设定偏移量即可。
  7. 脏数据需要从未来某个时间接入:建议生产者与消费者处于不同服务。对于脏数据,可以暂时断开消费者,等数据正常后直接将偏移量设置最大,即跳过脏数据,从数据正常的偏移开始消费。
  8. 删除节点bug(windows未解决,liunx正常)
    ps1:删除zookeeper注册的主题,但是kafka还能读取的到,并且kafka无法新建主题
    ps2:删除kafka的主题,kafka直接崩溃,因为删除时候需要删除日志文件,此时kafka还在运行,无权删除,导致崩溃。同时启动会失败,需要删除zookeeper注册的主题才可启动。并且启动后可以直接创建主题。
    ps3:删除zookeeper注册的主题、删除kafka的主题。kafka同样会崩溃,但是可以直接启动,启动后可以直接创建主题。
    ps4:windows删除方案–>删除zookeeper注册主题,在kafka停止后,直接删除kafka的本地数据log文件即可。(猜测可能是windows存在改问题。暂未验证linux,哪位小伙伴明知道的可以底下留言)
    ps5:linuxkafka调试正常,未出现删除kafka崩溃现象。建议使用linux环境部署kafka
  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值