Skip to content

本轮 消息中间件面试题库(带答案)

【面试题】1. RabbitMQ 中 Direct Exchange 和 Topic Exchange 的区别是什么?各自适用什么场景? ​

【标准答案】Direct Exchange 是精确匹配路由键,消息的 routing key 必须和绑定时指定的 routing key 完全一致才能路由到对应队列。比如绑定时指定 routing key = "order.create",那消息只有 routing key 也是 "order.create" 才会被投递。适合点对点的精确路由场景,比如日志按级别分发、任务按类型分配。

Topic Exchange 支持通配符匹配,routing key 可以用 *(匹配一个单词)和 #(匹配零个或多个单词)。比如绑定时指定 "order.*" 可以匹配 "order.create"、"order.cancel","order.#" 可以匹配 "order.create.success"、"order.cancel.fail" 等多级路由。适合多维度分类场景,比如日志系统按 "facility.severity" 分发、订单按 "模块.操作.状态" 路由。

面试关键点:Direct 是等值匹配,Topic 是模式匹配;Topic 的 routing key 必须用 . 分隔的单词列表;性能上 Direct 略优,Topic 需要做模式匹配但差距可忽略;选择依据是需不需要灵活的层级路由。

【面试题】2. Kafka 的 ISR 机制是什么?为什么需要 ISR?和副本同步有什么关系? ​

【标准答案】ISR(In-Sync Replicas)是 Kafka 中与 Leader 副本保持同步的副本集合。每个 Partition 有一个 Leader 和多个 Follower,Follower 持续从 Leader 拉取数据。只有跟得上 Leader 进度的 Follower 才会留在 ISR 中,如果 Follower 落后太多(超过 replica.lag.time.max.ms,默认 10 秒没有拉取请求或落后太多),就会被踢出 ISR。

ISR 的核心价值是平衡可靠性和可用性。Kafka 的 acks 机制依赖 ISR:acks=all 时,消息必须被 ISR 中所有副本确认才算写入成功,这样就保证了至少 ISR 中的副本都有这条消息。如果 Follower 挂了但不影响 ISR 中的其他副本,Leader 依然可以正常服务。如果 Leader 挂了,只会从 ISR 中选举新 Leader,确保新 Leader 拥有全部已确认的消息,不会丢数据。

和普通副本同步的区别:不是所有 Follower 都能参与选举,只有 ISR 中的副本才有资格。这避免了"落后副本当选 Leader 导致数据丢失"的问题。但 ISR 缩小到只剩 Leader 时,可用性风险增大,所以要在可靠性和可用性之间权衡——生产上通常 min.insync.replicas 设为 2,保证至少两个副本确认。

【面试题】3. RocketMQ 的 CommitLog 和 ConsumeQueue 分别是什么?为什么要这样设计? ​

【标准答案】CommitLog 是 RocketMQ 的物理存储文件,所有 Topic 的消息顺序写入同一个 CommitLog,每条消息追加写入,不按 Topic 分文件。ConsumeQueue 是逻辑消费队列,每条记录固定 20 字节(8 字节 CommitLog 偏移量 + 4 字节消息大小 + 8 字节 Tag 的 hashCode),相当于 CommitLog 的索引。

这样设计的好处有三点:第一,CommitLog 顺序写,所有 Topic 共享一个写入点,磁盘 IO 效率极高,避免了多 Topic 多文件随机写的问题。第二,ConsumeQueue 固定长度记录,消费时可以像数组一样按偏移量直接定位,O(1) 复杂度随机访问,性能极好。第三,消息体和索引分离,ConsumeQueue 极小可以常驻内存,消费端读取时先查 ConsumeQueue 拿到偏移量,再去 CommitLog 取完整消息,绝大部分读操作命中 PageCache,基本无磁盘 IO。

面试加分点:这是典型的"写放大换读性能"的设计,和 Kafka 每个 Partition 独立文件的设计思路不同——Kafka 依赖 PageCache 和顺序读写,RocketMQ 依赖 CommitLog 顺序写 + ConsumeQueue 索引加速。RocketMQ 的方案在 Topic 数量非常多时优势更明显,因为 Kafka 的 Partition 文件数会随 Topic 数量线性增长,而 RocketMQ 只有 CommitLog 一个大文件。

【面试题】4. 消息队列如何保证消息的顺序性?不同中间件的实现方案有什么区别? ​

【标准答案】消息顺序性分两个层面:全局有序和分区有序。全局有序要求所有消息严格按发送顺序消费,吞吐量极低,几乎不用。实际生产中追求的是分区有序——同一业务 Key 的消息保证有序。

RabbitMQ 的方案:单队列天然有序,一个 Queue 只有一个 Consumer 消费就是有序的。多 Consumer 时用 Single Active Consumer 特性,同一时刻只有一个消费者活跃。但牺牲了并发度。实际场景中,按业务 Key 路由到同一 Queue,每个 Queue 单 Consumer,是常见的有序方案。

Kafka 的方案:同一个 Partition 内消息严格有序,生产端指定相同的 Key 或自定义 Partitioner 将同一业务的消息发到同一 Partition,消费端每个 Partition 只由组内一个 Consumer 消费,天然保证分区有序。如果需要全局有序,设 Partition 数为 1(不推荐)。

RocketMQ 的方案:支持全局顺序消息和分区顺序消息。分区顺序消息通过 MessageQueueSelector 按业务 Key Hash 选队列,同一 Key 的消息进同一 Queue,单 Queue 单线程消费。全局顺序消息则是单 Topic 单 Queue。RocketMQ 还通过分布式锁和 Broker 端的队列锁定,确保故障恢复时顺序不乱。

面试关键:顺序性的代价是并发度降低,需要在顺序性和吞吐量之间权衡;分区有序是实用方案,全局有序是理论方案;保证顺序性还要考虑消费端是否重试、重平衡时是否会乱序,这些都要处理。

【面试题】5. RabbitMQ 的死信队列(DLX)是什么?消息什么情况下会进入死信队列? ​

【标准答案】死信队列(Dead Letter Exchange)是 RabbitMQ 处理"无法正常消费的消息"的机制。当消息变成"死信"后,不会被直接丢弃,而是被路由到绑定了 DLX 的死信队列中,供后续排查或重新处理。

消息进入死信队列有三种情况:第一,消费者显式 NACK 或 Reject 消息且设置 requeue=false;第二,消息在队列中存活时间超过 TTL(可以通过队列的 x-message-ttl 参数或消息本身的 expiration 属性设置);第三,队列达到最大长度(x-max-length),超出的最早消息会被挤到死信队列。

配置方式:声明队列时设置 x-dead-letter-exchange 和 x-dead-letter-routing-key 参数指定死信交换机和路由键。死信交换机可以是任何类型的 Exchange,通常用 Direct Exchange 配合专门的死信队列。

实际应用:延迟队列的经典实现就是 TTL + DLX,消息设 TTL 后不设消费者,到期自动进死信队列,死信队列的消费者处理延时逻辑。另外,死信队列也是消息消费失败后的兜底方案,可以对死信做人工介入、告警、或重试。

面试加分:注意 TTL + DLX 做延迟队列的坑——队列先进先出特性导致短 TTL 消息可能被长 TTL 消息阻塞,解决方案是不同延迟等级用不同队列,或者用 rabbitmq_delayed_message_exchange 插件。

【面试题】6. Kafka 的 Exactly-Once 语义如何实现?幂等 Producer 和事务分别解决了什么问题? ​

【标准答案】Kafka 的 Exactly-Once 分三个层面理解:

第一层,幂等 Producer(enable.idempotence=true)。Producer 为每条消息分配递增的 Sequence Number,Broker 端对每个 <PID, Partition> 维护最新的 Sequence Number,重复消息(相同序列号)直接丢弃。这解决了 Producer 重试导致的消息重复问题,但仅保证单 Partition 幂等,跨 Partition 无效,且重启后 PID 变化,无法跨会话幂等。

第二层,Kafka 事务(transactional.id + isolation.level=read_committed)。Producer 可以将多条消息的发送和 Offset 提交放在一个事务中,要么全部成功要么全部回滚。Consumer 设置 isolation.level=read_committed 后,只会读到已提交的事务消息。这解决了"消费-处理-生产"链路中的原子性问题,经典场景是 Consume-Transform-Produce 模式,消费 A Topic 处理后写 B Topic,事务保证消费位移和生产消息的原子性。

第三层,端到端 Exactly-Once。需要 Kafka 事务 + 业务逻辑幂等的配合。Kafka 只保证 Kafka 内部的 Exactly-Once,外部系统(数据库、Redis)的写入还需要业务层做幂等。

面试关键:幂等 Producer 解决单分区重复发送,事务解决跨分区原子性,端到端 Exactly-Once 还需要业务幂等;事务的性能开销不小,非必要不用;Consumer 端的 read_committed 模式会缓存未提交事务的消息,有额外的内存和延迟开销。

【面试题】7. 消息积压了怎么办?从发现到恢复的完整处理思路是什么? ​

【标准答案】消息积压是生产环境的常见问题,处理思路分三步:

第一步,紧急止损。先看积压量级和增长速度,如果持续快速积压,优先限制生产端发送速率——如果是业务峰期,可以在网关层限流或临时关闭非核心消息源;同时增加消费端并发度——加 Consumer 实例、增大 Consumer 的并发线程数(RabbitMQ 增加 prefetch count + 增加消费者,Kafka 增加 Partition 数 + Consumer 实例,RocketMQ 增加消费者实例)。如果 Consumer 有性能瓶颈,排查是 DB 慢查询、外部服务超时还是消费逻辑太重。

第二步,快速消费。如果积压量太大,常规消费速度跟不上,考虑临时方案:新建一个临时 Topic/Queue,把积压消息快速转储过去,用一批只做"转储"的轻量消费者快速搬走积压消息,然后再用多个消费者并行消费临时队列。也可以临时将消费逻辑简化——比如原来是"消费后写 DB 再发下游",积压时临时改为"消费后只写 DB",下游消息等积压处理完再补发。

第三步,根因排查和长效治理。积压原因通常是:消费端性能不足(DB 慢、下游超时)、消费端异常重启(反复重试拖慢整体)、Producer 突发流量(大促、批处理任务)、Consumer 配置不当(prefetch 太大、线程数太少)。对应措施:增加监控告警(队列深度、消费延迟 lag)、消费者加熔断和降级、生产端加限流和背压、做好容量规划。

面试加分:Kafka 的积压看 Consumer Lag 指标,可以用 kafka-consumer-groups.sh 监控;RocketMQ 可以用 mqadmin 查看消费进度;RabbitMQ 看 Queue 深度。提前设好告警阈值比事后处理更重要。

【面试题】8. RabbitMQ 的镜像队列和仲裁队列有什么区别?生产环境推荐用哪个? ​

【标准答案】镜像队列(Classic Mirrored Queue)是 RabbitMQ 3.x 早期的 HA 方案,主队列在 Master 节点,数据同步到 Mirror 节点,同步方式是所有操作(写入、确认、消费)都必须在所有镜像上完成才算成功。Master 挂了,最早加入的 Mirror 升级为新 Master。问题是:同步写性能差,扩展性不好;镜像队列的同步机制导致网络分区时可能出现数据不一致;Master 故障切换后,原 Master 恢复时需要全量同步,耗时很长。

仲裁队列(Quorum Queue)是 RabbitMQ 3.8+ 引入的基于 Raft 共识算法的新方案。数据以 Raft Log 形式在多个节点间复制,写入只需多数节点确认(而不是全部),读操作由 Leader 处理。优势:基于 Raft 协议保证了强一致性;网络分区时自动保障多数派,不会脑裂丢数据;故障恢复用增量同步,比全量同步快得多;支持死信策略、TTL、队列长度限制等特性。

生产环境推荐:RabbitMQ 官方明确推荐使用仲裁队列替代镜像队列。镜像队列在 RabbitMQ 3.13 中已标记弃用,4.0 版本彻底移除。新项目直接用仲裁队列,老项目迁移也建议尽早切换。但注意仲裁队列不支持一些经典队列的特性(如 exclusive 队列、部分插件兼容性),迁移前需确认。

【面试题】9. Kafka 的零拷贝和 RocketMQ 的 mmap 分别是什么?怎么提升消息中间件的 IO 性能? ​

【标准答案】零拷贝(Zero-copy)是 Kafka 用的高效数据传输技术。传统方式:磁盘数据 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡,4 次拷贝 + 2 次上下文切换。Kafka 使用 Java 的 FileChannel.transferTo() 底层调用 sendfile 系统调用,数据从磁盘直接到网卡,只需 2 次拷贝(磁盘到内核 PageCache,内核到网卡),零次用户态拷贝。配合 Kafka 的批量压缩和顺序写,消费者拉取消息时几乎不经过用户态,IO 效率极高。

mmap(Memory Mapped File)是 RocketMQ 用的技术。通过 mmap 系统调用将文件映射到用户态内存地址空间,用户程序直接读写内存就等于读写文件,省去了用户态和内核态之间的数据拷贝。RocketMQ 的 CommitLog 通过 mmap 映射后,写入消息就是写内存,由操作系统异步刷盘。读取时 ConsumeQueue 索引很小,基本命中 PageCache,再去 CommitLog 取消息时也大概率命中 mmap 映射的 PageCache。

两者对比:sendfile(零拷贝)适合"磁盘到网络"的直传场景,不需要在用户态处理数据,Kafka 的消费拉取就是这种场景。mmap 适合"磁盘到用户态"的随机读取场景,RocketMQ 的 CommitLog 读写都经过用户态(因为要做消息过滤、Tag 匹配等逻辑),所以用 mmap 更合适。两者都依赖操作系统的 PageCache,本质都是减少不必要的 CPU 拷贝。

面试关键:不是"零拷贝比 mmap 好"或反过来,而是场景不同。Kafka 消费者是拉模式,Broker 直接把文件数据发到网络,走 sendfile 最优;RocketMQ 消费者也需要拉模式,但 Broker 端需要根据 Tag 做过滤,必须在用户态处理数据,所以用 mmap。

【面试题】10. 如果让你设计一个百万级 TPS 的消息系统,你会怎么设计?从架构到存储到高可用都说一下。 ​

【标准答案】百万级 TPS 是个大工程,我从四个层面说:

架构层面:采用分区(Partition/Shard)+ 水平扩展的架构。Topic 按业务拆分,每个 Topic 设多个 Partition,Partition 分布在不同 Broker 上。Broker 无状态化,只负责存储和转发,元数据交给独立的协调服务(Kafka 用 ZooKeeper/KRaft,RocketMQ 用 NameServer)。Client 端直连 Broker,不做代理转发,减少一跳网络开销。支持批量发送和压缩(LZ4/Zstd),网络请求从 N 条变成 1 条批量请求,TPS 瞬间提升数倍。

存储层面:顺序写是核心——所有 Partition 的写入都是追加写,避免随机 IO。依赖 PageCache 做读写缓冲,写入先到 PageCache,异步刷盘。读取消费也优先命中 PageCache。大文件分段存储,过期数据按时间或大小删除旧段,避免文件无限膨胀。对于极高吞吐场景,用异步刷盘 + 主从同步复制保证可靠性;对可靠性要求更高的场景,用同步刷盘。零拷贝(sendfile)或 mmap 减少用户态拷贝。

高可用层面:每个 Partition 多副本(通常 3 副本),一主两从。主负责读写,从异步或半同步复制。ISR 机制保证只有同步完的副本才能参与选举。Controller/NameServer 负责故障检测和 Leader 切换,秒级自动恢复。跨机房部署时,可以做一个机房的副本做灾备,但跨机房延迟会影响吞吐,通常用异步复制。客户端也要做重试和故障转移。

性能调优层面:Partition 数量要合理,太多增加文件句柄和内存开销,太少并发度不够,一般按 TPS 目标和单 Partition 吞吐估算(Kafka 单 Partition 约 5-10 万 TPS)。批量大小调优(batch.size 和 linger.ms),在延迟和吞吐间平衡。消费者并发度匹配 Partition 数。JVM 堆不要太大(给 PageCache 留空间),用 G1 或 ZGC 减少 GC 停顿。网卡用万兆,磁盘用 SSD 或 NVMe。

总结:百万级 TPS 的核心不是某个黑科技,而是"分区水平扩展 + 顺序写 + PageCache + 批量压缩 + 副本容错"这套组合拳,每个环节都做到极致,整体才能到百万级。