【面试题】MQ部分[2025/1/13 ~ 2025/1/19]
MQ部分[2025/1/13 ~ 2025/1/19]
- 1. 如何处理重复消息?
- 2. 如何保证消息的有序性?
- 3. 如何处理消息堆积?
- 4. 如何保证消息不丢失?
- 5. [RabbitMQ] RabbitMQ 怎么实现延迟队列?
- 6. [RabbitMQ] RabbitMQ 中消息什么时候会进入死信交换机?
- 7. [RabbitMQ] RabbitMQ 中无法路由的消息会去到哪里?
- 8. [RocketMQ] 为什么 RocketMQ 不使用 Zookeeper 作为注册中心呢?而选择自己实现 NameServer?
- 9. [RocketMQ] 说一下 RocketMQ 中关于事务消息的实现?
- 10. [RocketMQ] RocketMQ 的事务消息有什么缺点?你还了解过别的事务消息实现吗?
- 11. [Kafka] Kafka为什么要抛弃 Zookeeper?
- 12. [Kafka] Kafka 中 Zookeeper 的作用?
- 13. [Kafka] 说一下 Kafka 中关于事务消息的实现?
我的博客地址
1. 如何处理重复消息?
只有让消费者的处理逻辑具有幂等性,保证无论同一条消息被消费多少次,结果都是一样的,从而避免因重复消费带来的副作用。
2. 如何保证消息的有序性?
- 单一生产者和单一消费者:
- 使用单个生产者发送消息到单个队列,并由单个消费者处理消息。这样可以确保消息按照生产者的发送顺序消费。
- 这种方法简单但容易成为性能瓶颈,无法充分利用并发的优势。
- 分区与顺序键(Partition Key):
- 在支持分区(Partition) 的消息队列中(如 Kafka、RocketMQ),可以通过 Partition Key 将消息发送到特定的分区。每个分区内部是有序的,这样可以保证相同 Partition Key 的消息按顺序消费。
- 例如,在订单处理系统中,可以使用订单号作为 Partition Key,将同一个订单的所有消息路由到同一个分区,确保该订单的消息顺序。
- 顺序队列(Ordered Queue):
- 一些消息队列系统(如 RabbitMQ)支持顺序队列,消息在队列中的存储顺序与投递顺序一致。如果使用单个顺序队列,消息将按顺序被消费。
- 可以使用多个顺序队列来提高并发处理能力,并使用特定规则将消息分配到不同的顺序队列中。
3. 如何处理消息堆积?
消息堆积是指在消息队列中,消息的生产速度远大于消费速度,导致大量消息积压在队列中。
- 消费者: 提升消费者的消费能力, 批量消费和异步消费
- 增加消费者线程数量:提高并发消费能力。
- 增加消费实例:在分布式系统中,可以水平扩展多个消费实例,从而提高消费速率。
- 优化消费者逻辑:检查消费者的代码,减少单个消息的处理时间。例如,减少 I/O 操作、使用批量处理等。
- 生产者:
生产端限流, 使用令牌桶、漏桶算法等限流策略,限制生产者的发送速率,从而避免消息队列被快速填满。
4. 如何保证消息不丢失?
主要从生产者
、broker
、消费者
:
- 生产者的消息确认:生产者在发送消息时,需要通过消息确认机制来确保消息成功到达。
- 存储消息:broker 收到消息后,需要将消息持久化到磁盘上,避免消息因内存丢失。即使消息队列服务器重启或宕机,也可以从磁盘中恢复消息。
- 消费者的消息确认:消费者在处理完消息后,再向消息队列发送确认(ACK),如果消费者未发送确认,消息队列需要重新投递该消息。
5. [RabbitMQ] RabbitMQ 怎么实现延迟队列?
-
TTL+DLX
RabbitMQ 本身不支持延迟消息,但是可以通过它提供的两个特性 TTL(Time-To-Live and Expiration ,消息存活时间)、DLX(Dead Letter Exchanges,死信交换器) 来实现。注: ttl+dlx问题: 队列是先进先出, 当前面消息时间过长,会导致后续消息延迟问题, 出现时序问题
-
利用RabbitMQ提供插件: delay_message_exchange
6. [RabbitMQ] RabbitMQ 中消息什么时候会进入死信交换机?
- 消息被拒绝(Rejection)
当消费者使用 basic.reject 或 basic.nack 明确拒绝消息,并且不要求重新投递(requeue 设置为 false)时,消息会被直接投递到死信交换机。 - 消息过期(TTL Expiration)
RabbitMQ 支持为消息或队列设置 TTL(Time-To-Live),即生存时间。当消息超过指定的存活时间后还未被消费,它会自动变为死信并被发送到死信交换机。 - 队列达到最大长度(或总大小)
如果队列设置了最大长度(x-max-length 或 x-max-length-bytes),当消息数量或总大小超出限制时,最早进入队列的消息会被移入死信交换机。
7. [RabbitMQ] RabbitMQ 中无法路由的消息会去到哪里?
- 丢弃消息
默认情况下,若消息无法找到符合条件的队列(即没有匹配的绑定关系),RabbitMQ 会直接丢弃消息,不会进行特殊处理。 - 备份交换机(Alternate Exchange)
可以为交换机配置一个备份交换机,无法被路由的消息将被发送到备份交换机,再由备份交换机根据其绑定关系决定如何处理消息。例如,可以将这些消息发送到指定队列进行保存或处理。 - 消息回退(Return Listener)
在使用 mandatory 参数的情况下,如果消息无法路由,则会触发返回机制,将消息退回到生产者,这样生产者可以自行处理未路由的消息。
8. [RocketMQ] 为什么 RocketMQ 不使用 Zookeeper 作为注册中心呢?而选择自己实现 NameServer?
- 轻量级和高性能需求
RocketMQ 需要一个高性能、低延迟的注册中心。Zookeeper 的强一致性和复杂的管理机制带来了一定的开销。NameServer 是一个简单的、轻量级的服务发现和路由管理组件,更适合于消息队列的需求。 - 动态拓扑和高扩展性
NameServer 采用无状态设计,可以随时增加或减少节点,且 NameServer 节点间不需要进行同步,减少了集群复杂度,适应动态的 Broker 集群环境,便于拓展和维护。 - 弱一致性要求
RocketMQ 的服务发现和路由信息允许短时间内的不一致,NameServer 只提供基础的服务发现功能,不需要像 Zookeeper 那样强一致的分布式一致性算法,这样可以提高性能和减少管理复杂度。
9. [RocketMQ] 说一下 RocketMQ 中关于事务消息的实现?
- 第一阶段(消息发送)
- 生产者先将消息发送到 RocketMQ 的 Topic,此时消息的状态为半消息(Half Message),消费者不可见。
- 然后,生产者执行本地事务逻辑,并根据本地事务的执行结果来决定下一步的操作。
- 第二阶段(提交或回查)
- 如果本地事务成功,生产者会向 RocketMQ 提交 Commit 操作,将半消息变为正式消息,消费者可见。
- 如果本地事务失败,生产者会向 RocketMQ 提交 Rollback 操作,RocketMQ 会丢弃该半消息。
- 如果生产者没有及时提交 Commit 或 Rollback 操作,RocketMQ 会定时回查生产者本地事务状态,决定是否提交或回滚消息。
10. [RocketMQ] RocketMQ 的事务消息有什么缺点?你还了解过别的事务消息实现吗?
- 从事务消息的改造成本来看,RocketMQ 的事务消息改造成本不小,需要改造原始逻辑实现特定的接口,且在应用层处理复杂的回查逻辑,确保回查不会重复或丢失。
- 从事务消息功能性来看,RocketMQ 仅支持单事务消息。
- 从可用性角度来看,依赖MQ集群的可靠性。如果MQ 集群挂了,事务就无法继续进行了,等于整个应用无法正常执行了。
其他解决方案 QMQ(Qunar Message Queue)(去哪儿),数据现存在数据库, 同时创建一张消息定时遍历去发送消息
- QMQ是什么?
QMQ(Qunar Message Queue)是由去哪儿网自主研发的一款高性能、低延迟、可靠的分布式消息队列系统。它主要用于解决高并发、高吞吐量的业务场景下的消息传递问题,特别适用于订单处理、支付系统等对实时性和可靠性要求较高的场景。 - QMQ的设计思路
- 分布式架构与去中心化设计
QMQ采用分布式架构,去中心化设计,避免了单点故障问题,同时支持水平扩展。这种设计使得系统在面对大规模消息处理时更加稳定,且易于扩展。 - 消息存储与持久化
QMQ的消息存储采用了顺序写入磁盘的方式,类似于Kafka的日志存储结构。消息按照到达顺序写入Broker的存储文件中,充分利用磁盘的顺序写性能,提升写入速度。此外,QMQ支持消息的多副本存储,确保消息的可靠性。 - 事务一致性与补偿机制
QMQ在消息发送时,会先将消息存储在本地磁盘队列中,然后异步发送到Broker。如果发送失败,系统会通过补偿任务重新发送消息,直到成功为止。这种机制确保了消息的可靠性和事务一致性。 - 动态映射与消费模式
QMQ通过添加一层拉取日志(pull log)来动态映射消费者与消息的逻辑关系。这种方式解决了消费者动态扩容缩容的问题,同时继续使用单一偏移量(offset)表示消费进度。此外,QMQ支持广播模式和集群模式两种消费模式,分别适用于全量消息消费和负载均衡场景。 - 高性能与低延迟
QMQ通过优化的消息序列化和网络通信机制(如Netty),实现了低延迟和高吞吐量。系统还支持异步非阻塞I/O机制,确保在处理大量并发请求时不会因某个操作的延迟而影响整体性能。
- 分布式架构与去中心化设计
11. [Kafka] Kafka为什么要抛弃 Zookeeper?
主要是为了简化架构、提升可扩展性和降低运维复杂性。Kafka 引入了 KRaft(Kafka Raft)模式,即使用 Kafka 自身实现的 Raft 共识算法替代 Zookeeper。
- 简化架构:Zookeeper 是一个独立的分布式协调服务,Kafka 需要依赖它来管理元数据。引入 Zookeeper 增加了系统复杂度,尤其在处理集群的动态扩展和管理时,Kafka 和 Zookeeper 之间的协调带来了额外的开销。通过 KRaft,Kafka 可以直接管理元数据,消除了对外部协调服务的依赖。
- 提升可扩展性:Zookeeper 的写入性能有限,随着 Kafka 集群的规模增大,元数据的读写操作可能会对 Zookeeper 造成压力,进而成为 Kafka 扩展性的瓶颈。KRaft 模式通过将元数据存储在 Kafka 自身中,并使用 Raft 协议来确保一致性,使 Kafka 的扩展能力得到提升。
- 降低运维成本:在生产环境中,Kafka 和 Zookeeper 之间需要进行一致性管理和维护,运维人员需要掌握两套系统的部署、监控和故障排查。去掉 Zookeeper 后,Kafka 只需维护自身的节点和协议,简化了运维流程。
Raft 是一种用于分布式系统中管理复制日志的共识算法,旨在通过清晰的角色划分(Leader、Follower、Candidate)和结构化的选举与日志复制流程来简化理解和实现。它确保了即使在网络分区或节点故障的情况下,集群中的多数节点仍能达成一致,从而维持系统的高可用性和数据一致性。Raft 的关键特性包括基于任期的领导选举、安全的日志复制机制以及对新 Leader 的严格限制,这些都保证了系统的稳定运行和数据的完整性。
12. [Kafka] Kafka 中 Zookeeper 的作用?
在 Kafka 中,Zookeeper 扮演了集群协调和管理的核心角色。它的主要作用是管理和协调 Kafka 集群中的元数据,帮助 Kafka 实现高可用性、负载均衡和容错性。
- 管理 Broker 元数据
Zookeeper 负责管理 Kafka 集群中 Broker 的注册、状态监控。当有新的 Broker 加入或离开集群时,Zookeeper 能够及时更新集群状态。 - 协调分区副本 Leader 选举
当某个分区的 Leader 副本故障时,Zookeeper 协调副本的选举过程,为该分区选出新的 Leader,确保分区高可用。 - 动态配置和负载均衡
Zookeeper 保存着 Kafka 配置和拓扑信息,当集群发生变化时(如增加或减少分区、调整副本因子),Zookeeper 协助完成负载均衡。
13. [Kafka] 说一下 Kafka 中关于事务消息的实现?
Kafka 的事务消息不同于我们理解的分布式事务消息,它的事务消息是实现了消息的Exactly Once 语义,即保证消息在生产、传输和消费过程中的“仅一次”传递。
-
几个核心组件来实现:
事务协调器(Transaction Coordinator)
:负责事务的启动、提交和中止管理,并将事务状态记录到 __transaction_state 内部主题。幂等生产者(Idempotent Producer)
:Kafka Producer 通过 Producer ID (PID) 识别每个事务的唯一性,确保同一事务的每条消息只写入一次。事务性消费
:在消费过程中,消费者可以选择隔离未完成事务的数据(通过 read_committed 设置),只消费已提交的事务消息,确保数据的最终一致性。
-
Kafka 事务消息流程:
- 启动事务:事务性生产者向 Transaction Coordinator 请求启动事务。
- 生产消息:生产者开始向 Kafka 写入事务消息,每条消息都带有唯一的 Producer ID 和 Sequence Number,以保证幂等性。
- 提交事务:在所有消息写入完成后,生产者向事务协调器发送 commit 或 abort 请求,提交或中止事务。
- 事务性消费:消费者可以通过设置 read_committed 隔离级别,仅消费已提交的消息,实现最终数据一致性。