消息队列 面试题


1. Kafka 高吞吐量设计

题目: Kafka 是如何实现高吞吐量的?从存储架构、网络模型、消费模型三个角度分别说明。

💡 答案:

Kafka 的高吞吐量来源于设计上的多个关键决策。

存储架构方面: Kafka 采用顺序写入磁盘的方式——数据以日志方式追加到文件末尾,充分利用磁盘的顺序写性能(机械盘顺序写 100MB+/s,接近随机写的几十倍),避免了传统消息队列频繁随机 IO 的问题。日志文件按 segment 分段存储,便于清理过期数据。

网络模型方面: Kafka 大量使用零拷贝技术——Producer 到 Broker 和 Broker 到 Consumer 的数据传输都尽可能避免不必要的数据复制和 CPU 参与,直接从文件系统缓存发送到网络。

消费模型方面: Kafka 采用 Pull 模式——消费者根据自己的处理能力拉取消息,避免了 Push 模式下消费者来不及处理造成的消息积压和 OOM,同时支持批量拉取减少网络往返次数。

日志压缩与分区并行: 也是一个重要因素——一个 Topic 可以有多个分区,每个分区可以由独立的 Broker 负责,生产和消费可以在分区级别实现完全并行。

追问1: Kafka 的零拷贝(Zero Copy)是什么意思?它和传统的四次拷贝两次 CPU 拷贝有什么区别?

零拷贝的核心是”不使用 CPU 在内核空间和用户空间之间拷贝数据”。传统方式下从磁盘文件发送到网络需要:磁盘 → 内核缓冲区(DMA 拷贝,1次)、内核缓冲区 → 用户空间(CPU 拷贝,2次)、用户空间 → Socket 缓冲区(CPU 拷贝,3次)、Socket 缓冲区 → 网卡(DMA 拷贝,4次),总共四次拷贝两次 CPU 参与。Kafka 利用 Linux 的 sendfile 系统调用(底层用 spliceDMA Scatter/Gather):磁盘 → 内核缓冲区(DMA 拷贝)、内核缓冲区 → 网卡(DMA 拷贝),数据从磁盘到网卡完全在内存中的”页面”级别完成,CPU 不必参与数据搬运,只有两次 DMA 拷贝。这个优化对性能影响巨大——在 100Gb 网卡上,没有零拷贝的话 CPU 很快就成为瓶颈。Java 中 FileChannel.transferTo 底层就使用了 Linux 的 sendfile(取决于操作系统支持)。

追问2: Kafka 为什么能做到百万级消息堆积而不影响性能?这和传统的 RabbitMQ 有什么根本区别?

根本区别在于存储模型。

RabbitMQ 每个消息都带有很多元信息来管理消息的确认、重试、过期等复杂逻辑,消息存储在内部数据库中,大量堆积时这个索引数据库的性能急剧下降。RabbitMQ 的消息每一条是独立存储、独立管理的,处理完后删除。

而 Kafka 的消息存储本质上是 append-only 日志文件 + 一个 index 索引文件(稀疏索引,非密集索引),Consumer 只需要记录自己消费到的 offset 位置。堆积对 Kafka 的影响仅仅是磁盘占用增加,而顺序读写的性能基本不受数据量影响。Kafka 的消息按分区整体管理,以 segment 为单位过期删除——Consumer 只是给偏移量”挪了一下指针”,这个操作的成本是常量级的。

所以”堆积 100 万条消息”在 Kafka 中是对性能几乎没有影响的,在 RabbitMQ 中则是管理负担随消息量线性增长。

📌 易错点 / 加分项:

  • 零拷贝不适用于需要修改数据的场景(如加密压缩),Kafka 只在不需要修改的传输路径上使用
  • Kafka 的分区数不是越多越好——分区越多,元数据更新开销、Leader 选举等成本越高
  • Producer 的 acks=all 配合 min.insync.replicas 是消息不丢失的关键配置

2. Kafka 消息不丢失与重复消费

题目: Kafka 如何保证消息不丢失?从 Producer、Broker、Consumer 三个角色分别说明。

💡 答案:

消息不丢失需要三个角色的配合:

Producer 端: acks 配置决定消息发送的确认级别——acks=0 发完不管、数据可能丢失;acks=1 等待 Leader 确认,如果 Leader 宕机但 Follower 未同步完则数据丢失;acks=all(或 -1)需要 Leader + 所有 ISR 中的副本确认,同时配合 min.insync.replicas 保证至少有 N 个副本存活,最大限度保证不丢。此外 retries 设为大于 0 的值启用重试,enable.idempotence=true 开启生产者幂等避免网络故障导致的重复写入。

Broker 端: replication.factor 大于 1 且 min.insync.replicas 至少 2,保证有副本;unclean.leader.election.enable=false 禁止 ISR 之外的副本竞选 Leader(宁可不可用也不丢数据是很多金融业务的默认选择)。

Consumer 端: 禁用自动提交(enable.auto.commit=false),在业务处理完成后手动提交 offset(commitSync),保证”处理一条、提交一条”,而不是”消费一条、提交一条但处理失败了”。

追问1: 消息已经保证”不丢失”了,为什么还会”重复消费”?什么样的业务场景需要做幂等处理?

消息不丢失和重复消费是两个维度的问题。“不丢失”保证的是数据至少到达一次(At-Least-Once),但无法保证精确一次(Exactly-Once)。

重复消费的根源:

Producer 端: 发送消息后网络超时,Producer 以为失败了于是重试,但其实 Broker 已经写入成功——Broker 中存了两条相同的消息。

Consumer 端: 在处理完消息后准备提交 offset 的时候进程挂了,重启后从上一次提交的位置重新消费,导致 Consumer 重复处理。

所以幂等处理是业务必须自己实现的——比如用数据库唯一键约束兜底(INSERT ... ON DUPLICATE KEY UPDATE)、Redis 的 setNX、或者给每条消息分配唯一的业务 ID 在消费前先查是否已处理过。

追问2: Kafka 的事务消息解决了什么问题?和 RocketMQ 的事务消息有什么不同?

Kafka 的事务消息(自 0.11 版本引入)主要解决”consume-transform-produce” 模式的原子性问题——比如从 Topic A 消费一条消息并处理后写入 Topic B,需要保证”读取 + 处理 + 写入”是一个原子操作,如果处理失败,消息的消费 offset 不提交同时不写入 Topic B。Kafka 通过 transaction coordinator 和两阶段提交实现,配合 isolation.level=read_committed 消费者只读已提交的事务消息。

RocketMQ 的事务消息解决的是”本地事务 + 发送消息”的原子性——先发一个 half 消息(对消费者不可见),执行本地事务,根据本地事务结果再决定是提交还是回滚 half 消息。典型的场景是”下单扣库存,同时发消息通知其他服务”——本地事务扣库存成功、发消息确实成功;如果本地事务失败,half 消息回滚。

这是 RocketMQ 最经典的差异化能力,Kafka 不提供这种内置的事务模式。

📌 易错点 / 加分项:

  • acks=all 不等于不丢消息——ISR 中只有一个节点且 min.insync.replicas=1,宕机仍可能丢
  • enable.auto.commit 是反模式——消费者在 poll 时自动提交,如果 poll 和业务处理之间发生异常数据就丢了
  • Kafka 事务的代价是吞吐量下降约 15-20%,不是所有场景都要开

3. RocketMQ 架构与事务消息

题目: RocketMQ 的整体架构是怎样的?NameServer 和 Kafka 的 ZooKeeper 有什么不同的设计哲学?

💡 答案:

RocketMQ 的架构由 NameServer、Broker、Producer、Consumer 四部分组成。NameServer 是整个集群的”路由注册中心”,每个 Broker 启动时注册到所有 NameServer,Producer 和 Consumer 定期从 NameServer 获取 Topic 的路由信息(该 Topic 落在哪些 Broker 上)。NameServer 之间不通信,是完全对等的节点,没有主从关系——这跟 ZooKeeper 的设计理念完全不同。

ZooKeeper 通过 ZAB 协议保持集群一致性,能作为分布式协调的基础设施;NameServer 是”最终一致性”的——Broker 定时心跳上报,NameServer 之间不同步,如果一个 NameServer 挂了,不影响其他 NameServer 及其上的路由信息。这种设计使得 NameServer 极其轻量、易部署,适合 RocketMQ 的”路由信息变化不频繁且允许短暂不一致”的场景。

Kafka 在 3.x 版本后也逐步去 ZooKeeper 依赖(KRaft 模式),方向上也是认为”为消息系统引入一个强一致性协调者太重”。

追问1: RocketMQ 的延迟消息是如何实现的?延迟级别可以任意设置吗?

RocketMQ 的延迟消息不是任意的——它预设了一系列延迟级别 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h(共 18 个级别,商业版可以更多)。实现原理是:Broker 收到延迟消息后不直接投递给 Consumer,而是按延迟级别分到对应的内部延迟队列中,通过定时任务检查——当消息的延迟时间到了,将其移到真正的消费队列中等待 Consumer 拉取。由于只支持固定级别,延迟队列数有限(18 个),定时器只需轮询这 18 个队列即可。不能任意设置的原因是:如果支持任意时间,延迟队列数量会爆炸,定时器的检查成本不可控。但如果业务需要精确的延迟(比如 17 秒后发消息),就需要自己在上层封装——先算最近可用的延迟级别,在消费者端再判断是否满足精确时间要求。

追问2: RocketMQ 的事务消息”回查机制”是什么?为什么要回查?

回查机制是 RocketMQ 事务消息的核心保障。

流程是:Producer 先发 half 消息成功,然后执行本地事务。如果本地事务执行成功,Producer 向 Broker 发送 Commit 请求。

但如果本地事务执行过程中 Producer 挂了或网络中断,Broker 永远收不到 Commit/Rollback——此时 half 消息的状态是 Unknown。回查机制:Broker 定期扫描 Unknown 状态的事务消息,主动回查 Producer 端,调用 Producer 注册的回查接口 checkLocalTransaction(),让 Producer 去检查本地事务到底成功了没有(比如查一下数据库那个订单创建了没有),然后确定 commit 还是 rollback。

需要回查的根本原因是:分布式环境下,“发送 Commit”这个动作本身可能因为网络原因没有到达 Broker,需要有一个独立的路径来确认事务的最终状态。

📌 易错点 / 加分项:

  • NameServer 是无状态、可水平扩展的——部署两个足够了,多了反而没必要
  • RocketMQ 的延迟消息是”近似延迟”——如果一条延迟消息所在队列积压严重,实际延迟可能比预设级别更长
  • RocketMQ 事务消息不是所有业务都适合——回查逻辑的复杂度需要业务方配合

4. RabbitMQ 核心架构与消息流转

题目: 请描述 RabbitMQ 的核心架构组件——Exchange、Queue、Binding、Virtual Host——以及一条消息从生产者到消费者的完整流转过程。

💡 答案:

RabbitMQ 的核心架构基于 AMQP 0-9-1 协议,有四个关键组件:

  • Exchange(交换机):消息的第一站,生产者不直接把消息发给队列,而是发给 Exchange。Exchange 根据路由规则决定消息去哪个 Queue。
  • Queue(队列):存储消息的缓冲区,消费者从 Queue 中拉取或推送消费。
  • Binding(绑定):Exchange 和 Queue 之间的路由规则,指定什么 routing key 的消息从 Exchange 到哪个 Queue。
  • Virtual Host(虚拟主机):RabbitMQ 内部的逻辑隔离单元,每个 vhost 有独立的 Exchange、Queue、Binding、权限控制,类似于一个”轻量级的 RabbitMQ 实例”。

一条消息的完整流转:生产者通过 Connection 建立 TCP 连接,创建 Channel(虚拟连接,轻量级),通过 Channel 发送消息到指定 Exchange。Exchange 拿到消息后,根据消息的 routing key 和 Queue 的 binding key 进行匹配。匹配规则取决于 Exchange 类型:Direct Exchange 精确匹配 routing key 和 binding key;Topic Exchange 按通配符(* 匹配一个词,# 匹配零个或多个词)匹配;Fanout Exchange 忽略 routing key,广播到所有绑定的 Queue;Headers Exchange 按消息 Header 匹配。匹配成功后消息被路由到对应 Queue。消费者从 Queue 获取消息——可以是 Push 模式(Broker 推给消费者)或 Pull 模式(消费者主动拉)。

追问1: Channel 为什么是”轻量级”的?为什么不直接用 TCP 连接(Connection)而要用 Channel?

一个 TCP Connection 的建立成本很高——三次握手、内核 socket 资源、网络带宽。如果每个业务线程都开一个 Connection,几百个线程就能把 Broker 的连接数打满。Channel 是”复用同一个 TCP 连接内的虚拟通道”——在一个 Connection 上可以创建多个 Channel,每个 Channel 有一个独立的 ID,TCP 数据包中携带 Channel ID 来区分属于哪个 Channel。Channel 的创建关闭几乎零成本(只是一次帧交换),这使得应用可以用很低的资源成本维护几百个 Channel。这也是 AMQP 协议的特色——连接复用。

📌 易错点 / 加分项:

  • 生产者也可以直接发消息到 Queue(不通过 Exchange)——但这是”default exchange”隐式实现了路由
  • Binding key 和 routing key 不是同一个概念——binding key 在绑定队列时设置,routing key 在发送消息时指定
  • 一个 Queue 可以绑定到多个 Exchange——灵活性很高

5. RabbitMQ 消息确认机制

题目: RabbitMQ 的消息确认机制分哪两个层面?Publisher Confirm 和 Consumer Ack 分别是如何工作的?如何保证消息不丢失?

💡 答案:

RabbitMQ 的消息确认分两个层面。

Publisher Confirm(生产者确认):生产者发送消息后,Broker 确认消息已安全到达。工作方式:生产者通过 channel.confirmSelect() 开启确认模式,此后每条消息发送后 Broker 会返回一个异步确认(Basic.Ack)或未确认(Basic.Nack)。Ack 表示 Broker 已接收并处理了该消息,Nack 表示处理失败(如队列满了、Exchange 不存在等)。Publisher Confirm 支持批量确认——多条消息可以一次性确认,也支持异步回调确认。

Consumer Ack(消费者确认):消费者告诉 Broker”我处理完了,可以把消息删了”。工作方式:Broker 将消息投递给消费者后,消息进入”Unacked”状态等待消费者 Ack。消费者调用 basicAck 确认后 Broker 从 Queue 中删除消息。如果消费者断开了连接(Channel/Connection 关闭)而消息还未确认,Broker 认为消息处理失败,将消息重新入队(requeue)或丢弃(取决于配置),重新投递给其他消费者。

保证消息不丢失需要三个环节都配置正确:

  • Broker 端:设置 Queue 为 durable(持久化),消息投递时设置 deliveryMode=2(消息持久化),这样 RabbitMQ 重启后队列和消息还在。
  • Producer 端:使用 Publisher Confirm,确保 Broker 确认收到消息再认为发送成功。
  • Consumer 端:关闭 autoAck(autoAck=false),手动 basicAck,在业务处理成功后再确认。如果业务处理失败,basicNack(deliveryTag, false, true) 让消息重新入队。

📌 易错点 / 加分项:

  • autoAck=true 是性能最高但最不安全的配置——消息一到消费者就从 Queue 删除了,不管消费者是否处理成功
  • Publisher Confirm 是异步的——如果 confirm 返回 Nack 需要重发,但重发可能导致顺序变化
  • 持久化有性能代价——每条消息都要 fsync,磁盘 IO 压力大。可以用批量确认来提升吞吐

6. RabbitMQ 死信队列与延迟队列

题目: RabbitMQ 的死信队列(DLX)和延迟队列分别是怎么实现的?它们解决了哪些典型的业务场景?

💡 答案:

死信队列(Dead Letter Exchange)是 RabbitMQ 的”垃圾回收站”——当一条消息变成”死信”时,Broker 不会直接丢弃它,而是重新投递到指定的死信 Exchange。消息变成死信有三种情况:

  • 消息被消费者 basicRejectbasicNack 且 requeue=false。
  • 消息在队列中的存活时间超过了 TTL。
  • 队列满了无法接收新消息(x-max-length 达到上限时,最早的消息变死信)。

配置方式:声明队列时加 x-dead-letter-exchangex-dead-letter-routing-key 参数。

延迟队列的经典实现是”死信队列 + TTL”。创建两个队列——“无消费者的队列 A”和”真正的消费队列 B”。消息先发到队列 A,设置消息 TTL 为延迟时间(如 30 秒)。队列 A 没有消费者,消息在 A 中到期后变成死信,自动转发到死信 Exchange,Exchange 路由到队列 B,B 有消费者正常消费。效果即为消息从发送到实际消费经过了 30 秒的延迟。

死信队列解决的核心场景:失败消息的后续处理(消费多次失败的消息进入死信队列,由人工或定时任务批量处理/补偿)、延迟任务(30 分钟未支付的订单自动取消)、异常消息的旁路(格式错误的消息进入死信队列防止阻塞正常消息队列)。

追问1: RabbitMQ 延迟队列的 TTL 是设在队列上还是消息上?有什么区别?

TTL 可以设在队列上(x-message-ttl,队列中所有消息都有相同的过期时间),也可以设在消息上(expiration 属性,每条消息可以有不同的过期时间)。两者的行为有微妙差异:队列级 TTL——消息从”进入队列”开始计时,到期后立即变死信,但如果它排在队列头部之后,需要前面的消息先到期或消费掉才能被移除——这就是局限性,队列头部消息没到期会把后面的到期消息堵住。消息级 TTL——消息到期后不会立即被移出,需要到达队列头部时才会被判定为死信。所以如果队列中消息的 TTL 不相同,排在后面的短 TTL 消息会被前面的长 TTL 消息堵住——这就是”队头阻塞”。解决方案是使用 RabbitMQ 延迟消息插件(rabbitmq-delayed-message-exchange),它在 Exchange 层面做延迟,不依赖队列顺序。

📌 易错点 / 加分项:

  • 死信队列也是普通的队列,需要对应有消费者消费——否则死信就一直堆在死信队列里
  • 延迟队列插件和 TTL+DLX 方案的效果不同——前者在交换机层延迟、后者在队列层过期
  • 消息变成死信后会丢失原有的 routing key——除非在队列上设 x-dead-letter-routing-key

7. RabbitMQ 镜像队列与 Quorum Queue

题目: RabbitMQ 的镜像队列和 Quorum Queue 有什么区别?它们分别如何保证高可用?在什么场景下选哪个?

💡 答案:

两者都是 RabbitMQ 的高可用方案,但设计理念完全不同。

镜像队列(RabbitMQ 3.x 时代):在一个”master + 多个 mirror”的架构下,master 接收所有读写,然后将每个写操作同步推送给所有 mirror。配置方式:ha-mode 策略设为 allexactly 指定镜像数。如果 master 宕机,存活最久的 mirror 自动升级为新 master。缺点:同步模型效率低(master 必须等待所有 mirror 确认收到才能返回成功,mirror 越多写入越慢)、脑裂风险(网络分区时可能出现两个节点都认为自己是 master)。

Quorum Queue(RabbitMQ 3.8 引入):底层基于 Raft 共识算法。队列的多个副本组成 Raft Group,写入只需要领导者 + 多数派确认即可(不需要全部副本确认),性能优于镜像队列的全同步。Leader 宕机后 Raft 协议自动选举新 Leader,不会有脑裂问题。还支持自动故障检测和快速恢复,使用分段日志存储做持久化优化。但有一些限制:不支持消息优先级、不支持 TTL per-message、不支持 exclusive consumer、消费模式更偏向”拉”(polling)而非”推”。

选型建议:新项目直接上 Quorum Queue——共识协议更可靠、没有脑裂、多数确认性能更好。镜像队列在逐步被取代——RabbitMQ 官方计划在未来版本中弃用镜像队列。如果需要在 Quorum Queue 和 Classic Queue(普通队列)之间选——高可用 > 性能优先选 Quorum Queue(写入延迟稍高但一致性强),吞吐量极致且能容忍短暂服务中断可选 Classic Queue。

📌 易错点 / 加分项:

  • Quorum Queue 的写入延迟比镜像队列高约 10-20%(因为 Raft 协议额外的网络交互),但比”all mirrors”的镜像队列快
  • 镜像队列的”同步模式”在 3.8+ 中默认启用——这意味着如果 master 坏了、mirror 还没同步,则丢消息
  • Quorum Queue 依赖 RabbitMQ 3.8+ 和 Erlang 23+——老版本无法使用

8. RabbitMQ QoS 与消费者预取

题目: RabbitMQ 的 QoS(Quality of Service)机制是什么?prefetchCount 参数如何影响消费者的处理能力和消息分发公平性?

💡 答案:

QoS 在 RabbitMQ 中指消费者端的”预取控制”——控制 Broker 一次可以给某个消费者投递多少条未确认的消息。核心参数是 basicQos(int prefetchCount)——设置该 Channel 上的未确认消息上限。比如 prefetchCount=1 意味着 Broker 一次只给这个消费者投递一条消息,只有这条消息被 Ack 后才会投递下一条。

prefetchCount 对系统行为的影响非常大:

  • 设为 1:可以实现”公平分发”——即使消费者的处理速度不同,处理快的消费者能更快获得下一条消息,避免了”一个累死一个闲死”的不均衡。
  • 设为较大值(如 100):可以提高吞吐——Broker 不需要等待消费者 Ack 才发下一条消息,消息可以源源不断地批量投递过去,减少网络往返。但如果消费者处理速度不一致,设置太大导致处理慢的消费者积压了大量未确认消息,处理快的消费者反而没消息——这就是消息分配不公。
  • 设为 0:表示无限制(Broker 把所有消息尽可能快地推给消费者)。但这是危险的——极端情况下 Broker 把队列中所有消息推到消费者,导致消费者内存 OOM。

调优建议:如果消费者处理消息的耗时基本均匀,设 prefetchCount=50-100 左右可以获得高吞吐。如果消费者处理时间差异大(有的消息处理 10ms、有的 5 秒),设 prefetchCount=1 或较小的值(5-10)可以保证消息均匀分布。

📌 易错点 / 加分项:

  • basicQos 的作用域可以是 Channel 级别也可以考虑 Consumer 级别——basicQos(0, 1, false) 限制单个消费者而非整个 Channel
  • prefetchCount 设得太大 + autoAck=true 是内存泄漏的经典原因——消费者内存中积压了大量未处理的消息
  • 多个消费者场景下,prefetchCount 小的消费者”有权获得更多消息”——因为 Broker 为了公平分发会优先投递给”未确认消息少”的消费者

9. RabbitMQ 集群模式与网络分区

题目: RabbitMQ 集群的几种部署模式(普通集群、镜像集群)在工作原理上有何不同?什么是网络分区(Split-Brain)?RabbitMQ 如何处理它?

💡 答案:

RabbitMQ 集群的元数据(Exchange、Queue、Binding、Vhost 等定义)在所有节点间同步——无论连到哪个节点,都能看到相同的拓扑。但队列内容(消息数据)的存储方式不同。

  • 普通集群:队列内容只存储在创建队列的那个节点上,其他节点只存储元数据 + 指向实际节点的指针。如果你连到 node2 想消费一个位于 node1 的队列中的消息,node2 内部转发请求到 node1 取消息再返回给消费者。节点挂了——那个节点上的所有队列内容不可用。
  • 镜像集群:队列在所有镜像节点上保留完整副本,master 处理读写、mirrors 同步。master 宕机 mirror 自动提升,数据不丢。缺点是同步开销大,写入性能随 mirror 数增加而降低。

网络分区是指集群中部分节点之间网络中断,形成两个或多个无法相互通信的子集群。每个子集群都认为自己是”集群的合法统治者”,各自继续接受客户端连接和消息写入——这就是裂脑。网络分区恢复后,两套数据无法自动合并。RabbitMQ 提供了四种处理模式:

  • ignore:不处理,等待手动恢复(默认)。
  • pause_minority:暂停少数派节点的服务,保证只有多数派继续工作。
  • pause_if_all_down:如果指定节点不可达则暂停。
  • autoheal:网络恢复后自动选择一个获胜方,丢弃另一个的数据。

生产环境建议用 pause_minority——少数派暂停(只读不写),多数派继续正常服务,恢复后没有数据冲突。但要求集群节点数是奇数(如 3 个),否则可能出现 split vote。

📌 易错点 / 加分项:

  • RabbitMQ 集群中所有节点必须运行相同版本的 Erlang 和 RabbitMQ——小版本不一致也可能导致莫名问题
  • pause_minority 只在节点数为奇数时有效——偶数节点时网络分裂可能导致两边节点数相同
  • 命令行 rabbitmq-diagnostics cluster_status 可以查看当前集群状态和分区情况

10. RabbitMQ 消息积压处理

题目: RabbitMQ 队列中消息大量积压时,你会从哪些方面排查和处理?如果不允许丢消息但积压已经影响业务了,怎么最快地消除积压?

💡 答案:

消息积压的排查方向分三层。

消费者侧:先用 rabbitmqctl list_queues name messages_ready messages_unacknowledged consumers 查看积压队列的未消费消息数和消费者数量。如果 consumers=0 说明没有人消费这个队列。如果 consumers > 0 但 messages_ready 持续增长,说明消费者的处理速度低于生产者的生产速度。

队列配置侧:检查 prefetchCount 是否太小(消费者有处理能力但 Broker 不投递足够消息)、是否有消息 TTL 过期造成死信堆积。

代码侧:消费者处理逻辑是否存在慢操作(大 SQL、等待外部 API)、是否频繁异常导致大量 requeue。

快速消积压的策略(不能丢消息的前提下):

  • 临时扩容消费者:起一批新消费者实例,把 prefetchCount 调大,快速消耗积压。
  • 启用多线程处理:一个消费者内用线程池并行处理消息(注意消息确认要在处理成功后才 ack,不能提前)。
  • 紧急转移:把一部分积压消息转移到另一个队列(用 shovel 插件或其他工具),由专门的应急队列用更多消费者处理。
  • 暂停生产者:在业务网关层做限流或熔断,限制发往 RabbitMQ 的消息速率,给消费者争取时间来消化积压。

📌 易错点 / 加分项:

  • 不要盲目增加 prefetchCount——如果消费者内存有限,prefetch 过大反而导致消费者 OOM
  • 增加消费者实例也要注意数据库连接数等下游瓶颈——消费者增加可能把压力从 RabbitMQ 转移到数据库
  • rabbitmqctl list_queues 在生产环境慎用——在队列非常多的时候会消耗大量资源

11. RabbitMQ 与 Kafka 核心区别与选型

题目: RabbitMQ 和 Kafka 在设计哲学上有什么区别?从消息模型、性能特点、适用场景三个维度对比,什么场景该选 RabbitMQ,什么场景该选 Kafka?

💡 答案:

设计哲学是两者最根本的区别。RabbitMQ 基于 AMQP 协议,核心是”智能 Broker + 简单消费者”——Broker 负责复杂的路由、过滤、投递逻辑,消费者被动接收消息。Kafka 的核心是”简单 Broker + 智能消费者”——Broker 就是一个 append-only 日志,消费者自己维护消费位置(offset),自行决定消费什么和怎么消费。

消息模型:RabbitMQ 是”消费后删除”——消息被确认后从 Queue 中移除。Kafka 是”按时间保留”——消息在 Partition 中保留固定时间(如 7 天),消费者可反复回放。RabbitMQ 天然支持”推模式”,Kafka 是”拉模式”。RabbitMQ 的消息优先级、TTL、死信队列等”消息管理”功能远强于 Kafka。Kafka 的消息顺序保证在单个 Partition 内是严格的。

性能:Kafka 的顺序磁盘 IO + 零拷贝 + 分区并行让它在吞吐量上碾压 RabbitMQ——Kafka 单机百万 QPS,RabbitMQ 单机几万 QPS。但 RabbitMQ 的端到端延迟更低(通常 < 1ms),Kafka 的延迟在 5-50ms 量级(受限于批量拉取和磁盘结构)。

选型:RabbitMQ 适合——消息量适中(万级 QPS)、路由逻辑复杂、需要优先级和 TTL、需要”消费确认后删除”的精准消费控制、传统企业应用。Kafka 适合——海量数据(百万级 QPS)、需要消息回放和重放、多消费者组需要独立消费同一条数据流、Event Sourcing / 日志收集 / 大数据管道。

📌 易错点 / 加分项:

  • RabbitMQ 也支持 Pull 模式(basicGet),但性能不如 Push——不是 RabbitMQ 不能用拉模式
  • Kafka 的”消息删除”是时间驱动的(retention),不是消费驱动的——消费者确认不影响消息是否保留
  • 小规模场景下不要被 Kafka 的”高性能”迷惑——Kafka 的运维成本远高于 RabbitMQ

12. RabbitMQ 的 Return 机制与备用交换机

题目: RabbitMQ 中的 Mandatory 标志和 Return 机制是什么?备用交换机(Alternate Exchange)解决了什么问题?

💡 答案:

当生产者发送消息到 Exchange 时,可能出现”消息无法路由到任何 Queue”的情况——routing key 和所有 binding key 都不匹配。默认情况下 RabbitMQ 会静默丢弃这条消息,生产者根本不知道消息丢了。

Mandatory 标志解决了这个问题:生产者在 basicPublish 时设置 mandatory=true,如果消息无法被路由到任何 Queue,Broker 会将消息通过 Basic.Return 返回给生产者,而不是默默丢弃。Return 机制触发的条件有两个:mandatory=true 且消息无法路由到任何 Queue。

**备用交换机(Alternate Exchange / AE)**是解决消息”无路由”问题的另一个方案——在 Exchange 声明时指定 alternate-exchange 参数。当消息无法被路由时,Broker 不丢弃消息也不 Return,而是自动将消息转发到指定的备用交换机。备用交换机可以绑一个”未路由消息队列”,所有无法匹配原始路由的消息都落入这个队列,由监控消费者采样分析或人工复查。这在生产环境中比 Return 机制更可靠——因为 Return 是异步回调,如果生产者没实现 ReturnListener 就漏掉了。

📌 易错点 / 加分项:

  • Mandatory 标志同时关联 Publisher Confirm——mandatory=true 且 confirm 模式下,先触发 Return 再触发 Confirm
  • 备用交换机如果也路由失败(它自己的绑定也匹配不上),消息会被静默丢弃——可以形成一个无限的 AE 链来保证不丢
  • 监控未路由消息队列是发现”配置错误”的最佳手段——生产环境中经常出现 routing key 写错了导致消息丢失

13. RabbitMQ 的 TTL 与队列过期

题目: RabbitMQ 中消息 TTL 和队列 TTL 分别如何配置?消息过期后是立即删除还是惰性删除?如果队列的 TTL 过期了会怎样?

💡 答案:

消息 TTL 有两种设置方式:

  • 队列级 TTL:声明队列时加 x-message-ttl: 60000(单位毫秒),队列中所有消息都有相同的 60 秒过期时间。消息从进入队列开始计时。
  • 消息级 TTL:发送消息时在 properties.expiration 中设置毫秒值,不同消息可以有不同的 TTL。

无论哪种方式,过期的消息不会立即从队列中删除——RabbitMQ 只在以下时机检查并清除过期消息:一是消息到达队列头部即将被投递时检查;二是队列中有消息被消费后检查队列头的下一条。这意味着如果一条过期的消息卡在队列中间(前面有未过期的消息),它不会被立即删除,会一直占据内存。这就是”惰性删除”。

队列 TTL 通过 x-expires 参数设置——如果队列在指定时间内没有任何消费者连接、也没有被重新声明,队列自动删除。队列被重新声明、有消费者连接、或者有 basicGet 操作时会重置 TTL。队列过期后,队列和其中的所有消息都被删除(不管消息是否被消费过)。这个功能常用于”临时队列”——用完一段时间没人用就自动清理掉,避免僵尸队列堆积。

📌 易错点 / 加分项:

  • 消息的惰性删除意味着 TTL 不能用于实现”精确的延迟删除”——如果前面的消息没被消费,后面过期的消息还在
  • Per-message TTL + 队列长度限制可能有矛盾——消息过期了但没到队列头不会被删除,不算”已移除”
  • RabbitMQ 3.8+ 支持 x-queue-type=quorum 的 Quorum Queue——它的消息 TTL 行为可能与 Classic Queue 不同