kafka学习

本文深入介绍了Apache Kafka,包括消息队列的作用、Kafka的组件(Broker、Topic、Producer、Consumer、ConsumerGroup)以及分区概念。详细阐述了Kafka的安装、创建Topic、发送和消费消息的过程,探讨了单播与多播的区别,并展示了Java客户端实现Kafka生产者和消费者的示例。此外,还提到了Kafka集群操作、消费者组细节、线程安全与幂等性、消息丢失与重复消费的预防策略,以及SpringBoot整合Kafka的方法。文章最后讨论了Kafka的性能优化和延时队列实现策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、消息队列

1.解决多个服务之间通信的方法

  • 同步
    – 网络通信等原因,每个服务之间可能有很大的响应时间和开销
    – 整个流程中的服务全部顺利执行完才会成功,用户体验极差
  • 异步
    – 上游将所需的服务放到消息队列中,下游服务与消息队列采用生产者-消费者模型完成服务
    – 上游服务即刻成功并进行反馈,提升系统吞吐量与用户体验
    – 即使下游有服务失败,也可以通过分布式事务来解决保证最终一致性
    实质:消息队列解决的是通信问题

2.不同消息队列流派间的区别

基本概念

名称解释
Broker消息中间件处理节点,⼀个Kafka节点就是⼀个broker,⼀个或者多个Broker可以组成⼀个Kafka集群
TopicKafka根据topic对消息进⾏归类,发布到Kafka集群的每条消息都需要指定⼀个topic
Producer消息⽣产者,向Broker发送消息的客户端
Consumer消息消费者,从Broker读取消息的客户端
ConsumerGroup每个Consumer属于⼀个特定的Consumer Group,⼀条消息可以被多个不同的Consumer Group消费,但是⼀个Consumer Group中只能有⼀个Consumer能够消费该消息
Partition物理上的概念,⼀个topic可以分为多个partition,每个partition内部消息是有序的

kafka属于有Broker,重Topic

二、kafka的基本使用

安装

官网地址:https://kafka.apache.org/downloads
阿里镜像:http://mirrors.aliyun.com/apache/kafka/
1、下载后解压后创建logs目录
2、修改service.properties

log.dirs=/opt/kafka/data/kafka-logs
listeners=PLAINTEXT://localhost:9092

3、启动zookeeper

zookeeper-server-start.sh -daemon ./config/zookeeper.properties

4、启动kafka

bin/kafka-server-start.sh -daemon ./config/server.properties

5、在zookeeper中查看节点目录可以得知kafka是否启动成功

[zk: localhost:2181(CONNECTED) 0] ls /
[admin, brokers, cluster, config, consumers, controller, controller_epoch, feature, isr_change_notification, latest_producer_id_block, log_dir_event_notification, zookeeper]

启动失败可至kafka/logs/server.log中查看失败原因

创建Topic

  • 通过kafka命令向zk中创建一个主题
[root@iZ0jli9zrxt29wd918ru5mZ kafka] bin/kafka-topics.sh --create --topic quickstart-events --bootstrap-server localhost:9092
Created topic quickstart-events.
  • zookeeper中查看
[zk: localhost:2181(CONNECTED) 4] ls /
[admin, brokers, cluster, config, consumers, controller, controller_epoch, feature, isr_change_notification, latest_producer_id_block, log_dir_event_notification, zookeeper]
  • 通过kafka查看topic的详情
[root@iZ0jli9zrxt29wd918ru5mZ kafka] bin/kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092
Topic: quickstart-events	TopicId: 7q3jJxRyT_eVbWnFKsAIdQ	PartitionCount: 1 ReplicationFactor: 1	Configs: segment.bytes=1073741824
	Topic: quickstart-events	Partition: 0	Leader: 0	Replicas: Isr: 0

事件

  • 生产者将事件写入topic
[root@iZ0jli9zrxt29wd918ru5mZ kafka] bin/kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092
>This is my first event
>This is my second event
  • 消费者消费事件
# --from-beginning从开始读写入的事件(包括连入之前的事件)
[root@iZ0jli9zrxt29wd918ru5mZ bin] ./kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
This is my first event
This is my second event
# 从连入之后产生的事件开始读
[root@iZ0jli9zrxt29wd918ru5mZ bin]# ./kafka-console-consumer.sh --topic quickstart-events --bootstrap-server localhost:9092

三、单播与多播

[root@iZ0jli9zrxt29wd918ru5mZ bin] ./kafka-console-consumer.sh --bootstrap-server localhost:9092 --consumer-property group.id=testGroup --from-beginning --topic quickstart-events		

利用“group.id=”来区分是否是同一个组

  • 单播
    同一个Topic的同一个组,只有一个消费者可以接收到消息
  • 多播
    同一个Topic的不同组,每组只有一个消费者可以接收到消息

查看消费组的详细信息

[root@iZ0jli9zrxt29wd918ru5mZ bin] ./kafka-consumer-groups.sh  --bootstrap-server localhost:9092 --describe --group testGroup

Consumer group 'testGroup' has no active members.

GROUP           TOPIC             PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG             CONSUMER-ID     HOST            CLIENT-ID
testGroup       quickstart-events 0          15              19              4               -               -               -
  • GROUP:组名
  • TOPIC:topic组名
  • PARTITION:分区
  • CURRENT-OFFSET:最后被消费的消息的偏移量
  • LOG-END-OFFSET:消息总量
  • LAG:挤压消息量

四、kafka中分区与主题的概念

主题:Topic

  • 主题是一个逻辑概念,kafka通过topic将消息划分,给不同的消费者。但如果消息量过大,存到log文件中到要用几T才能存下来的时候,为了解决文件过大的问题,kafka提出了分区的概念。

分区:partition

在这里插入图片描述

  • 通过patition将topic消息分区存储的好处
    – 解决统一存储文件过大的问题
    – 提供了读写吞吐量,读和写可以同时在多个分区中进行

创建

./kafka-topics.sh --create --zookeeper 172.16.253.35:2181 --replication-factor 1 --partitions 2 --topic test1

kafka消息⽇志⽂件中保存的内容

  • 000000000.log中保存的消息
  • _consumer_offsets-49:
    – kafka中共创建了50个分区,每个消费者都会维护消费的主题的偏移量,也就是说每个消费者会把消费的主题的偏移量⾃主上报给kafka中的默认主题:consumer_offsets。因此kafka为了提升这个主题的并发性,默认设置了50个分区。
  • 七天后会被删除

五、kafka集群操作

1.搭建kafka集群(三个broker)

  • 创建三个server.properties
# 0 1 2
broker.id=2
# 9092 9093 9094
listeners=PLAINTEXT://192.168.65.60:9094
#kafka-logs kafka-logs-1 kafka-logs-2
log.dir=/usr/local/data/kafka-logs-2
  • 启动三台broker
./kafka-server-start.sh -daemon ../config/server.properties
./kafka-server-start.sh -daemon ../config/server1.properties
./kafka-server-start.sh -daemon ../config/server2.properties
  • 校验是否成功
ls /brokers/ids
[0, 1, 2]

2.副本

创建带有副本的topic

[root@ecs-219137 bin] ./kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --create --topic my-replicated-topic --partitions 2 --replication-factor 3
Created topic my-replicated-topic.
  • –partitions 2 :分区数
  • –replication-factor 3:副本数

查看topic

[root@ecs-219137 bin] ./kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --describe --topic my-replicated-topic
Topic: my-replicated-topic	TopicId: hfq-8L25T3qqGB41NwjJVw	PartitionCount: 2 ReplicationFactor: 3	Configs: segment.bytes=1073741824
	Topic: my-replicated-topic	Partition: 0	Leader: 2	Replicas: 2,1,0	Isr: 2,1,0
	Topic: my-replicated-topic	Partition: 1	Leader: 1	Replicas: 1,0,2	Isr: 1,0,2

在这里插入图片描述
将消息拆分到两个分区中,每个分区创建三个副本

  • 对于分区0来说
    – 此时leader是2
    – 0和1会从2同步消息
  • 对于分区1来说
    – 此时leader是1
    – 0和2会从1同步消息
  • isr:可以同步和已同步的节点

3.集群消费

向集群发送消息

./kafka-console-producer.sh --broker-list 127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094 --topic my-replicated-topic

从集群中消费消息(指定消费组)

./kafka-console-consumer.sh --bootstrap-server 127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094 --from-beginning --consumer-property group.id=testGroup1 --topic my-replicated-topic

ps:不允许自动创建未创建的topic,如果报错检查是否已创建topic

消费组中的细节

在这里插入图片描述

  • 一个partition只能由一个consumer来消费
  • 一个group中的consumer数量最好不要超过partition数量,否则多余的consumer消费不到消息
  • 如果消费者挂了,那么会触发rebalance机制,会让其他消费者来消费该分

六、java客户端实现kafka生产者

引入依赖

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.1.0</version>
</dependency>

生产者发消息的基本实现

public class MySimpleProducer {
    private final static String TOPIC_NAME = "my-replicated-topic";

    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        //1.设置参数
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
        //把发送的key从字符串序列化为字节数组
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //把发送消息value从字符串序列化为字节数组
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //2.创建⽣产消息的客户端,传⼊参数
        Producer<String, String> producer = new KafkaProducer<String, String>(props);
        //3.创建消息
        //key:作⽤是决定了往哪个分区上发,value:具体要发送的消息内容
        ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, 0, "mykeyvalue", "hellokafka");
        /*//4.发送消息,得到消息发送的元数据并输出
        RecordMetadata metadata = producer.send(producerRecord).get();
        System.out.println("同步⽅式发送消息结果:" + "topic-" + metadata.topic() + "|partition-" + metadata.partition() + "|offset-" + metadata.offset());*/

        //异步回调⽅式发送消息
        producer.send(producerRecord, new Callback() {
            public void onCompletion(RecordMetadata metadata, Exception
                    exception) {
                if (exception != null) {
                    System.err.println("发送消息失败:" + exception.getStackTrace());
                }
                if (metadata != null) {
                    System.out.println("异步⽅式发送消息结果:" + "topic-" + metadata.topic() + "|partition-" + metadata.partition() + "|offset-" + metadata.offset());
                }
            }
        });

        Thread.sleep(10000000L);
    }
}

生产者同步发送

在这里插入图片描述
如果⽣产者发送消息没有收到ack,⽣产者会阻塞,阻塞到3s的时间,如果还没有收到消息,
会进⾏重试。重试的次数3次。

RecordMetadata metadata = producer.send(producerRecord).get();
System.out.println("同步⽅式发送消息结果:" + "topic-" + metadata.topic() + "|partition-" + metadata.partition() + "|offset-" + metadata.offset());

生产者异步发送

在这里插入图片描述
异步发送,⽣产者发送完消息后就可以执⾏之后的业务,broker在收到消息后异步调⽤⽣产
者提供的callback回调⽅法。

producer.send(producerRecord, new Callback() {
    public void onCompletion(RecordMetadata metadata, Exception
            exception) {
        if (exception != null) {
            System.err.println("发送消息失败:" +
                    exception.getStackTrace());
        }
        if (metadata != null) {
            System.out.println("异步⽅式发送消息结果:" + "topic-" +
                    metadata.topic() + "|partition-" + metadata.partition() + "|offset-" + metadata.offset());
        }
    }
});
//休眠看到callback的发生
Thread.sleep(10000000L);

生产者中的ack配置

同步发送的前提下,生产者在获得集群返回的ack前会一直阻塞
ack的配置:

  • ack=0
    – kafka集群不需要任何的broker收到消息,就⽴即返回ack给⽣产者
    – 最容易丢消息,效率最高
  • ack=1(默认)
    – 多副本之间的leader已经收到消息,并把消息写⼊到本地的log中,才会返回ack给⽣产者
    – 性能和安全性是最均衡的
  • ack=-1/all
    – ⾥⾯有默认的配置min.insync.replicas=2(默认为1,推荐配置⼤于等于2),此时就需要leader和⼀个follower同步完后,才会返回ack给⽣产者(此时集群中有2个broker已完成数据的接收)
    – 最安全,但性能最差。
props.put(ProducerConfig.ACKS_CONFIG, "1");
/*发送失败会重试,默认重试间隔100ms,重试能保证消息发送的可靠性,但是也可能造
成消息重复发送,⽐如⽹络抖动,所以需要在
接收者那边做好消息接收的幂等性处理*/
props.put(ProducerConfig.RETRIES_CONFIG, 3);
//重试间隔设置
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);
//kafka默认会创建⼀个消息缓冲区,⽤来存放要发送的消息,缓冲区是32m
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
//kafka本地线程会去缓冲区中⼀次拉16k的数据,发送到broker
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
//如果线程拉不到16k的数据,间隔10ms也会将已拉到的数据发到broker
props.put(ProducerConfig.LINGER_MS_CONFIG, 10);

七、java客户端实现kafka消费者

实现

public class MySimpleConsumer {
    private final static String TOPIC_NAME = "my-replicated-topic";
    private final static String CONSUMER_GROUP_NAME = "testGroup";

    public static void main(String[] args) {
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
        // 消费分组名
        props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        //1.创建⼀个消费者的客户端
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
        //2. 消费者订阅主题列表
        consumer.subscribe(Arrays.asList(TOPIC_NAME));
        while (true) {
            /*
             * 3.poll() API 是拉取消息的⻓轮询
             */
            ConsumerRecords<String, String> records =
                    consumer.poll(Duration.ofMillis(1000));
            for (ConsumerRecord<String, String> record : records) {
                //4.打印消息
                System.out.printf("收到消息:partition = %d,offset = %d, key = %s, value = %s %n ", record.partition(), record.offset(), record.key(), record.value());
            }
        }
    }
}

提交offset

  • 提交的内容
    消费者⽆论是⾃动提交还是⼿动提交,都需要把所属的消费组+消费的某个主题+消费的某个
    分区及消费的偏移量,这样的信息提交到集群的_consumer_offsets主题⾥⾯。
  • 自动提交
    消费者poll下来之后就会自动提交offset
// 是否⾃动提交offset,默认就是true
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
// ⾃动提交offset的间隔时间
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");

如果poll下来之后消费者挂了会产生丢消息的情况

  • 手动提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");

– 手动同步提交
在消费完消息后调⽤同步提交的⽅法,当集群返回ack前⼀直阻塞,返回ack后表示提交
成功,执⾏之后的逻辑

while (true) {
    /*
     * poll() API 是拉取消息的⻓轮询
     */
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
    for (ConsumerRecord<String, String> record : records) {
        System.out.printf("收到消息:partition = %d,offset = %d, key = %s, value = %s%n ", record.partition(), record.offset(), record.key(), record.value());
    }
    //所有的消息已消费完
    if (records.count() > 0) {//有消息
        // ⼿动同步提交offset,当前线程会阻塞直到offset提交成功
        // ⼀般使⽤同步提交,因为提交之后⼀般也没有什么逻辑代码了
        consumer.commitSync();//=======阻塞=== 提交成功
    }
}

– 手动异步提交
在消息消费完后提交,不需要等到集群ack,直接执⾏之后的逻辑,可以设置⼀个回调⽅
法,供集群调⽤

while (true) {
   /*
    * poll() API 是拉取消息的⻓轮询
    */
   ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
   for (ConsumerRecord<String, String> record : records) {
       System.out.printf("收到消息:partition = %d,offset = %d, key = %s, value = %s %n", record.partition(), record.offset(), record.key(), record.value());
   }
   //所有的消息已消费完
   if (records.count() > 0) {
       // ⼿动异步提交offset,当前线程提交offset不会阻塞,可以继续处理后⾯的程序逻辑
       consumer.commitAsync(new OffsetCommitCallback() {
           @Override
           public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
               if (exception != null) {
                   System.err.println("Commit failed for " + offsets);
                   System.err.println("Commit failed exception: " + exception.getStackTrace());
               }
           }
       });
   }
}

长轮询poll消息

默认情况下消费者一次会poll500条消息

//⼀次poll最⼤拉取消息的条数,可以根据消费速度的快慢来设置
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);

长轮询时间为1000ms

while (true) {
   /*
    * poll() API 是拉取消息的⻓轮询
    */
   ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
   for (ConsumerRecord<String, String> record : records) {
       System.out.printf("收到消息:partition = %d,offset = %d, key = %s, value = %s %n", record.partition(), record.offset(), record.key(), record.value());
   }
  • 如果一次poll500条消息就会进行for循环
  • 如果1s内没有poll到500条消息(可累加)也会进行for循环
  • 如果两次poll的间隔超过30s,集群会认为该消费者的消费能⼒过弱,该消费者被踢出消
    费组,触发rebalance机制,rebalance机制会造成性能开销。可以通过设置这个参数,
    减少⼀次poll的消息的数量
//⼀次poll最⼤拉取消息的条数,可以根据消费速度的快慢来设置
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
//如果两次poll的时间如果超出了30s的时间间隔,kafka会认为其消费能⼒过弱,将其踢出消费组。将分区分配给其他消费者。-rebalance
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);

消费者健康状态检查

消费者每隔1s向kafka集群发送⼼跳,集群发现如果有超过10s没有续约的消费者,将被踢出
消费组,触发该消费组的rebalance机制,将该分区交给消费组⾥的其他消费者进⾏消费。

//consumer给broker发送⼼跳的间隔时间
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);
//kafka如果超过10秒没有收到消费者的⼼跳,则会把消费者踢出消费组,进⾏rebalance,把分区分配给其他消费者。
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);

其他配置

//指定分区消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
//从头消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));
//指定offset消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seek(new TopicPartition(TOPIC_NAME, 0), 10);

指定时间消费

List<PartitionInfo> topicPartitions = consumer.partitionsFor(TOPIC_NAME);
//从1⼩时前开始消费
long fetchDataTime = new Date().getTime() - 1000 * 60 * 60;
Map<TopicPartition, Long> map = new HashMap<>();
for (PartitionInfo par : topicPartitions) {
    map.put(new TopicPartition(TOPIC_NAME, par.partition()), fetchDataTime);
}
Map<TopicPartition, OffsetAndTimestamp> parMap = consumer.offsetsForTimes(map);
for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : parMap.entrySet()) {
    TopicPartition key = entry.getKey();
    OffsetAndTimestamp value = entry.getValue();
    if (key == null || value == null) continue;
    Long offset = value.offset();
    System.out.println("partition-" + key.partition() + "|offset-" + offset);
    System.out.println();
    //根据消费⾥的timestamp确定offset
    if (value != null) {
        consumer.assign(Arrays.asList(key));
        consumer.seek(key, offset);
    }
}

根据时间,去所有的partition中确定该时间对应的offset,然后去所有的partition中找到该
offset之后的消息开始消费。

新消费组的消费规则

新消费组中的消费者在启动以后,默认会从当前分区的最后⼀条消息的offset+1开始消费(消
费新消息)。

props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
  • latest:默认从最后一条开始消费
  • earliest:第一次从头开始消费,之后都从自己最新的开始消费

八、SpringBoot整合kafka

引入依赖

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

编写配置文件

spring:
  kafka:
    bootstrap-servers: 127.0.0.1:9092
    producer: # ⽣产者
      # 设置⼤于0的值,则客户端会将发送失败的记录重新发送
      retries: 3
      #kafka本地线程会去缓冲区中⼀次拉16k的数据,发送到broker
      batch-size: 16384
      #kafka默认会创建⼀个消息缓冲区,⽤来存放要发送的消息,缓冲区是32m
      buffer-memory: 33554432 #32M
      #leader收到消息,并把消息写⼊到本地的log中,返回ack给⽣产者
      acks: 1
    # 指定消息key和消息体的编解码⽅式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer:
      group-id: default-group
      enable-auto-commit: false
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      max-poll-records: 500
    listener:
      # 当每⼀条记录被消费者监听器(ListenerConsumer)处理之后提交
      # RECORD
      # 当每⼀批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
      # BATCH
      # 当每⼀批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间⼤于TIME时提交
      # TIME
      # 当每⼀批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量⼤于等于COUNT时提交
      # COUNT
      # TIME | COUNT 有⼀个条件满⾜时提交
      # COUNT_TIME
      # 当每⼀批poll()的数据被消费者监听器(ListenerConsumer)处理之后, ⼿动调⽤Acknowledgment.acknowledge()后提交
      # MANUAL
      # ⼿动调⽤Acknowledgment.acknowledge()后⽴即提交,⼀般使⽤这种
      # MANUAL_IMMEDIATE
      ack-mode: MANUAL_IMMEDIATE
  • auto-offset-reset
    – latest:从当前offset开始消费
    – earliest:只有第一次从最开始进行消费,之后都从当前offset开始消费(有时会造成消息的重复提醒,不重启服务器的话没有问题)

编写生产者

@RestController
@RequestMapping("/msg")
public class MyKafkaProducer {
    private static final String TOPIC_NAME = "my-replicated-topic";
    @Autowired
    private KafkaTemplate kafkaTemplate;

    @RequestMapping("/send")
    public String sendMsg() {
        kafkaTemplate.send(TOPIC_NAME, 0, "key", "this is a message");
        return "success!";
    }
}

编写消费者

@Component
public class MyKafkaConsumer {
    @KafkaListener(topics = "my-replicated-topic", groupId = "MyGroup1")
    public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        //⼿动提交offset
        ack.acknowledge();
    }
}

消费者中的配置

acknowledge的配置:

listener:
 # 当每⼀条记录被消费者监听器(ListenerConsumer)处理之后提交
 # RECORD
 # 当每⼀批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
 # BATCH
 # 当每⼀批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间⼤于TIME时提交
 # TIME
 # 当每⼀批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量⼤于等于COUNT时提交
 # COUNT
 # TIME | COUNT 有⼀个条件满⾜时提交
 # COUNT_TIME
 # 当每⼀批poll()的数据被消费者监听器(ListenerConsumer)处理之后, ⼿动调⽤Acknowledgment.acknowledge()后提交
 # MANUAL
 # ⼿动调⽤Acknowledgment.acknowledge()后⽴即提交,⼀般使⽤这种
 # MANUAL_IMMEDIATE
 ack-mode: MANUAL_IMMEDIATE

配置分组,分区,偏移量

@Component
public class MyKafkaConsumer {
    @KafkaListener(groupId = "testGroup", topicPartitions = {
            @TopicPartition(topic = "topic1", partitions = {"0", "1"}),
            @TopicPartition(topic = "topic2", partitions = "0", partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))}, concurrency = "3")//concurrency就是同组下的消费者个数,就是并发消费数,建议⼩于等于分区总数
    public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        //⼿动提交offset
        ack.acknowledge();
    }
}

九、kafka集群中的controller、rebalance、HW

controller

  • 每个集群创建时会像zk创建一个临时序号节点,序号最小的broker会作为集群的broker
  • controller功能
    – 重新选举leader:选举isr集合中最左面的follower
    – 当集群中的broker或partition有变动,同步给其他broker

rebalance机制

  • 前提:消费组中消费者没有指明partition
  • 触发条件:消费者和分区的关系出现变化(消费者宕机)
  • 分配策略
    – range:分区数/消费者数
    7个分区3个消费者,则每个消费者分别分到3/2/2个分区
    – 轮询:轮着给每个消费者指定partition
    – sticky:粘合策略
    如果需要rebalance会在之前以分配好的基础上将需要分配的partition重新分配,如果不开启会将所有分区重新分配,影响性能。

HW和LEO

  • leo:某个副本最后消息的消息位置(log-end-offset)
  • HW:已同步完的位置
    消息在写入broker时,如果有副本需要往副本同步,若未完成同步则消费者是无法消费leader中最新的这条消息的,如果同步完成之后消费者则可以消费,已完成同步的消息的位置就称之为HW(水位线),目的是防止消息丢失(未完成同步时leader宕机)

在这里插入图片描述

十、kafka线上的优化问题

如何防止消息丢失

  • 生产者
    – 使用同步发送
    – ack值设为1或者all
    – 设置同步分区数>=2
  • 消费者
    – 把自动提交改为手动提交

如何防止重复消费

  • 原因:生产者将消息发送给broker后由于网络抖动生产者没有收到broker发送的ack确认,此时生产者重新发送消息,造成消费者的重复消费
  • 解决方案
    – 生产者关闭重试(不建议,会造成消息丢失)
    – 消费者解决非幂等消费问题
    (幂等性:多次访问的结果是一样的。对于rest请求,get,put,del是幂等,post是非幂等)
    在数据库中创建联合索引,防止创造出多条记录
    使用分布式锁,以业务id为锁,保证只有一条记录能够成功创建
    在这里插入图片描述

如何顺序消费

  • 生产者:使用同步发送,ack设置为非0
  • 消费者:一个topic只能由一个patition,一个group中只能由一个consumer
  • kafka对顺序消费没有功能支持,建议使用rocketmq

消息挤压怎么解决

  • 原因:消费者消费的速度太慢赶不上生产消息的速度,大量未消费的消息堆积,从而对消费者的寻址造成影响,导致性能极差,可能影响其他服务的访问速度,造成雪崩
  • 解决办法:
    – 使用多线程,充分利用机器性能
    – 优化代码和架构,提升消费者消费能力
    – 创建爱你多个消费组,部署到其他机器上
    – 创建⼀个消费者,该消费者在kafka另建⼀个主题,配上多个分区,多个分区再配上多个
    消费者。该消费者将poll下来的消息,不进⾏消费,直接转发到新建的主题上。此时,新
    的主题的多个分区的多个消费者就开始⼀起消费了。——不常⽤

延时队列的实现

  • 场景:订单30分钟后自动取消
  • 方案:
    – 去kafka中创建响应主题
    – 消费者轮询消费该主题
    – 消费者判断是否超时
    超时则改变订单状态
    未超时则记录当前offset,并不再继续消费之后的消息,等待一个固定时间后在此拉取该offset及之后的消息。进行判断
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值