kafka学习笔记
kafka:
定义:是一个分布式基于的基于发布/订阅模式的消息队列,主要用于大数据数据的实时处理
使用消息队列的好处:
1.解耦
允许你独立的扩展或者修改两边的处理过程,只要确保它们遵守同样的接口约束。
2.可恢复性
系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
3.缓冲
有助于控制和优化数据流经过系统的速度, 解决生产消息和消费消息的处理速度不一致的情况。
4.灵活性 & 峰值处理能力(削峰)
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
5.异步通信
很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
消息队列的两种方式:
点对点的消费-一对一的进行消费,消费者消费后消息就被清除了
发布订阅模式-一对多,消费者消费消息之后,消息不会被清除
基础架构
Producer : 消息生产者,就是向 Kafka ;
Consumer : 消息消费者,向 Kafka broker 取消息的客户端;
Consumer Group (CG): 消费者组,由多个 consumer 组成。 消费者组内每个消费者负责
消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。 所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
Broker :经纪人 一台 Kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个 broker可以容纳多个 topic。
Topic : 话题,可以理解为一个队列, 生产者和消费者面向的都是一个 topic;
Partition: 为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列;
Replica: 副本(Replication),为保证集群中的某个节点发生故障时, 该节点上的 partition 数据不丢失,且 Kafka仍然能够继续工作, Kafka 提供了副本机制,一个 topic 的每个分区都有若干个副本,一个 leader 和若干个 follower。
Leader: 每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 leader。
Follower: 每个分区多个副本中的“从”,实时从 leader 中同步数据,保持和 leader 数据的同步。 leader 发生故障时,某个 Follower 会成为新的 leader。
运行zookeeper
因现有新版的kafka内置了zk 所以我目前下载的不需要下载zk 直接运行就可以。
cd到解压的Kafka的文件位置。我的是E:\kafka_2.12-2.2.0\kafka_2.12-2.2.0 执行下面的命令
bin\windows\zookeeper-server-start.bat config\zookeeper.properties
运行结果:
修改配置文件
1.进入E:\kafka_2.12-2.2.0\kafka_2.12-2.2.0\config目录下,打开server.properties,吧listeners注释打开,地址换成本地
2.修改log地址
运行kafka(9092)
cd到解压的Kafka的文件位置。我的是D:\Documents\Downloads\kafka_2.12-2.2.0 执行下面的命令
bin\windows\kafka-server-start.bat config\server.properties
运行结果:
问题
打开kafka服务的时候报错连接到zookeeper超时,看一看端口占用情况,发现打开zookeeper的命令行窗口一关闭,zookeeper服务就不占用这个端口了。所以不关闭打开zookeeper服务和kafka服务的命令行端口。可以成功使用。但是不知道为何启动信息都不太正常,会卡在正常信息的某个部位。但是还是能够正常使用。
简单使用
创建主题topic,topic = demo
bin\windows\kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 -partitions 1 -topic demo
查看创建的topic
bin\windows\kafka-topics.bat --list --zookeeper localhost:2181
启动生产者 producer
bin\windows\kafka-console-producer.bat --broker-list localhost:9092 --topic demo
启动生产者之后进入编辑页面,发送了 "nihao",
启动消费者 customer
消费所有消息
bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic demo --from-beginning
消费从最后一条消息的偏移量+1的地方开始读取消息
bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic demo
当消费者启动之后,收到了之前生产者发送的 “nihao”。
关于消息的细节
生产者将消息发送给broker,broker会将消息保存在本地的日志文件中,日志文件的路径就是我们之前在配置文件中配置的路径
E:\\kafka_2.12-2.2.0\\kafka_2.12-2.2.0\\kafka-logs\\主题+分区\\000000.log
消息的保存时有序的,通过offset偏移量来描述消息的有序性。
消费者在消费消息时也是通过offset来确定读取消息的位置。
单播消息
在kafka中启动两个消费者,一个生产者,那么一个生产者产出的消息会被两个消费者同时消费么?
如果多个消费者在同一个组里那么,只有一个可以收到订阅的topic中的消息。
bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092 --consumer-property group.id=testGroup --topic demo
多播消息
就是一个生产者,两个不同组中的消费者订阅同一个topic,都可以收到订阅的消息。
bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092 --consumer-property group.id=testGroup --topic demo
bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092 --consumer-property group.id=testGroup2 --topic demo
查看消费组的详细信息
#查看当前主题下有哪些分组
bin\windows\kafka-consumer-groups.bat --bootstrap-server localhost:9092 --list
#查看消费组中的具体信息:比如当前偏移量、最后一条消息的偏移量、堆积的消息数量
bin\windows\kafka-consumer-groups.bat --bootstrap-server localhost:9092 --describe --group testGroup
组名 主题 分区 已消费 共多少 未消费
主题和分区的概念
主题
主题-topic在kafka中是-个逻辑的概念,kafka通过topic将消 息进行分类。不同的topic会被订阅该topic的消费者消费。
但是有一个问题,如果说这个topic中的消息非常非常多,多到需要几T来存,因为消息是会被保存到log日志文件中的。为了解决这个文件过大的问题,kafka提 出了Partition分区的概念
分区(Partition)
消息发送时都被发送到一个topic,其本质就是一个目录,而topic是由一些Partition Logs(分区日志)组成,其组织结构如下图所示:
我们可以看到,每个Partition中的消息都是有序的,生产的消息被不断追加到Partition log上,其中的每一个消息都被赋予了一个唯一的offset值。
1)分区的原因
(1)方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了;
(2)可以提高并发,因为可以以Partition为单位读写了。
2)分区的原则
(1)指定了patition,则直接使用;
(2)未指定patition但指定key,通过对key的value进行hash出一个patition;
(3)patition和key都未指定,使用轮询选出一个patition。
#创建主题分区partitions
bin\windows\kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 -partitions 2 -topic test1
kafka中日志文件中保存的内容
●00000.log: 这个文件中保存的就是消息
●_ consumer_ _offsets-49:
kafka内部自己创建了_ _consumer_ offsets主题包含了50个分区。这个主题用来存放消费者消费某个主题的偏移量。因为每个消费者
都会自己维护着消费的主题的偏移量,也就是说每个消费者会把消费的主题的偏移量自主上报给kafka中的默认主题:
consumer_ offsets。 因此kafka为 了提升这个主题的并发性,默认设置了50个分区。
。提交到哪个分区:通过hash函数: hash(consumerGroupld) %_ consumer_ offsets主 题的分区数
。提交到该主题中的内容是: key是consumerGroupld+topic+分区号,value就是当前offset的值
●文件中保存的消息,默认保存7天。七天到后消息会被删除。
查看topic详情
bin\windows\kafka-topics.bat --describe --zookeeper localhost:2181 --topic my-replicated-topic
创建多个副本
#创建三个副本、两个分区、一个主题
bin\windows\kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 3 -partitions 2 -topic test1
副本的概念:
同一个partition可能会有多个replication(对应 server.properties 配置中的 default.replication.factor=N)。没有replication的情况下,一旦broker 宕机,其上所有 patition 的数据都不可被消费,同时producer也不能再将数据存于其上的patition。引入replication之后,同一个partition可能会有多个replication,而这时需要在这些replication之间选出一个leader,producer和consumer只与这个leader交互,其它replication作为follower从leader 中复制数据。
通过查看主题信息,其中的关键数据: .
replicas:
当前副本存在的broker节点
leader: 副本里的概念
每个partition都有一个broker作为leader。
消息发送方要把消息发给哪个broker?就看副本的leader是在哪个broker上面。副本里的leader专门用来接收消息。接收到消息,其他follower通过poll的方式来同步数据。
follower:
leader处理所有针对这个partition的读写请求,而follower被动复制leader,不提供读写(主要是为了保证多副本数据与消费的一致性),如果leader所在的broker挂掉,那么就会进行新leader的选举。
isr:
可以同步和已同步的节点会被存入到isr集合中。这里有一个细节: 如果isr中的节点性能较差,会被提出isr集合。
(重点~! )此时,broker. 主题、分区、副本这些概念就全部展现了,需要把这些概念梳理清楚:
集群中有多个broker,创建主题时可以指明主题有多个分区(把消息拆分到不同的分区中存储),可以为分区创建多个副本,不同的副本存放在不同的broker里。
关于分区消费组消费者的细节
图中Kafka集群有两个broker,每个broker中有多个partition。一个partition只能被 一个消费组里的某一个消费者消费,从而保证消费顺序。Kafka只在partition的范围内保证消息消费的局部顺序性,不能在同一个topic中的多个partition中保证总的消费顺序性。一个消费者可以消费多个partition。
消费组中消费者的数量不能比一个topic中的partition数量多, 否则多出来的消费者消费不到消息。
关于集群消费
●向集群发送消息:
. /kafka-console-consumer .sh --bootstrap-server 172.16.253.38:9092,172. 16.253.38:9093,172.16.253.38:9094 --from- beginning --consumer -property group. id=testGroupl --topic my-replicated-topic
●从集群中消费消息
./kafka-console-producer.sh --broker-list 172.16.253.38:9092,172.16.253.38:9093, 172.16.253.38:9094 --topic my-replicated-topic
●指定消费组来消费消息
./kafka-console-consumer.sh --bootstrap-server 172.16.253.38:9092, 172.16.253.38:9093, 172.16.253.38:9094 --from beginning --consumer -property group. id=testGroupl --topic my-replicated-topic
分区分消费组的集群消费中的细节
1.一个partition只能被 一个消费组中的一个消费者消费,目的是为了保证消费的顺序性,但是多个partion的多个消费者消费的总的顺序性是得不到保证的,那怎么做到消费的总顺序性呢?
2.partition的数量决定了消费组中消费者的数量,建议同一个消费组中消费者的数量不要超过partition的数量, 否则多的消费者消费不到消息
3.如果消费者挂了,那么会触发rebalance机制 (后面介绍),会让其他消费者来消费该分区
java代码进行kafka操作
引入依赖
(需要与自己安装kafka版本相同)
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.2.0</version>
</dependency>
简单测试小发送功能
package com.baidu.java.controller;
import com.alibaba.fastjson.JSON;
import com.baidu.java.entity.Order;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class MySimpleProducer {
private final static String TOPIC_NAME = "demo";
public static void main(String[] args) throws ExecutionException, InterruptedException {
//设置参数
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());
//发消息的客户端
//创建生产消息 并传入参数
Producer<String, String> producer = new KafkaProducer<String, String>(props);
//创建消息
//key的作用是决定了向那个分区上发送 value 是具体要发送的消息内容
ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME,"mykey","hellokafka");
//等待消息发送成功的同步阻塞方法
RecordMetadata metadata = producer. send( producerRecord).get();
//==== =阻塞====== =
System. out. println("同步方式发送消息结果:"+ "topic-" + metadata.topic() +"| partition-"
+ metadata.partition() + "|offset-" + metadata.offset());
}
}
发送消息的同步发送和异步发送
生产者同步发送消息
// RecordMetadata metadata = producer.send( producerRecord2).get();
//==== =阻塞====== =
// System. out. println("同步方式发送消息结果:"+ "topic-" + metadata.topic() +"| partition-"
// + metadata.partition() + "|offset-" + metadata.offset());
生产者异步发送消息
producer.send(producerRecord, new Callback() {
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null){
System.out.println("消息发送失败"+e.getStackTrace());
}
if (recordMetadata != null){
System. out. println("异步方式发送消息结果:"+ "topic-" + recordMetadata.topic() +"| partition-"
+ recordMetadata.partition() + "|offset-" + recordMetadata.offset());
}
}
});
生产者中的ack的配置
在同步发送的前提下,生产者在获得集群返回的ack之前会-直阻塞。 那么集群什么时候返回ack呢?此时ack有3个配置:
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已完成数据的接收),这种方式最安全,但性能最差。
关于配置ack和重试的(如果没有收到ack就会进行重试)配置
//设置ack 的值
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客户端消费者的实现细节
消费者的基本实现
package com.baidu.java.controller;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
public class MySimpleConsumer {
private final static String TOPIC_NAME = "test1";
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());
//创建一个消费者的客户端
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
//消费者订阅主题列表
consumer.subscribe(Arrays.asList(TOPIC_NAME));
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());
}
}
}
}
消费者offset自动提交和手动提交
提交的内容
消费者无论是自动提交还是手动提交,都需要把所属的消费组+消费的某个主题+消费的某个分区及消费的偏移量,这样的信息提交到集群的——consumer——offsets主题里面。
自动提交
消费者poll下来之后,就会自动提交offset。
缺点 与ack 差不多一思 消息poll下来后,有可能还没有消费消息 就挂掉了,但是自动提交已经将偏移量提交了,造成了数据丢失。
//是否是自动提交offset 默认是true
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");
//自动提交offset的时间间隔
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");
手动提交分为两种
手动同步提交
在消费完消息后调用同步提交的方法,当集群返回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.commitAsync();//=====阻塞==== 提交成功
}
}
异步提交
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){//有消息
//手动异步提交
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception e) {
if (e != null){
System.out.println("commit failed for "+ offsets);
System.out.println("commit failed exception "+ e.getStackTrace());
}
}
});
}
}
长轮询poll消息
默认情况下,消费者一次会poll500条消息。
//一次poll最大拉取的消息条数,可以根据消费速度的快慢来进行设置
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG,"500");
代码中设置的长轮询的时间间隔是1000毫秒
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();//=====阻塞==== 提交成功
}
}
这就意味着:
如果一次poll到500条,就直接执行for循环
如果这一次没有poll到500条。 且时间在1秒内,那么长轮询继续poll, 要么到500条, 要么到1s
如果多次poll都没达到500条,且1秒时间到了,那么直接执行for循环
如果两次pol的间隔超过30s,集群会认为该消费者的消费能力过弱,该消费者被踢出消费组,触发rebalance机制, rebalance机制会造成性能开销。可以通过设置这个参数,让一次poll的消息条数少一点
//一次poll最大拉取的消息条数,可以根据消费速度的快慢来进行设置
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG,"500");
//如果两次pol的间隔超过30s,集群会认为该消费者的消费能力过弱,该消费者被踢出消费组,触发rebalance机制
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG,30 * 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消费
//指定offset消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));
consumer.seek(new TopicPartition(TOPIC_NAME,0),10);
指定消费某个时间段内的数据
根据时间,去所有的partition中确定该事件对应的offset,然后去所有的partition中找到该offset之后的消息开始消费。
//从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() + "loffset-" + offset);
System.out.println();
//根据消费里的times tamp确定offset
if (value != null) {
consumer.assign(Arrays.asList(key));
consumer.seek(key, offset);
}
}
新消费组的消费偏移量
当消费主题的是⼀个新的消费组,或者指定offset的消费⽅式,offset不存在,那么应该如何 消费?
latest(默认) :只消费自己启动之后发送到主题的消息
earliest:第⼀次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer.seekToBeginning(每次都从头开始消费)
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
Springboot中使⽤Kafka
引⼊依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
编写配置⽂件
server:
#端口号
port: 18080
spring:
kafka:
bootstrap-servers: 127.0.0.1:9092
producer: # ⽣产者
retries: 3 # 设置⼤于0的值,则客户端会将发送失败的记录重新发送
batch-size: 16384
buffer-memory: 33554432
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
redis:
host: 127.0.0.1
编写消息⽣产者
package com.baidu.java.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @author ZhangMingYang
* @version 1.0.0
* @description MyKafkaController
* @since 2021/10/14 10:19
*/
@RestController
@RequestMapping("/msg")
public class MyKafkaController {
private final static String TOPIC_NAME = "test1";
@Resource
private KafkaTemplate<String,String> kafkaTemplate;
@RequestMapping("send")
public String sendMessage (){
kafkaTemplate.send(TOPIC_NAME,0,"my","my-kafka");
return "send ok";
}
}
编写消费者
package com.baidu.java.controller;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
/**
* @author ZhangMingYang
* @version 1.0.0
* @description MyKafkaConsumer
* @since 2021/10/16 17:27
*/
@Component
public class MyKafkaConsumer {
@KafkaListener(topics = "test1")
public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
String value = record.value();
System.out.println(value);
System.out.println(record);
//手动提交offset
ack.acknowledge();
}
}
消费者中配置消费主题、分区和偏移量
@KafkaListener(groupId = "testGroup", topicPartitions = {
@TopicPartition(topic = "topic1", partitions = {"0", "1"}),
@TopicPartition(topic = "topic2", partitions = "0",
partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))
},concurrency = "3")//concurrency就是同组下的消费者个数,就是并发消费数,建议小于等于分区总数,我的jar包是2.1.7的所有没有这个,视频中2.5以上的
public void listenGroupPro(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
1.controller
集群中谁来充当controller ?
每个broker启动时会向zk创建⼀个临时序号节点,获得的序号最⼩的那个 broker将会作为集
群中的controller,负责这么几件事:
当集群中有⼀个副本的leader挂掉,需要在集群中选举出⼀个新的leader,选举的规则是从isr集合中最左边获得。
当集群中有broker新增或减少,controller会同步信息给其他broker
当集群中有分区新增或减少,controller会同步信息给其他broker
2.rebalance机制
前提:消费组中的消费者没有指明分区来消费
触发的条件:当消费组中的消费者和分区的关系发⽣变化的时候
分区分配的策略:在rebalance之前,分区怎么分配会有这么三种策略
range:根据公示计算得到每个消费消费哪⼏个分区:前⾯的消费者是分区总数/消费者数量+1,之后的消费者是分区总数/消费者数量
轮询:⼤家轮着来
sticky:粘合策略,如果需要rebalance,会在之前已分配的基础上调整,不会改变之前的分配情况。如果这个策略没有开,那么就要进⾏全部的重新分配。建议开启。
HW和LEO
LEO是某个副本最后消息的消息位置(log-end-offset)
HW是已完成同步的位置。消息在写⼊broker时,且每个broker完成这条消息的同步后,hw才会变化。在这之前消费者是消费不到这条消息的。在同步完成之后,HW更新之后,消费者才能消费到这条消息,这样的⽬的是防⽌消息的丢失。
Kafka中的优化问题
如何防⽌消息丢失
⽣产者:1)使⽤同步发送 2)把ack设成1或者all,并且设置同步的分区数>=2
消费者:把⾃动提交改成⼿动提交
如何防止重复消费
在防⽌消息丢失的⽅案中,如果⽣产者发送完消息后,因为⽹络抖动,没有收到ack,但实际上broker已经收到了。
此时⽣产者会进⾏重试,于是broker就会收到多条相同的消息,⽽造成消费者的重复消费。
怎么解决:
⽣产者关闭重试:会造成丢消息(不建议)
消费者解决⾮幂等性消费问题:
所谓的幂等性:多次访问的结果是⼀样的。对于rest的请求(get(幂等)、post(非幂等)、put(幂等)、delete(幂等))
解决⽅案:
在数据库中创建联合主键,防⽌相同的主键 创建出多条记录
使⽤分布式锁,以业务id为锁。保证只有⼀条记录能够创建成功
如何做到消息的顺序消费
生产者:保证消息按顺序消费,且消息不丢失——使⽤同步的发送,ack设置成非0的值。
消费者:主题只能设置⼀个分区,消费组中只能有⼀个消费者
kafka的顺序消费使用场景不多,因为牺牲掉了性能,但是比如rocketmq在这⼀块有专⻔的功能已设计好。
如何解决消息积压问题
1)消息积压问题的出现
消息的消费者的消费速度远赶不上⽣产者的⽣产消息的速度,导致kafka中有⼤量的数据没有被消费。随着没有被消费的数据堆积越多,消费者寻址的性能会越来越差,最后导致整个kafka对外提供的服务的性能很差,从⽽造成其他服务也访问速度变慢,造成服务雪崩。
2)消息积压的解决⽅案
在这个消费者中,使⽤多线程,充分利⽤机器的性能进⾏消费消息。
通过业务的架构设计,提升业务层⾯消费的性能。
创建多个消费组,多个消费者,部署到其他机器上,⼀起消费,提⾼消费者的消费速度
创建⼀个消费者,该消费者在kafka另建⼀个主题,配上多个分区,多个分区再配上多个
消费者。该消费者将poll下来的消息,不进⾏消费,直接转发到新建的主题上。此时,新 的主题的多个分区的多个消费者就开始⼀起消费了。——不常⽤
实现延时队列的效果
应用场景
订单创建后,超过30分钟没有⽀付,则需要取消订单,这种场景可以通过延时队列来实现
具体方案
kafka中创建创建相应的主题
消费者消费该主题的消息(轮询)
消费者消费消息时判断消息的创建时间和当前时间是否超过30分钟(前提是订单没⽀
付)
如果是:去数据库中修改订单状态为已取消
如果否:记录当前消息的offset,并不再继续消费之后的消息。等待1分钟后,再次
向kafka拉取该offset及之后的消息,继续进⾏判断,以此反复。