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 中的分区分配是动态的,但分配策略的变化不会发生在每次接收到新消息时。一旦分配给消费者的分区被确定,通常会保持不变一段时间,以确保消息的有序消费。但在一些情况下,分区分配可能会发生变化:
-
消费者加入或离开: 当消费者加入消费者组或离开消费者组时,分区可能会重新分配。这确保了分区在消费者组内的平衡负载。
-
新的主题:有新的主题被创建,重新平衡机制可能会触发,导致分区重新分配。
-
手动分配: 您也可以通过手动指定分区分配来更改消费者的分区分配,但这需要谨慎使用,以避免引入不必要的复杂性。
在您提到的情况中,即分配给消费者 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这么分配
第二个轮询
挨个消费者分配分区
第三个粘合
其实这个不算策略,这个只算是消费者组发生变化的时候,基于当前状态 是否重新分配消费者分区