1、发布订阅系统
1.1、发布订阅系统简介
生产者产生消息,发送给消息中转站,在Kafka中名为Broker,消费者消费消息,定时从Broker中拉取消息。生产者以主题为分类发送消息,消费者以主题为分类订阅消息。
1.2、Kafka名词解释
- 生产者(Producer):生产消息的服务
- 消费者(Consumer):消费消息的服务,在Kafka中消费者是通过主动拉取的方式去Broker中拉取消息,而像RabbitMQ则是MQ向消费者推送消息。
- 主题(Topic):Kafka的消息通过主题分类。主题内可以包含多个分区。主题内的消息不保证有序,分区内的消息保证有序。
- 分区(Partitions):一个主题可以划分为多个分区,分区可以分布在不同的Broker上,也就是一个主题的消息可以分布在不同的服务器上。在创建主题时可以指定分区数,默认为1个分区,分区可以在主题创建之后更改,但是只能往大改,不能改小。如下图所示。
- Broker:组成集群的最小单位,一个独立的Kafka服务就被称为一个Broker。用来接收生产者发来的消息,设置偏移量,并持久化到磁盘;为消费者提供拉取消息的服务。
- 集群:多个Broker组成的一个高可用的Kafka服务。每个Kafka集群中都有一个Broker作为集群控制器的角色(自动从活跃的的Broker成员中选举出来)。控制器负责管理工作,包括将分区分配给broker和监控broker。
1.3、分区和Broker的关联,分区复制
分区从属于一个Broker,该Broker属于该分区的首领。当一个分区分配给多个Broker时,会发生分区复制。这种复制机制保证了分区的数据在多个Broker之间有冗余。当该分区的首领Broker失效之后,其他Broker会从拥有该分区的Broker列表选举出新的Broker首领。
2、Kafka服务基本使用
linux环境启动Zookeeper和Kafka
从官网下载之后,配置好Java环境,无需修改配置,可以直接启动。
位于Kafka目录下,启动Zookeeper
./bin/zookeeper-server-start.sh -daemon ./config/zookeeper.properties
位于Kafka目录下,启动Kafka
./bin/kafka-server-start.sh -daemon ./config/server.properties
操作topic
# 查询当前broker已创建的topic
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
# 创建主题,并指定分区数,如不指定分区数则默认一个分区
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --topic 主题名 --create --partitions 分区数
# 修改主题分区数,分区数只能变大,不能变小
./bin/\kafka-topics.sh --bootstrap-server localhost:9092 --topic 主题名 --alter --partitions 分区数
# 删除分区
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --topic 主题名 --delete
启动消费者从主题中拉取消息
# 启动消费者从主题中拉取消息
./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic 主题名
使用生产者发送消息到指定队列
# 启动生产者在命令行发送消息
./bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic 主题名
3、整合SpringBoot
创建普通SpringBoot项目,引入如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
配置application.yaml
spring:
kafka:
bootstrap-servers: localhost:9092
4、生产者
4.1、生产者快速入门
@RestController
public class TestController {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@PostMapping("/test")
public void test() {
kafkaTemplate.send("test", "first msg");
}
}
SpringBoot
帮我们创建了KafkaTemplate
,我们直接注入即可使用,KafkaTemplate
两个泛型类型分别对应的是消息Key和Value所对应的泛型,Key和Value如果不是String类型时,我们需要自定义Key和Value的序列化器,后文有讲。kafkaTemplate.send
有很多重载方法,这里使用最简单的方法,arg0为主题,arg1为消息内容。有其他的重载方法可以指定key,指定分区,指定时间戳等。
**注意:**在生产者发送消息到主题之前,我们最好
自己先创建主题,可以使用命令行方式创建主题,也可以使用第三方kafkaWeb页面创建。当然,如果我们之前没有改动过Kafka的auto.create.topics.enable
的默认配置,默认为true,则意味着,如果生产者将消息发送给一个不存在的主题时,该主题将自动创建。
默认情况下, Kafka 会在如下几种情形下自动创建主题:
- 当一个生产者开始往主题写入消息时;
- 当一个消费者开始从主题读取消息时;
- 当任意一个客户端向主题发送元数据请求时 。
很多时候,这些行为都是非预期的。而且,根据 Kafka 协议,如果一个主题不先被创建,根本无法知道它是否已经存在。如果显式地创建主题,不管是手动创建还是通过其他配置系统来创建,都可以把auto.create.topics.enable
设为 false。
4.2、序列化器和自定义序列化器
SpringBoot
和Kafka帮我们提供了很多序列化器和反序列化器。默认帮我们配置的是StringSerializer
,可以序列化和反序列化String类型的Key和Value。如果我们发送的消息内容格式不是字符串,想自定义实体对象,我们可以自定义序列化器,当然生产者定义了序列化器,消费者反之要定义反序列化器,用来解析消息。在Kafka消息中,Key和Value可以配置不同的序列化器,下面我们来定义KeySerializer
和ValueSerializer
。
先定义一个实体对象,用来做序列化
@Data
public class User {
private String username;
private String password;
private Integer age;
}
定义消息的KeySerializer,这里我将Integer类型转为了String类型,然后转为了字节数组。一般情况下,key都是字符串,也不需要序列化,这里仅为演示Key的自定义序列化。
import org.apache.kafka.common.serialization.Serializer;
import java.nio.charset.StandardCharsets;
/**
* @author Xinxin.Wang
* @since 2023/1/5 14:07
*/
public class KeySerializer implements Serializer<Integer> {
@Override
public byte[] serialize(String topic, Integer data) {
return String.valueOf(data).getBytes(StandardCharsets.UTF_8);
}
}
定义消息的ValueSerializer,这里我还是将User类型的对象序列化为JSON字符串,当然也可以直接使用SpringBoot提供的org.springframework.kafka.support.serializer.JsonSerializer<T>
。
import com.alibaba.fastjson.JSON;
import org.apache.kafka.common.serialization.Serializer;
import org.wxx.testkafka.domain.User;
import java.nio.charset.StandardCharsets;
/**
* @author Xinxin.Wang
* @since 2023/1/5 14:08
*/
public class ValueSerializer implements Serializer<User> {
@Override
public byte[] serialize(String topic, User data) {
return JSON.toJSONString(data).getBytes(StandardCharsets.UTF_8);
}
}
定义完KeySerializer和ValueSerializer也需要配置一下这两个序列化器,yaml配置文件如下。
spring:
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: 具体包名.KeySerializer
value-serializer: 具体包名.ValueSerializer
我们发送消息的代码可以改造为如下。
@Autowired
private KafkaTemplate<Integer, User> kafkaTemplate;
@PostMapping("/testSerializer")
public void testSerializer() {
user.setUsername("test");
user.setPassword("testPwd");
user.setAge(18);
kafkaTemplate.send("test", 0, 1000L, 1, user);
}
如果读者之前启动了消费者并配置的是test主题,则可以看到如下输出。
可以看到图片,控制台启动的消费者收到了序列化为JSON字符串的数据。
4.3、分区器和自定义分区器
Kafka 的消息是一个个键值对, 发送消息时可以只包含目标主题和值,键可以设置为默认的 null,不过大多数应用程序会用到键。键有两个用途:可以作为消息的附加信息;可以用来决定消息该被写到主题的哪个分区。拥有相同键的消息将被写到同一个分区。也就是说,如果一个进程只从一个主题的分区读取数据,那么具有相同键的所有记录都会被该进程读取。
如果键值为 null,并且使用了默认的分区器,那么记录将被随机地发送到主题内各个可用的分区上。分区器使用轮询(Round Robin)算法将消息均衡地分布到各个分区上。
SpringBoot默认配置的Kafka的默认分区处理类为:org.apache.kafka.clients.producer.internals.DefaultPartitioner
,该类的分区策略为:
-
如果消息指定了分区,则使用消息指定的分区
-
如果消息没指定分区,但是有key值,则根据key值的hash值取模分区大小来选择分区
-
如果即没有指定分区,也没有key值,则使用粘性分区策略,即,首先会随机选择一个分区,并且在一定时间内/一定消息量之内消息分区不会改变。
自定义分区器
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import java.util.List;
import java.util.Map;
/**
* @author Xinxin.Wang
* @since 2023/1/5 15:08
*/
public class CustomPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 首先获取改主题有多少个分区
List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
// 分区数
int numPartitions = partitionInfos.size();
// key 如果没有那么多分区,则消息放到最后一个分区
// 0-999 0号分区
// 1000-1999 1号分区
// 2000-2999 2号分区
// ...
Integer keyInt = (Integer) key;
int i = keyInt / 1000;
// 如果有对应分区,则放入对应分区中,如果没有则放到最后一个分区。
return Math.min(numPartitions - 1, i);
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
yaml文件配置自定义分区器。
spring:
kafka:
bootstrap-servers: localhost:9092
producer:
properties:
partitioner:
class: 具体包名.CustomPartitioner
改造生产者代码。
@RestController
@Slf4j
public class TestController {
@Autowired
private KafkaTemplate<Integer, User> kafkaTemplate;
@PostMapping("/test/{key}")
public void testPartition(@PathVariable Integer key) {
User user = new User();
user.setUsername("test");
user.setPassword("testPwd");
user.setAge(18);
kafkaTemplate.send("test", key, user).completable()
.whenComplete((result, throwable) -> {
RecordMetadata recordMetadata = result.getRecordMetadata();
int partition = recordMetadata.partition();
log.info("key:{}, partition:{}", key, partition);
});
}
}
这里我们使用send发送成功之后的回调方法打印发送消息发送到那个分区了。我现在给test分区创建了3个分区。
POST http://localhost:8080/test/1
POST http://localhost:8080/test/1000
POST http://localhost:8080/test/2000
POST http://localhost:8080/test/3000
POST http://localhost:8080/test/4000
可以看到打印结果,分区执行了我们自定义的分区策略。
4.4、生产者配置spring.kafka.producer
这里列举一些基本的生产者配置,作者未测试配置的相关作用,其解释来源于官网或文档直译。
- acks:acks参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功的。这个参数对消息丢失的可能性有重要影响。该参数有如下选项。
- 如果acks=0,生产者在成功写入消息之前不会等待任何来自服务器的响应。也就是说,如果当中出现了问题,导致服务器没有收到消息,那么生产者就无从得知,消息也就丢失了。不过,因为生产者不需要等待服务器的响应,所以它可以以网络能够支持的最大速度发送消息,从而达到很高的吞吐量。
- 如果 acks=1,只要集群的首领节点收到消息,生产者就会收到一个来自服务器的成功响应。如果消息无法到达首领节点(比如首领节点崩溃,新的首领还没有被选举出来),生产者会收到一个错误响应,为了避免数据丢失,生产者会重发消息。不过,如果一个没有收到消息的节点成为新首领,消息还是会丢失。这个时候的吞吐量取决于使用的是同步发送还是异步发送。如果让发送客户端等待服务器的响应(通过调用 Future 对象的 get() 方法),显然会增加延迟(在网络上传输一个来回的延迟)。如果客户端使用回调,延迟问题就可以得到缓解,不过吞吐量还是会受发送中消息数量的限制(比如,生产者在收到服务器响应之前可以发送多少个消息)。
- 如果 acks=all或者acks=-1,只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。这种模式是最安全的,它可以保证不止一个服务器收到消息,就算有服务器发生崩溃,整个集群仍然可以运行。不过,它的延迟比 acks=1 时更高,因为我们要等待不只一个服务器节点接收消息。
- buffer-memory:该参数用来设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。如果应用程序发送消息的速度超过发送到服务器的速度,会导致生产者空间不足。这个时候,send() 方法调用要么被阻塞,要么抛出异常。单位:byte。1KB=1024
- compression-type:默认情况下,消息发送时不会被压缩,即为none。该参数可以设置为none、zstd、snappy、 gzip 或 lz4,它指定了消息被发送给 broker 之前使用哪一种压缩算法进行压缩。 snappy压缩算法由Google发明,它占用较少的CPU,却能提供较好的性能和相当可观的压缩比,如果比较关注性能和网络带宽,可以使用这种算法。 gzip 压缩算法一般会占用较多的 CPU,但会提供更高的压缩比,所以如果网络带宽比较有限,可以使用这种算法。使用压缩可以降低网络传输开销和存储开销,而这往往是向 Kafka 发送消息的瓶颈所在。
- retries:生产者从服务器收到的错误有可能是临时性的错误(比如分区找不到首领)。在这种情况下, retries 参数的值决定了生产者可以重发消息的次数,如果达到这个次数,生产者会放弃重试并返回错误。默认情况下,生产者会在每次重试之间等待100ms,不过可以通过
retry.backoff.ms
(SpringBoot需要通过生产者的properties来设置)参数来改变这个时间间隔。建议在设置重试次数和重试时间间隔之前,先测试一下恢复一个崩溃节点需要多少时间(比如所有分区选举出首领需要多长时间),让总的重试时间比Kafka集群从崩溃中恢复的时间长,否则生产者会过早地放弃重试。不过有些错误不是临时性错误,没办法通过重试来解决(比如“消息太大”错误)。一般情况下,因为生产者会自动进行重试,所以就没必要在代码逻辑里处理那些可重试的错误。你只需要处理那些不可重试的错误或重试次数超出上限的情况。 - batch-size:当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算(而不是消息个数)。当批次被填满,批次里的所有消息会被发送出去。不过生产者并不一定都会等到批次被填满才发送,半满的批次,甚至只包含一个消息的批次也有可能被发送。所以就算把批次大小设置得很大,也不会造成延迟,只是会占用更多的内存而已。但如果设置得太小,因为生产者需要更频繁地发送消息,会增加一些额外的开销。单位:byte。1KB=1024
- client-id:该参数可以是任意的字符串,服务器会用它来识别消息的来源,还可以用在日志和配额指标里。
- transaction-id-prefix:事务id的前缀。当配置了事务id的前缀,则必须配置acks为-1或者all。否则使用事务消息时会报错。
- key-serializer:消息key的序列化器,前面自定义序列化器小节已经使用过。
- value-serializer:消息value的序列化器,前面自定义序列化器小节已经使用过。
以上配置均可以在producer下配置出来,其他配置可以参考org.apache.kafka.clients.producer.ProducerConfig
,配置方式需要在properties下去配置,idea无提示,如我们之前使用的自定义分区器配置:partitioner.class。
spring:
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: 具体包名.KeySerializer
value-serializer: 具体包名.ValueSerializer
transaction-id-prefix: tx-
acks: -1
buffer-memory: 1024
compression-type: snappy
retries: 1
batch-size: 1024
client-id: p1
properties:
partitioner:
class: 具体包名.CustomPartitioner
5、消费者示例
5.1、消费者快速入门
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
/**
* @author Xinxin.Wang
* @since 2023/1/5 16:13
*/
@Component
@Slf4j
public class TestConsumer {
@KafkaListener(topics = "test")
void kafkaListener(String content) {
log.info(content);
}
}
5.2、@KafkaListener注解
我们只需要在被Spring管理的Bean方法上加@KafkaListener
,即可从配置的主题中拉取消息。
@KafkaListener
还可以配置拉取主题参数信息
@KafkaListener(topicPartitions = {
@TopicPartition(topic = "test", partitions = {"0", "1", "2"}),
@TopicPartition(topic = "test2", partitionOffsets = @PartitionOffset(partition = "0", initialOffset = "1"))
}, concurrency = "2", errorHandler = "testErrorHandler")
topicPartitions
:指定了了具体的主题以及主题下对应的分区,订阅的更加具体,partitionOffsets
指的是该消费者启动时从该分区的哪个偏移量(从0开始)开始读取数据,partitionOffsets
和partitions
互斥,只能配置一个。
concurrency
:配置该消费者的并发线程数,会覆盖全局配置的concurrency
errorHandler
:配置当处理消息发送异常是的处理方式。value为Spring容器中实现了KafkaListenerErrorHandler
接口的Bean。
错误处理器可以的返回值可以被转发到其他主题。配合@SendTo("topicName")
使用,当消息处理过程中发送了异常,则被errorHandler
处理,如果还配置了@SendTo
,则将errorHandler``返回的结果
转发到配置的topicName
中去,需要注意的是转发的过程中会使用当前系统中配置的KeySerializer
和ValueSerializer
,以及自定义分区器。如果没有配置@SendTo
则返回值无实质作用。
@SendTo
:可以将该消费者消费的消息继续转发给其他的topic,前提是该方法有返回值,则转发该返回值。如果消息处理过程中发生了异常,如若该@KafkaListener
注解配置了errorHandler
,并且handleError
方法返回值不为null则转发errorHandler
的返回值,反之则不转发。
具体代码如下:
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.annotation.PartitionOffset;
import org.springframework.kafka.annotation.TopicPartition;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Component;
/**
* @author Xinxin.Wang
* @since 2023/1/5 16:13
*/
@Component
@Slf4j
public class TestConsumer {
@KafkaListener(topicPartitions = {
@TopicPartition(topic = "test", partitions = {"0", "1", "2"}),
@TopicPartition(topic = "test2", partitionOffsets = @PartitionOffset(partition = "0", initialOffset = "1"))
}, concurrency = "2", errorHandler = "testErrorHandler")
@SendTo("test3")
void kafkaListener(String content) {
int a = 1 / 0;
log.info(content);
}
@KafkaListener(topics = "test3")
void deadMsgListener(String content) {
log.info("test3:{}", content);
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.listener.KafkaListenerErrorHandler;
import org.springframework.kafka.listener.ListenerExecutionFailedException;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
/**
* @author Xinxin.Wang
* @since 2023/1/5 16:27
*/
@Component
@Slf4j
public class TestErrorHandler implements KafkaListenerErrorHandler {
@Override
public Object handleError(Message<?> message, ListenerExecutionFailedException exception) {
log.error("错误处理器,payload:{},exception:{}", message.getPayload(), exception.getMessage());
return message.getPayload() + "该条消息有问题";
}
}
5.3、消费者配置spring.kafka.consumer
- group-id:消费者群组id
- fetch-min-size:服务器应为提取请求返回的最小数据量。
- fetch-max-wait:服务器在响应提取请求之前阻止的最长时间(如果没有足够的数据来立即满足“获取最小大小”给出的要求)
- max-poll-records:在对 poll() 的单次调用中返回的最大记录数。
- heartbeat-interval: 对消费者协调人的预期心跳间隔时间。
- auto-offset-reset:该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下(因消费者长时间失效,包含偏移量的记录已经过时并被删除)该作何处理。
-
它的默认值是 latest,意思是说,在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)。
-
earliest,意思是说,在偏移量无效的情况下,消费者将从起始位置读取分区的记录。
-
none:如果未找到使用者组的先前偏移量,则向使用者引发异常
-
exception:向消费者抛出异常。
-
- enable-auto-commit:该属性指定了消费者是否自动提交偏移量,默认值是 true。为了尽量避免出现重复数据和数据丢失,可以把它设为 false,由自己控制何时提交偏移量。如果把它设为 true,还可以通过配置 auto-commit-interval属性来控制提交的频率
- auto-commit-interval:来控制提交的频率,单位:秒
- isolation-level:读取事务消息的隔离级别。READ_UNCOMMITTED,读未提交。READ_COMMITTED,读已提交。
- key-deserializer: 消息key的反序列化器
- value-deserializer:消息value的反序列化器
和生产者基本配置相同,以上均可在consumer下配置,其他配置可以参考org.apache.kafka.clients.consumer.ConsumerConfig
,配置方式需要在properties下去配置,idea无提示。
6、监听配置spring.kafka.listener
- ack-mode:控制提交偏移量的频率,默认为
BATCH
- RECORD:当监听器处理完一条记录后提交偏移量。
- BATCH:当poll()拉取返回的所有记录被处理完时,提交偏移量。
- TIME:当poll()拉取返回的所有记录被处理完时,并且距离上次提交时间大于TIME时提交偏移量。TIME通过
ack-time
设置,单位:秒。 - COUNT:当poll()拉取返回的所有记录被处理完时,并且从上次提交结束之后到现在已处理的记录数大于等于COUNT时提交偏移量。COUNT通过
ack-count
设置。 - COUNT_TIME:当poll()拉取返回的所有记录被处理完时,并且满足TIME或者COUNT任一条件时,提交偏移量。TIME和COUNT都必须设置。
- MANUAL:消息监听器调用
Acknowledgment.acknowledge()
确认。之后,应用与BATCH
相同的语义。 - MANUAL_IMMEDIATE:当消息监听器调用
Acknowledgment.acknowledge()
方法时立即提交偏移量。 - 注意:使用
MANUAL
或MANUAL_IMMEDIATE
时,消费者的enable-auto-commit
应设置为false,并且监听器要求是AcknowledgingMessageListener
类型或BatchAcknowledgingMessageListener
类型。
- client-id:消费者属性的前缀。覆盖消费者工厂的 client.id 属性;在并发容器中,-n 被添加为每个消费者实例的后缀。
- concurrency:要管理的子
KafkaMessageListenerContainer
的数量。也就是开多少线程消费消息。 - poll-timeout:消费者拉取超时时间,单位:秒。
7、常见解决方案(个人理解)
7.1、如何保证消费顺序?
Kafka最终消息是会被发送到主题下的分区中的,主题内消息不保证有序,但分区内保证有序,所以生产者发送消息时指定分区号或者Key是可以保证消息被有序消费的,因为后写入分区的消息偏移量总是大于先写入的消息偏移量。
7.2、如何保证消息不重复消费?
最后好业务做幂等性。其次是关闭自动提交,改为手动提交。手动提交也有时机问题,不管是先处理业务后提交偏移量,还是先提交偏移量后处理业务,都有可能还会导致重复消费,所以根本还是要业务做好幂等性。
7.3、如何保证消息不丢失?
首先从生产者方来解决,生产者方配置acks响应类型,Broker会根据生产者配置的响应类型达到具体的要求之后响应给生产者,生产者可以通过发送方法的同步返回,或者异步回调来确认消息是否被成功发送到Broker集群,如果发送失败,还可以重试,重试次数可以使用retries
来配置,还可以通过retry.backoff.ms
配置重试间隔,避免短时间内多次重试,没有达到重试的预期效果。
再者Kafka侧提供了分区复制,通过生产者配置的acks类型,Broker会根据acks类型进行分区复制,已达到数据冗余存储。如果分区的Broker首领宕机,其他该分区的Broker会重新选举出新的首领Broker,生产者消费者会连接到新的首领Broker,所以消息不会丢失。
再者消费者方如果接收到了消息,在处理过程中,消费者宕机,导致消息丢失。这种情况一般是关闭消费者方的自动提交,改为手动提交偏移量,因为一般都是消费者方处理完业务之后,手动提交偏移量即可,当然手动提交偏移量最好和业务在同一个事务中。