消息队列是面试中一定会被问到的技术模块,虽然它在面试题占比不及并发编程和数据库,但也属于面试中的关键性问题。所以今天我们就来看一道,MQ 中高频,但可能会打破你以往认知的一道面试题。

所谓的关键问题指的是这道面试题会影响你整体面试结果。

我们在面试消息队列(Message Queue,MQ)时,尤其是面试 Kafka 时,经常会被问到:如何保证消息不丢失?

那么,我们的回答会分为以下 3 部分:

  1. 保证生产者消息不丢失
  2. 保证 Kafka 服务(器端)消息不丢失
  3. 保证消费者消息不丢失

只有保证这 3 部分消息都不丢失,才能保证 Kafka 整体消息不丢失。

因为 Kafka 消息的传递流程如下(总共包含 3 部分):

1.如何保证生产者消息不丢失?

那怎么保证生产者消息不丢失呢?

要搞明白这个事,我们就要先了解一下生产者发送消息的执行流程。

Kafka 生产者发送消息的执行流程如下:

默认情况下,所有的消息会先缓存到 RecordAccumulator 缓存中,再由 Sender 线程拉取消息发送到 Kafka 服务器端,通过 RecordAccumulator 和 Sender 线程的协作,实现了消息的批量发送、性能优化和异常处理等功能,确保了消息的高效可靠传输。

1.1 RecordAccumulator 缓存作用

  1. 暂存消息:RecordAccumulator 是 Kafk a生产者中的一个关键组件,它充当了一个缓存的角色,用于暂存主线程(Main Thread)发送过来的消息。这些消息在 RecordAccumulato r中等待被 Sender 线程批量发送。
  2. 批量发送:RecordAccumulator 通过批量收集消息,减少了单个消息发送的网络请求次数,从而提高了发送效率。Sender 线程可以从 RecordAccumulator 中批量获取消息,一次性发送到 Kafka 集群,减少了网络传输的资源消耗。
  3. 性能优化:RecordAccumulator的缓存大小可以通过生产者客户端参数 buffer.memory 进行配置(默认值为 32MB)。合理的缓存大小设置可以平衡内存使用与发送效率,达到最优的性能表现。
  4. 内存管理:如果 RecordAccumulator 的缓存空间被占满,生产者再次调用 send() 方法发送消息时,会出现阻塞(默认阻塞时间为 60 秒,可通过 max.block.ms 参数配置)。如果阻塞超时,则会抛出异常。这种机制有助于防止生产者因为无限制地缓存消息而耗尽系统资源。
  5. ByteBuffer 复用:为了减少频繁创建和释放 ByteBuffer 所造成的资源消耗,RecordAccumulator 内部还维护了一个 BufferPool,用于实现 ByteBuffer 的复用。特定大小的 ByteBuffer 会被缓存起来,以便后续消息发送时重复使用。

1.2 Sender 线程作用

  1. 拉取消息:Sender 线程是 Kafka 生产者中的一个后台线程,它负责从 RecordAccumulator 中拉取缓存的消息。Sender 线程会定期轮询 RecordAccumulator,检查是否有新消息需要发送。
  2. 批量构建请求:当 Sender 线程发现有新消息需要发送时,它会构建一个或多个 ProducerRequest 请求。每个请求包含多个消息,以便进行有效的批量发送。这种批量发送机制可以显著提高网络传输效率。
  3. 发送消息到 Kafka 集群:Sender 线程将构建的 ProducerRequest 请求发送到 Kafka 集群的相应分区。它会根据分区的 Leader 节点信息,将消息发送给对应的 Broker 节点。
  4. 异常处理:在消息发送过程中,可能会出现网络故障、分区不可用等异常情况。Sender 线程负责处理这些异常,例如进行重试、重新连接等操作,以确保消息的可靠发送。
  5. 状态更新:一旦消息被成功接收并记录在 Kafka Broker 的日志中,Sender 线程会通知 RecordAccumulator 更新消息的状态。这样,生产者就能够知道哪些消息已经被成功发送,哪些消息还需要重试发送。

2.生产者消息丢失的两种场景

了解了 Kafka 生产者发送消息的流程之后,我们就能知道在这个环节丢失消息的情况有以下两种:

  1. 网络抖动(消息不可达):生产者与 Kafka 服务端之间的链路不可达,发送超时。此时各个节点的状态是正常,但消费端就是没有消费消息,就像消息丢失了一样。
  2. 无消息确认(ack):生产者消息发送之后,无 ack 消息确认,直接返回消息发送成功,但消息发送之后,Kafka 服务宕机或掉电了,导致消息丢失。

怎么解决这个问题呢?

2.1 网络波动问题处理

网络波动的话设置消息重试即可,因为网络抖动消息不可达,所以只要配置了重试次数,那么就会消息重试以此来保证消息不丢失。

在 Spring Boot 项目中,只需要在配置文件 application.yml 中,设置生产者的重试次数即可:

spring:  
  kafka:  
    producer:  
      retries: 3

2.2 消息确认设置

Kafka 生产者的 ACK(Acknowledgment)机制是指生产者在发送消息到 Kafka 集群后,等待确认的方式。这个机制决定了生产者何时认为消息已经成功发送,并直接影响到消息的可靠性和性能。

Kafka 生产者的 ACK 机制主要有以下三种类型。

① acks=0

生产者在将消息发送到网络缓冲区后,立即认为消息已被提交,不会等待任何来自服务器的响应。这时设置的重试次数 retries 无效。

特点

  • 最高性能:由于不需要等待任何确认,因此具有最高的吞吐量。
  • 最低可靠性:消息可能会在发送过程中丢失,生产者无法知道消息是否成功到达服务器。

适用场景:对消息可靠性要求不高,但追求极致性能的场景。

② acks=1

生产者在将消息发送到主题的分区 leader 后,等待 leader 的确认,即认为消息已被提交(此时 leader 写入成功,并没有刷新到磁盘),不用等待所有副本的确认。

特点

  • 中等可靠性和性能:提供了一定程度的可靠性,因为只有领导者副本确认消息后生产者才会收到确认。但如果领导者副本在确认后发生故障,而消息还未复制到其他副本,则消息可能会丢失。
  • 性能与可靠性平衡:在生产者性能和消息可靠性之间提供了一个折衷方案。

适用场景:适用于传输普通日志,允许偶尔丢失少量数据的场景。

③ acks=all 或 acks=-1

生产者需要等待所有同步副本(ISR, In-Sync Replicas)都成功写入消息后,才认为消息已被提交。

特点

  • 最高可靠性:只有当所有同步副本都确认接收到消息后,生产者才会收到确认,确保了消息的可靠性。
  • 较低性能:由于需要等待所有同步副本的确认,因此可能会导致消息发送的延迟增加,从而影响性能。

适用场景:适用于对消息可靠性要求极高的场景,如金融交易等关键任务应用。

在 Spring Boot 项目中,acks 可以在配置文件 application.yml 中设置:

spring:  
  kafka:  
    producer:  
      acks: all

3.acks=all消息一定不会丢失吗?

正常情况下当我们设置 acks=all 时,其实是可以保证数据不丢失了。但是有一种特殊情况,如果 Topic 只有一个 Partition(分区时),也就是只有一个 Leader 节点时,此时消息也是会丢失的

如果只有一个 Leader 节点,acks=all 的设置和 acks=1 的设置效果基本类似,当 Leader 确认消息之后,还没来得及将消息刷到磁盘之前宕机了,那么就会造成消息丢失。

万事必有妖,当面试官用疑问语句问你时,答案基本是否定的。如果是确定的话,面试官可能也就不会再问你了,所以当你听到一个有悖于常识的问题时,先努力思考这个问题还有没有其他答案。

课后思考

Kafka 服务器端和消费者如何保证消息不丢失呢?

特殊说明

以上内容来自我的《Java 面试突击训练营》,这门课程是有着十几年工作经验(前 360 开发工程师),10 年面试官经验的我,花费 4 年时间打磨完成的一门视频面试课。学完训练营的课程之后,基本可以应对目前市面上绝大部分公司的面试了,并且课程配备了 9 大就业服务,帮助上千人找到 Java 工作,其中上百人拿到大厂 Offer,学员最高薪资 70W 年薪,面试课目录和 9 大服务如下:

加我微信咨询:vipStone【备注:训练营】