Skip to content

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

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


【面试题】1. RabbitMQ的镜像队列和仲裁队列有什么区别?生产环境怎么选? ​

【标准答案】镜像队列(Classic Mirrored Queue)是RabbitMQ早期的高可用方案,主队列同步数据到镜像节点,主节点宕机后镜像晋升为主。问题在于:同步是单方向的,镜像节点不参与读写,纯粹是热备;而且队列数据在多个节点完整拷贝,存储开销大;最致命的是——如果镜像同步 lag 严重,主节点挂掉后镜像晋升会丢消息。仲裁队列(Quorum Queue)是3.8+推出的新方案,基于Raft共识协议,所有副本参与写入投票,多数派确认才算写入成功。核心区别:一是仲裁队列天然保证数据一致性,不会出现主从不一致丢消息的问题;二是仲裁队列不再需要人工配置同步策略,Raft协议自动管理;三是仲裁队列的性能更稳定,因为写入路径统一。生产环境选型建议:新项目直接用仲裁队列,官方也明确说仲裁队列是未来方向,镜像队列已经标记为deprecated;老项目迁移时注意仲裁队列不支持一些经典队列特性(比如exclusive、TTL自动过期策略有些差异),需要做兼容性测试。一句话总结:镜像队列是"主从热备",仲裁队列是"Raft共识",后者更可靠但特性略有缩减。

【面试题】2. Kafka的分区策略有哪些?自定义分区器怎么设计?分区数怎么规划? ​

【标准答案】Kafka Producer发送消息时,分区路由决定了消息进哪个分区。默认有三种策略:第一,指定了Partition就直接发;第二,没指定Partition但指定了Key,就对Key做hash取模路由到固定分区,保证相同Key的消息有序;第三,既没指定Partition也没指定Key,就用轮询(Round Robin)或Sticky分区策略均匀分布。自定义分区器实现Partitioner接口,核心逻辑在partition()方法里:比如订单系统按用户ID分区保证同用户消息有序,但又要避免数据倾斜,可以对用户ID做一致性hash而不是直接取模;再比如日志场景按时间分区分天存储方便清理。分区数规划是生产环境的关键决策:分区数决定了并行度,也决定了Consumer Group的最大消费者数。经验公式:分区数 = 目标吞吐量 / 单分区吞吐量。单分区写入一般10MB/s左右,消费5-10MB/s,按业务峰值算就行。但分区数不是越多越好:分区越多,Leader选举和Rebalance时间越长,文件句柄和内存开销也越大。建议单个Broker分区数控制在1000-2000以内,集群总分区数别超过数万。一句话:分区数按吞吐量算,留够余量,但不能无脑加。

【面试题】3. RocketMQ的延迟消息18个等级是怎么设计的?为什么不能支持任意延迟? ​

【标准答案】RocketMQ开源版支持18个固定延迟等级:1s、5s、10s、30s、1m、2m、3m、4m、5m、6m、7m、8m、9m、10m、20m、30m、1h、2h。实现原理:Broker内部为每个延迟等级创建一个专用Topic(SCHEDULE_TOPIC_XXXX),下面18个Queue对应18个等级。Producer发送延迟消息时,Broker先把消息写入对应等级的Queue,然后由定时线程ScheduleMessageService按等级轮询,到时间后把消息从延迟Topic转投到目标Topic。为什么不能支持任意延迟?核心原因是性能:如果支持任意延迟时间,就需要对每条消息做定时调度,海量消息下定时器开销巨大。而固定等级方案把同一等级的消息批量处理,一个Timer轮询一个Queue就行,复杂度从O(N)降到O(18)。阿里云商业版通过TimeWheel时间轮+分层存储支持了任意延迟,但开源版没有这个实现。如果你确实需要任意延迟,有两个变通方案:一是用RabbitMQ的延迟插件(支持任意毫秒级延迟),二是自己基于Redis的ZSet实现延迟队列再对接消息中间件。一句话:固定等级是性能和功能的折中,开源版18个等级覆盖了90%的业务场景。

【面试题】4. Kafka的Offset管理有哪些方式?自动提交和手动提交分别有什么风险?生产环境怎么选? ​

【标准答案】Kafka Consumer的Offset管理分两种:自动提交和手动提交。自动提交(enable.auto.commit=true)是Consumer在后台定时(默认5秒)把当前拉取到的最大Offset提交到__consumer_offsets内部Topic。问题很明显:消息拉取后还没处理完,Offset就提交了,如果这时Consumer挂掉,消息就丢了——这就是"at most once"语义。手动提交分同步提交(commitSync)和异步提交(commitAsync)。同步提交阻塞等待Broker确认,可靠但影响吞吐;异步提交不阻塞,但可能提交失败需要重试。生产环境的标准做法是:关闭自动提交,处理完消息后手动提交,且优先用commitSync保证可靠性;如果追求吞吐,可以用"处理完+异步提交+异步失败转同步"的组合策略。还有一个关键点是Offset重置策略(auto.offset.reset):earliest从最早开始消费,latest只消费新消息,none直接报错。生产环境建议用earliest避免丢消息,配合监控消费Lag。另外手动提交时要注意分区粒度——可以按分区提交Offset(commitSync(Map<TopicPartition, OffsetAndMetadata>)),而不是一次性提交所有分区,这样某个分区处理失败不会影响其他分区的进度。一句话:生产环境关闭自动提交,业务处理完再手动提交,宁可重复消费也不能丢消息。

【面试题】5. RabbitMQ的ACK/NACK/Reject机制怎么用?NACK后消息重回队列会有什么问题? ​

【标准答案】RabbitMQ消费确认机制是消息可靠性的关键环节。ACK表示消息处理成功,Broker可以删除消息;NACK表示消息处理失败,可以设置requeue=true让消息重回队列;Reject类似NACK但只能处理单条消息。NACK重回队列的风险很大:如果消息处理失败的原因是代码bug或外部服务不可用,重回队列的消息会立刻被重新投递,然后又失败、又重回,形成无限循环——消费者线程被反复占用,正常消息反而消费不了,系统直接被"毒消息"卡死。生产环境的正确做法:第一,NACK设置requeue=false,让消息进入死信队列(DLX),人工或自动处理死信;第二,配合x-message-ttl设置队列TTL,避免死信队列无限增长;第三,对于可重试的临时性故障,可以在消费端自己实现重试计数器,超过最大重试次数再进死信队列。Spring AMQP的RetryInterceptor就是干这个的——本地重试N次后再Reject进DLX。还有一个细节:手动ACK模式下,如果不ACK也不NACK,消息会一直处于Unacked状态,Consumer断开连接后才会重回队列,这其实是一种隐式的"重回"。所以一定要确保每条消息都有明确的确认或拒绝,别让消息卡在Unacked状态。一句话:NACK+requeue=false+DLX是标准方案,别让毒消息循环卡死系统。

【面试题】6. Spring Kafka和Spring AMQP在项目中怎么集成?有什么最佳实践? ​

【标准答案】Spring Kafka集成核心是KafkaTemplate发送消息、@KafkaListener消费消息。关键配置:Producer端的acks=all+retries=Integer.MAX_VALUE+enable.idempotence=true保证发送可靠性;Consumer端关闭自动提交(enable.auto.commit=false),在@KafkaListener方法里手动确认Acknowledgment.acknowledge()。最佳实践:第一,用@KafkaListener的containerFactory属性配置不同的并发度和错误处理策略,不同业务用不同的ContainerFactory;第二,消费异常用DefaultErrorHandler替换过时的SeekToCurrentErrorHandler,配置重试次数和死信Topic;第三,Producer用KafkaTemplate的回调确认发送成功,配合CompletableFuture.get()或回调处理失败重发。Spring AMQP集成核心是RabbitTemplate和@RabbitListener。关键配置:Publisher Confirm机制(RabbitTemplate.setConfirmCallback)、Consumer手动ACK(@RabbitListener的ackMode=MANUAL)。最佳实践:第一,Publisher Confirm必须开,同步确认用RabbitTemplate.invoke()包装,异步确认用ConfirmCallback;第二,@RabbitListener配合自定义ErrorHandler处理消费异常,别让异常导致消息无限重试;第三,连接池配置要合理——每个@RabbitListener默认创建一个Channel,高并发场景下要调大connection-cache-size。两者共同的最佳实践:消息体用JSON序列化(Jackson2JsonMessageConverter/StringJsonMessageConverter),消费端做好幂等,监控消费Lag/Unacked消息数,生产环境必须配死信兜底。一句话:框架只是工具,可靠性配置和错误处理策略才是核心。

【面试题】7. Kafka的消息写入流程是怎样的?批量发送和压缩是怎么配合的? ​

【标准答案】Kafka Producer的写入流程分五步:第一步,Producer调用send()方法,消息进入RecordAccumulator缓冲区,按TopicPartition分组;第二步,RecordAccumulator内部为每个Partition维护一个双端队列(Batch Queue),消息追加到对应的ProducerBatch中;第三步,当Batch满了(batch.size默认16KB)或者等待时间到了(linger.ms默认0ms),Sender线程被唤醒;第四步,Sender线程把Batch按Broker分组,每个Broker一个请求批量发送,Broker收到后追加到本地Log;第五步,Broker返回ACK,Producer执行回调。批量发送的核心参数:batch.size控制单个Batch最大值,linger.ms控制等待时间——两者配合实现"满了就发,没满等一会再发"。比如batch.size=32KB,linger.ms=10ms,意思是消息攒到32KB就发,如果10ms内没攒够也发,不无限等。压缩是在Batch级别做的:Producer设置compression.type(snappy/lz4/zstd/gzip),整个Batch被压缩后发送,Broker存压缩数据,Consumer拉取后解压。关键点:压缩是在Batch级别的,所以Batch越大压缩比越高——这也是为什么调大batch.size和linger.ms能同时提升吞吐和压缩效果。但注意压缩是CPU换带宽,如果CPU是瓶颈就别开压缩或用轻量的snappy/lz4。一句话:写入流程是"攒Batch→按Broker分组→批量发→压缩传输",调好batch.size和linger.ms是性能优化的第一步。

【面试题】8. RocketMQ的消息过滤(Tag和SQL92)是怎么实现的?性能影响大吗? ​

【标准答案】RocketMQ支持两种消息过滤方式:Tag过滤和SQL92过滤。Tag过滤是基础方案:Producer发送消息时指定Tag(如"TagA"),Consumer订阅时用"Topic||TagA"指定只消费特定Tag。实现原理:Broker端在ConsumeQueue中存储了Tag的hashCode(8字节),Consumer拉取消息时先在Broker端用hashCode做一次粗筛(不需要读CommitLog),匹配的消息再回Consumer端做Tag字符串精确比对。性能影响极小,因为ConsumeQueue是顺序读+hash比对,几乎不增加开销。SQL92过滤是高级方案:Consumer订阅时可以用SQL表达式,比如"a > 5 AND b = 'xyz'",Producer发送消息时通过putUserProperty设置属性。实现原理:Broker端需要读取消息的属性数据做SQL解析和计算,这个开销比Tag过滤大,因为要解析消息体中的属性。性能影响:SQL92过滤需要Broker加载消息属性做计算,比Tag过滤慢,但比Consumer端全量拉取再过滤好得多——毕竟网络传输量少了。生产建议:Tag过滤覆盖90%场景,优先用Tag,简单高效;需要多条件组合过滤时用SQL92,但要注意索引——属性是存在CommitLog中的,过滤时Broker需要读CommitLog,属性越多IO越重。另外Tag过滤有个限制:一个Consumer只能订阅一个Topic下的一个或多个Tag,不能做OR/AND组合,复杂过滤逻辑必须用SQL92。一句话:Tag过滤是性价比之王,SQL92是功能补充,能用Tag就用Tag。

【面试题】9. Kafka集群扩容增加Broker后,怎么做分区迁移和负载均衡?有哪些坑? ​

【标准答案】Kafka扩容加Broker后,新Broker是空的——现有Topic的分区Leader和副本都在老Broker上,新Broker不自动接管任何负载。必须手动做分区迁移(Partition Reassignment)。操作方式:第一,生成迁移方案,用kafka-reassign-partitions.sh的--generate生成建议方案,或手动编写JSON指定哪些分区迁移到哪些Broker;第二,执行迁移,用--execute启动迁移任务;第三,验证迁移完成,用--verify检查状态。关键参数:迁移过程中Broker间会复制数据,要控制迁移速度避免影响在线业务——通过设置replica.fetcher.size、replica.fetch.max.bytes和动态配置follower.replication.throttled.rate限制迁移带宽。常见坑:第一,迁移期间Leader可能切换,消费者会短暂Rebalance,建议低峰期操作;第二,迁移完成后别忘了检查Preferred Leader是否生效,用kafka-preferred-replica-election.sh让Leader均匀分布;第三,大分区(几百GB)迁移耗时很长,要监控迁移进度,别中途放弃导致分区副本不一致;第四,迁移不是"移动"而是"复制+删除",先在新Broker建副本同步数据,同步完再删老副本,期间磁盘占用会临时翻倍;第五,Cruise Control是自动化分区均衡的好工具,能基于磁盘、CPU、网络流量自动规划迁移方案,大规模集群推荐使用。一句话:扩容后必须手动迁移,控制速度、低峰操作、验证完成、别忘了Preferred Leader。

【面试题】10. Pulsar和Kafka的区别是什么?Pulsar的分层架构有什么优势? ​

【标准答案】Pulsar是Yahoo开源的分布式消息系统,和Kafka最大的架构区别是"存算分离"——Kafka的Broker既负责消息收发又负责消息存储,Pulsar把计算层(Broker无状态)和存储层(BookKeeper)分开了。核心区别:第一,架构上,Kafka Broker直接读写本地磁盘,Pulsar Broker只做消息路由和分发,数据持久化交给BookKeeper的Ledger;第二,扩容上,Kafka扩Broker后要手动迁移分区(上一题讲过),Pulsar Broker无状态,加Broker直接接管流量,秒级扩容;第三,多租户上,Pulsar原生支持多租户(Tenant→Namespace→Topic),Kafka多租户靠Topic命名约定和ACL模拟;第四,消息模型上,Pulsar统一了队列和流模型——同一个Topic既能做独占消费(Queue模式)又能做故障切换/共享消费(Stream模式),Kafka只有流模型;第五,订阅模式上,Pulsar支持四种:Exclusive、Failover、Shared、Key_Shared,Kafka只有Consumer Group一种;第六,延迟消息上,Pulsar原生支持任意延迟,Kafka不支持。但Pulsar也有劣势:社区规模不如Kafka,生态工具少,运维复杂度高(要同时管Broker和BookKeeper两层),学习曲线陡峭。选型建议:如果需要多租户、延迟消息、分层存储、秒级扩容这些特性,且团队有能力运维BookKeeper,选Pulsar;如果追求稳定性、社区支持和生态成熟度,Kafka依然是首选。一句话:Pulsar架构更先进,Kafka生态更成熟,选哪个看团队能力和业务需求。