Skip to content

消息中间件面试题库 ​

生成时间:2026-05-21 22:23


【面试题】1. RabbitMQ的四种Exchange类型分别适合什么场景?请结合实际业务举例说明。 ​

【标准答案】四种Exchange:Direct——路由键精确匹配,适合点对点投递,比如订单系统只把消息发给"order.create"绑定的队列;Fanout——广播到所有绑定队列,不看路由键,适合比如用户注册后同时通知短信服务、邮件服务、积分服务;Topic——路由键支持通配符匹配(*匹配一个词、#匹配零或多个词),适合日志系统按"order.payment.success"这种层级路由,消费者可以用"order.#"订阅所有订单日志;Headers——根据消息头属性匹配而非路由键,性能差、用得少,基本上只有在路由键不够表达复杂条件时才考虑,比如需要同时匹配messageType=A且region=east。面试重点说前三种,Headers一笔带过就行。

【面试题】2. Kafka的ISR机制是什么?为什么需要ISR而不是简单的主从同步? ​

【标准答案】ISR(In-Sync Replicas)是Kafka中与Leader保持同步的副本集合。Leader维护一个ISR列表,只有ISR中的副本才有资格被选为新Leader。核心逻辑:Follower从Leader拉取数据,如果Follower落后太多(超过replica.lag.time.max.ms),就会被踢出ISR。为什么不用简单主从?因为简单主从要么等所有副本确认(一个慢节点拖垮吞吐),要么只等Leader确认(一个副本宕机就可能丢数据)。ISR是折中方案:acks=all时只等ISR内的副本确认,既保证数据不丢(ISR内都有),又不会因为一个慢节点阻塞写入。面试时点出"ISR本质是可用性和一致性的动态平衡"就到位了。

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

【标准答案】RocketMQ所有主题的消息顺序写入同一个CommitLog(顺序写,极致性能),ConsumeQueue是CommitLog的索引队列,每条记录只有固定20字节(8字节偏移量+4字节消息大小+8字节Tag哈希)。分开的好处:第一,CommitLog顺序写,写入性能拉满,不需要按Topic分文件随机写;第二,ConsumeQueue极其精简,一页4K能放200条索引,消费者读取效率极高,基本全命中PageCache;第三,不同消费组各自维护消费进度,互不影响,天然支持多Topic多消费组的并发消费。简单说就是"写扩散读聚合"——写入时只管往CommitLog追加,读取时通过ConsumeQueue快速定位。这也是RocketMQ单机吞吐能到10万+的核心原因之一。

【面试题】4. 消息中间件如何保证消息的顺序性?跨队列/分区的全局有序和分区有序分别怎么实现? ​

【标准答案】分区有序:把需要保证顺序的消息发到同一个分区/队列就行。Kafka用相同的Key做分区路由,RabbitMQ用相同路由键路由到同一队列,RocketMQ用MessageQueueSelector指定队列。同一个分区内消息天然有序,单线程消费即可保序。全局有序:只设一个分区/一个队列,所有消息串行写入、串行消费。代价就是吞吐极低,一般不推荐。实战建议:99%场景用分区有序就够了,比如订单的创建→支付→发货,用订单ID做分区键,同一个订单的消息一定在同一分区。只有在像binlog同步这种严格要求全局有序的场景才用单分区方案。面试时强调"业务上多数只需要业务键级别的局部有序"。

【面试题】5. 消费端如何实现幂等性?请对比三种主流方案的优缺点。 ​

【标准答案】方案一:唯一ID+数据库去重表。消费前查去重表,不存在则插入并处理,存在则跳过。优点是强一致、可靠;缺点是数据库是瓶颈,高并发下性能差,且需要额外维护去重表。方案二:唯一ID+Redis的SETNX。用消息ID做key,SETNX成功则处理,失败则跳过,设过期时间自动清理。优点是高性能;缺点是Redis宕机可能重复消费,需要考虑Redis自身的一致性。方案三:业务状态机判重。比如订单状态只能是 待支付→已支付→已发货,如果消费到"已支付"消息时订单已经是"已发货",直接跳过。优点是不依赖外部组件,天然幂等;缺点是只适用于有状态机的业务。实战推荐:优先用业务状态机(最优雅),没有状态机的场景用Redis去重(高性能兜底),对一致性要求极高的用数据库去重表。

【面试题】6. Kafka的Exactly-Once语义是怎么实现的?为什么说它是端到端的? ​

【标准答案】Kafka的Exactly-Once分两层:幂等Producer——给每个Producer分配PID,每条消息带Sequence Number,Broker对<PID, Partition, SeqNum>去重,保证单分区内不重复。但这只能防生产端重试导致的重复。事务机制——引入Transaction Coordinator,将消息写入和Offset提交放在同一个事务中,要么全成功要么全回滚。典型场景:消费Topic A → 处理 → 写入Topic B + 提交Offset,这两个操作原子完成。为什么说端到端:因为Kafka同时控制了输入端(消费Offset提交)和输出端(消息写入),两者在一个事务里原子完成,所以是Kafka集群内的端到端Exactly-Once。注意,如果下游是MySQL/Redis,Kafka事务管不了,还需要消费端自己做幂等。

【面试题】7. RabbitMQ如何实现延迟队列?两种方案的区别和适用场景是什么? ​

【标准答案】方案一:TTL+DLX。消息设置TTL过期,过期后进入死信交换机(DLX),DLX绑定到实际消费队列。可以在普通队列上设TTL,也可以在单条消息上设TTL。注意陷阱:队列TTL是先入先出——队列头消息没过期,后面过期了也排着等,导致延迟不准。单条消息TTL也有问题——消息过期是在投递时判断的,不是实时的。方案二:rabbitmq_delayed_message_exchange插件。消息发到延迟交换机时带着x-delay头,交换机不立即路由,等延迟时间到了再投递。优点是延迟精确、配置简单;缺点是消息存在Mnesia(内置数据库),大量延迟消息会占用内存,不适合超大规模场景。选型:少量延迟消息用插件(简单精准),海量延迟消息用TTL+DLX(但要接受一定延迟误差)或直接换RocketMQ的延迟消息。

【面试题】8. 消息积压了怎么办?请给出从应急到根治的完整处理方案。 ​

【标准答案】第一步:应急扩消费能力。最快的办法是增加消费者实例数,但要注意不能超过分区/队列数(Kafka一个分区同一时刻只能被一个消费者消费,超过没用)。如果分区数不够,先紧急扩分区。RabbitMQ可以增加队列消费者并发度(prefetch调大+增加consumer线程)。第二步:临时转储快速消化。如果积压量太大(几百万上千万),新建一个临时Topic/Queue,用快速消费者把积压消息批量转储下来,再慢慢消化,避免影响正常消息的实时消费。第三步:排查根因。是消费逻辑慢(DB慢查询?外部API超时?),还是消费端有Bug导致反复重试,还是生产端突发流量。针对性优化:加索引、加缓存、异步化非核心逻辑。第四步:长期治理。设置积压监控告警、消费者健康检查、生产端限流。面试重点是把"应急—排查—根治"三步说清楚。

【面试题】9. Kafka零拷贝和RocketMQ的mmap分别是什么?为什么能提升性能? ​

【标准答案】Kafka零拷贝:用sendfile()系统调用,数据从磁盘读取后直接通过DMA传到网卡,不需要经过用户态内存拷贝。路径:磁盘→内核PageCache→网卡(2次DMA,0次CPU拷贝)。前提是消费时不需要修改消息内容,直接把磁盘数据转发出去。RocketMQ的mmap:用mmap()将CommitLog文件映射到用户态内存地址空间,写入时直接写内存,由操作系统异步刷盘;读取时直接读映射内存,不需要read()系统调用。路径:用户态直接操作映射内存→OS异步刷盘。对比:零拷贝适合"读后直接转发"场景(Kafka的日志分发就是这种),mmap适合"读写都需要"的场景(RocketMQ既要写入又要按索引读取)。本质都是减少用户态和内核态之间的数据拷贝,降低CPU开销。

【面试题】10. 如果你来设计一个百万级TPS的消息系统,核心架构你会怎么设计? ​

【标准答案】存储层:顺序写+PageCache是基石,CommitLog顺序追加写入(不管是Kafka还是RocketMQ都是这个思路)。配合mmap或零拷贝减少拷贝开销。数据分层——热数据走内存,温数据走PageCache,冷数据落盘归档。分区/分片:水平扩展的基本单位,分区数 = 并行度。百万TPS至少需要几十到上百个分区,分散到多台Broker。生产端:批量发送+压缩(LZ4/Zstd),减少网络IO次数。异步发送+内存池复用,避免GC压力。消费端:分区级并行消费,消费者组水平扩展。Offset管理用异步提交减少阻塞。高可用:多副本+ISR机制,Leader故障秒级切换。Broker间数据迁移支持在线扩缩容。关键数字:单分区TPS上限约1-2万(取决于消息大小和磁盘),百万TPS需要50-100个分区,10-20台Broker。面试时把"存储是瓶颈→顺序写是解法→分区是扩展手段→批量压缩是加速器"这条主线讲清楚。