Kafka消息队列入门

rabbitMQ的区别

RabbitMQ支持事务

Kafka性能高

消息队列的功能

流量消峰

异步通信

解耦

消息队列的模式

(1)Producer:消息生产者,就是向 Kafka broker 发消息的客户端。

(2)Consumer:消息消费者,向 Kafka broker 取消息的客户端。

(3)Consumer Group(CG):消费者组,由多个 consumer 组成。消费者组内每个消 费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不 影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。

(4)Broker:一台 Kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个 broker 可以容纳多个 topic。

(5)Topic:可以理解为一个队列,生产者和消费者面向的都是一个 topic。

(6)Partition:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服 务器)上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。

(7)Replica:副本。一个 topic 的每个分区都有若干个副本,一个 Leader 和若干个 Follower。 (8)Leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数 据的对象都是 Leader。

(9)Follower:每个分区多个副本中的“从”,实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,某个 Follower 会成为新的 Leader

Kafka安装

基本概念

Kafka命令

创建主题

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

查看有哪些主题

./kafka-topics.sh --list --zookeeper 172.16.253.35:2181

发送消息

./kafka-console-producer.sh --broker-list 172.16.253.38:9092 --topic test

消费消息,从尾开始

./kafka-console-consumer.sh --bootstrap-server 172.16.253.38:9092 -- topic test

消费消息,从头开始

./kafka-console-consumer.sh --bootstrap-server 172.16.253.38:9092 -- from-beginning --topic test

172.16.253.38:9092 为你的Kafka地址

172.16.253.35:2181为你的zookeeper地址

单/多播消息

单播消息:⼀个消费组⾥ 只会有⼀个消费者能消费到某⼀个topic中的消息。于是可以创建多 个消费者,这些消费者在同⼀个消费组中

./kafka-console-consumer.sh --bootstrap-server 10.31.167.10:9092 -- consumer-property group.id=testGroup --topic test

多播消息:在⼀些业务场景中需要让⼀条消息被多个消费者消费,那么就可以使⽤多播模式。 kafka实现多播,只需要让不同的消费者处于不同的消费组即可。

./kafka-console-consumer.sh --bootstrap-server 10.31.167.10:9092 -- consumer-property group.id=testGroup1 --topic test

./kafka-console-consumer.sh --bootstrap-server 10.31.167.10:9092 -- consumer-property group.id=testGroup2 --topic test

查看消费组及信息

#查看当前主题下有哪些消费组 ./kafka-consumer-groups.sh --bootstrap-server 10.31.167.10:9092 --list

# 查看消费组中的具体信息:⽐如当前偏移量、最后⼀条消息的偏移量、堆积的消息数量

./kafka-consumer-groups.sh --bootstrap-server 172.16.253.38:9092 -- describe --group testGroup

主题和分区

当主题过大,为了解决文件过大的问题,kafka提供了一个分区的概念

相当于把一个大的Topic拆分成几个小文件

可以并行写/读,提高生产者/消费者的效率

实际上是存在data/kafka-logs/test-0 和 test-1中的0000000.log⽂件中

为一个主题创建多个分区

 ./kafka-topics.sh --create --zookeeper localhost:2181 --partitions 2 -- topic test

查看分区信息

./kafka-topics.sh --describe --zookeeper localhost:2181 --topic test

Kafka文件

实际上是存在data/kafka-logs/test-0 和 test-1中的0000000.log⽂件中

定期将⾃⼰消费分区的offset提交给kafka内部topic:__consumer_offsets,提交过去的 时候,key是consumerGroupId+topic+分区号,value就是当前offset的值,kafka会定 期清理topic⾥的消息,最后就保留最新的那条数据 因为__consumer_offsets可能会接收⾼并发的请求,kafka默认给其分配50个分区(可以 通过offsets.topic.num.partitions设置),这样可以通过加机器的⽅式抗⼤并发。通过如下公式可以选出consumer消费的offset要提交到__consumer_offsets的哪个分区 公式:hash(consumerGroupId) % __consumer_offsets主题的分区数

简单说就是,offset存储在__consumer_offsets里面,__consumer_offsets一共有50个,为了提高并发量,实现并发写

Kafka集群

修改配置文件

broker.id=0

listeners=PLAINTEXT://192.168.65.60:9092

log.dir=/usr/local/data/kafka-logs

每个节点的配置文件都应该不一样

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

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

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

在使用命令分别指定配置文件进行启动

每个Broker有两个分区

每个分区有三个副本,再不同Broker上

集群消费/发送

集群发送消息

./kafka-console-producer.sh --broker-list 172.16.253.38:9092,172.16.253.38:9093,172.16.253.38:9094 --topic myreplicated-topic

集群消费消息

./kafka-console-consumer.sh --bootstrap-server 172.16.253.38:9092,172.16.253.38:9093,172.16.253.38:9094 --frombeginning --topic my-replicated-topic

集群消费机制

在 Kafka 中,每个分区只能被消费者组中的一个消费者消费。这是 Kafka 消费者组的工作方式。具体来说:

  • 对于一个主题的分区,消费者组内的消费者不能同时消费同一个分区的消息。
  • 每个分区只能分配给消费者组内的一个消费者,确保了消息的有序性和一致性。

这种机制确保了每条消息只会被消费者组内的一个消费者处理,但不同分区的消息可以并行地被不同消费者处理。这也是 Kafka 实现高吞吐量和水平扩展的关键机制之一。

如果希望多个消费者并行处理消息,您可以将消费者部署到不同的消费者组,或者确保主题的分区数大于消费者的数量,这样每个消费者都能被分配到至少一个分区来处理消息。

有序性是指partition内部的有序性,消费者只连接一个partition

 Kafka动态分区

Kafka 中的分区分配是动态的,但分配策略的变化不会发生在每次接收到新消息时。一旦分配给消费者的分区被确定,通常会保持不变一段时间,以确保消息的有序消费。但在一些情况下,分区分配可能会发生变化:

  1. 消费者加入或离开: 当消费者加入消费者组或离开消费者组时,分区可能会重新分配。这确保了分区在消费者组内的平衡负载。

  2. 新的主题:有新的主题被创建,重新平衡机制可能会触发,导致分区重新分配。

  3. 手动分配: 您也可以通过手动指定分区分配来更改消费者的分区分配,但这需要谨慎使用,以避免引入不必要的复杂性。

在您提到的情况中,即分配给消费者 A 的 Partition 0 在下一次消息到达时分配给消费者 B,这不是 Kafka 默认的行为。Kafka 倾向于保持分区分配的稳定性,以确保消息的有序消费。分区重新分配通常在消费者加入、离开或主题变更等情况下发生。

如果您需要更深入地了解 Kafka 消费者组的分区分配和重新平衡机制,可以查阅 Kafka 官方文档中关于"Consumer Rebalance"的内容。

Java-api

在java中可以对数据进行发送

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

生产者基本实现

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;
import java.util.concurrent.ExecutionException;

/**
 * @projectName: kafkaTest
 * @package: PACKAGE_NAME
 * @className: MySimpleProducer
 * @author: Eric
 * @description: TODO
 * @date: 8/7/2023 4:03 PM
 * @version: 1.0
 */
public class MySimpleProducer {
    private final static String TOPIC_NAME = "myreplicated-topic";
    public static void main(String[] args)  {
        //1.设置参数
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
                "192.168.232.151:9092,192.168.232.151:9093,192.168.232.151:9094");
        //把发送的key从字符串序列化为字节数组
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
                StringSerializer.class.getName());
        //把发送消息value从字符串序列化为字节数组
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                StringSerializer.class.getName());
        //2.创建⽣产消息的客户端,传⼊参数
        try(Producer<String,String> producer = new KafkaProducer<>(props)) {

            //3.创建消息
            //key:作⽤是决定了往哪个分区上发,value:具体要发送的消息内容
            ProducerRecord<String,String> producerRecord = new ProducerRecord<>
                    (TOPIC_NAME,"2-3c","hellokafka");
            //4.发送消息,得到消息发送的元数据并输出
            RecordMetadata metadata = null;
            metadata = producer.send(producerRecord).get();
            System.out.println("同步⽅式发送消息结果:" + "topic-" +
                    metadata.topic() + "|partition-"+ metadata.partition() + "|offset-" + metadata.offset());

        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }

    }
}

总结

1. 对Producer 进行配置(地址字符串序列设置)

2. 将发送的数据存储在ProducerRecord (发送给哪个主题 Key Value)

3. 调用 producer.send(producerRecord) 对信息进行发送

4. 获取RecordMetadata 反馈消息

异步发送消息

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

ack设置

ack = 0 kafka-cluster不需要任何的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)

消费者自动提交和手动提交

1)提交的内容 消费者⽆论是⾃动提交还是⼿动提交,都需要把所属的消费组+消费的某个主题+消费的某个 分区及消费的偏移量,这样的信息提交到集群的_consumer_offsets主题⾥⾯。

2)⾃动提交

消费者poll消息下来以后就会⾃动提交offset

// 是否⾃动提交offset,默认就是true props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");

// ⾃动提交offset的间隔时间 props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");

自动提交间隔越短丢失的消息越少

3)⼿动提交 需要把⾃动提交的配置改成false

props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");

手动提交分为异步提交和同步提交

手动提交

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();//=======阻塞=== 提交成功
 }
 }
 }

异步提交

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

主要的差别

if (records.count() > 0) {//有消息 // ⼿动同步提交offset,当前线程会阻塞直到offset提交成功 // ⼀般使⽤同步提交,因为提交之后⼀般也没有什么逻辑代码了 consumer.commitSync();//=======阻塞=== 提交成功 }

if (records.count() > 0) { // ⼿动异步提交offset,当前线程提交offset不会阻塞,可以继续处理后⾯ 的程序逻辑 consumer.commitAsync(new OffsetCommitCallback() { @Override public void onComplete(Map offsets, Exception exception) { if (exception != null) { System.err.println("Commit failed for " + offsets); System.err.println("Commit failed exception: " + exception.getStackTrace()); } 

consumer.commitSync() 是否传入OffsetCommitCallback回调函数

OffsetCommitCallback回调函数重写了onComplete代码

如果传入了回调函数,就异步的执行回调函数中的代码

长轮询Poll

默认情况下,消费者⼀次会poll500条消息。

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

//通过向poll函数传入Duration.ofMillis(1000)表名如果不到500条消息,就等待1000毫秒

ConsumerRecords records = consumer.poll(Duration.ofMillis(1000));

如果两次poll的间隔超过30s,集群会认为该消费者的消费能⼒过弱,该消费者被踢出消 费组,触发rebalance机制,rebalance机制会造成性能开销。可以通过设置这个参数, 让⼀次poll的消息条数少⼀点

可以把这个MAX_POLL_RECORDS_CONFIG变小一点,这样就不会发生poll了无数次

指定分区和偏移量、时间消费

指定分区进行消费

consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));

//上面这个语句不能和下面这个语句同时出现

//consumer.subscribe(Collections.singletonList(TOPIC_NAME));

从头消费

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

凡是跟offset相关的 都使用seek命令对offset进行指定

新消费组的消费offset规则

如果消费者是新创建的,他想对原来的消息队列进行消费,但是原来的消息队列的offset不存在,他就不会消费消息

这个时候可以通过设置使消费者消费之前的消息

Latest:   默认的,消费新消息

earliest:第⼀次从头开始消费。之后开始消费新消息(最后消费的位置的偏移量+1)

props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

SpringBoot 中使用Kafka

引入依赖

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>

  <groupId>com.qf</groupId>

  <artifactId>kafka-spring-boot-demo</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <name>kafka-spring-boot-demo</name>

  <description>Demo project for Spring Boot</description>

  <properties>

    <java.version>1.8</java.version>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

    <spring-boot.version>2.3.7.RELEASE</spring-boot.version>

  </properties>

  <dependencies>

    <dependency>

      <groupId>org.springframework.kafka</groupId>

      <artifactId>spring-kafka</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>

      <exclusions>

        <exclusion>

          <groupId>org.junit.vintage</groupId>

          <artifactId>junit-vintage-engine</artifactId>

        </exclusion>

      </exclusions>

    </dependency>

  </dependencies>

  <dependencyManagement>

    <dependencies>

      <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-dependencies</artifactId>

        <version>${spring-boot.version}</version>

        <type>pom</type>

        <scope>import</scope>

      </dependency>

    </dependencies>

  </dependencyManagement>

  <build>

    <plugins>

      <plugin>

        <groupId>org.apache.maven.plugins</groupId>

        <artifactId>maven-compiler-plugin</artifactId>

        <version>3.8.1</version>

        <configuration>

          <source>1.8</source>

          <target>1.8</target>

          <encoding>UTF-8</encoding>

        </configuration>

      </plugin>

      <plugin>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-maven-plugin</artifactId>

        <version>2.3.7.RELEASE</version>

        <configuration>

          <mainClass>

                com.qf.kafka.spring.boot.demo.KafkaSpringBootDemoApplication

          </mainClass>

        </configuration>

        <executions>

          <execution>

            <id>repackage</id>

            <goals>

              <goal>repackage</goal>

            </goals>

          </execution>

        </executions>

      </plugin>

    </plugins>

  </build>

</project>

写配置文件

server:
port: 8080

spring:
kafka:
bootstrap-servers: 192.168.232.121:9092,192.168.232.121:9093,192.168.232.121:9094
producer:
retries: 3
# 如果没有收到ACK就会进行重试,重使3次
batch-size: 16384
# 一次打包发送16k
buffer-memory: 33554432
# 缓冲池32M
acks: 1
# 多副本之间的leader已经收到消息,并把消息写⼊到本地的log中,才 会返回ack给⽣产者,性能和安全性是最均衡的
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
# 一次最多拉500条数据
listener:
ack-mode: manual_immediate


redis:
host: 192.168.232.121:6379

# 当每⼀批poll()的数据被消费者监听器(ListenerConsumer)处理之后, ⼿动调 ⽤Acknowledgment.acknowledge()后提交 # MANUAL # ⼿动调⽤Acknowledgment.acknowledge()后⽴即提交,⼀般使⽤这种 # MANUAL_IMMEDIATE

kafka集群中的controller、 rebalance、HW

集群中谁来充当controller 每个broker启动时会向zk创建⼀个临时序号节点,获得的序号最⼩的那个broker将会作为集 群中的controller,负责这么⼏件事:

当集群中有⼀个副本的leader挂掉,需要在集群中选举出⼀个新的leader,选举的规则是 从isr集合中最左边获得。

当集群中有broker新增或减少,controller会同步信息给其他broker

当集群中有分区新增或减少,controller会同步信息给其他broker

总结 同步信息,进行选举

rebalance机制

分区分配的策略:在rebalance之前,分区怎么分配会有这么三种策略

range:根据公示计算得到每个消费消费哪⼏个分区:前⾯的消费者是分区总数/消费 者数量+1,之后的消费者是分区总数/消费者数量

轮询:⼤家轮着来

sticky:粘合策略,如果需要rebalance,会在之前已分配的基础上调整,不会改变之 前的分配情况。如果这个策略没有开,那么就要进⾏全部的重新分配。建议开启。

第一个range

假如说有10个分区 3个消费者 就按 4 3 3这么分配

第二个轮询

挨个消费者分配分区

第三个粘合

其实这个不算策略,这个只算是消费者组发生变化的时候,基于当前状态 是否重新分配消费者分区

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值