Skip to content

消息中间件面试题库 ​

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


【面试题】1. Kafka的ISR机制是什么?为什么需要ISR而不是简单地用所有副本参与写入确认? ​

【标准答案】ISR(In-Sync Replicas)是Kafka中与Leader保持同步的副本集合。Leader维护一个动态ISR列表,只有ISR中的副本才被认为数据是完整的。

为什么不能直接用所有副本?核心原因是可用性。假设acks=all,如果某个Follower因为网络抖动或磁盘故障迟迟跟不上,Leader就要一直等它,整个写入就被卡住了。ISR机制就是解决这个问题——落后太多的Follower会被踢出ISR,Leader只等ISR内的副本确认就行,兼顾了可靠性和可用性。

具体来说,Follower超过replica.lag.time.max.ms(默认10秒)没拉取数据就会被移出ISR;等它追上了再移回来。这样新增副本不会拖慢写入,故障副本也不会阻塞生产者。

面试中还可以提一句:min.insync.replicas配合acks=all是Kafka可靠性的黄金搭档,比如3副本集群设min.insync.replicas=2,允许1个副本挂掉仍能正常工作。

【面试题】2. RabbitMQ的死信队列(DLX)是什么?哪些场景下消息会进入死信队列?如何利用DLX实现延迟队列? ​

【标准答案】死信队列(Dead Letter Exchange)是RabbitMQ中处理"无法被正常消费的消息"的机制。消息变成死信有三种情况:①消息被消费者basic.nack或basic.reject且requeue=false;②消息TTL过期;③队列达到最大长度,新消息被挤出。

DLX本质就是一个普通Exchange,只是绑定了某个队列作为"死信目的地"。设置方式是在队列声明时加x-dead-letter-exchange参数指向DLX,可选x-dead-letter-routing-key指定路由键。

利用DLX实现延迟队列的经典方案:消息发到TTL队列(不设消费者),TTL过期后自动转入DLX绑定的消费队列。比如下单30分钟未支付自动取消——消息设TTL=30分钟发到order.delay队列,过期后进入DLX路由到order.close队列,消费者处理关闭逻辑。

注意点:RabbitMQ只会检查队列头部的消息是否过期,如果前面有消息阻塞,后面的消息即使TTL到了也不会被移除。所以生产上通常每个延迟级别用独立队列,或者用rabbitmq_delayed_message_exchange插件直接支持延迟投递,避免这个问题。

【面试题】3. RocketMQ的事务消息是如何实现的?半消息是什么?回查机制怎么工作? ​

【标准答案】RocketMQ事务消息采用二阶段提交+回查机制,保证本地事务和消息发送的最终一致性。

流程:①生产者先发一条"半消息"到Broker,半消息对消费者不可见(存在特殊的RMQ_SYS_TRANS_HALF_TOPIC);②Broker返回确认后,生产者执行本地事务;③根据本地事务结果发送commit或rollback,commit后消息对消费者可见,rollback则删除半消息。

回查机制解决的是:如果生产者执行本地事务后没发commit/rollback(比如宕机了),Broker怎么处理?Broker会定时扫描未确认的半消息,向生产者发起回查,生产者根据本地事务状态再次返回commit/rollback。默认回查15次,超过则rollback。回查间隔从10秒开始递增。

实际开发注意:本地事务检查逻辑要幂等;回查接口要轻量;事务消息不支持延时和批量;4.3.0以上版本才支持。

面试加分点:对比本地消息表方案——事务消息把"本地事务+消息发送"的原子性交给了Broker保障,不需要额外的本地表存储,架构更简洁,但依赖MQ的可靠性。

【面试题】4. 消息积压了怎么处理?线上突然发现消费者延迟几百万条消息,你的应急方案是什么? ​

【标准答案】消息积压是线上高频问题,分三步走:紧急止血→加速消费→根因修复。

紧急止血:先看能不能临时扩容消费者。如果Topic分区数允许,快速增加消费者实例。但注意Kafka中消费者数不能超过分区数,分区数是上限。如果分区数不够,先紧急扩分区。

加速消费:如果扩不了消费者,就写一个临时消费程序,只做转发——快速消费积压消息,批量写到新的临时Topic(分区数设大),再用大量消费者并行消费。这是经典的"扩容分流"思路。

根因修复:排查是消费者处理慢(慢SQL、外部服务超时)、还是消费逻辑异常频繁重试、还是流量突增。针对性地优化:加索引、加缓存、降级非核心逻辑、消息降级写库后异步处理。

还要关注积压带来的副作用:消息TTL过期被丢弃、消费位移丢失、磁盘打满。RabbitMQ要监控队列内存水位,Kafka要监控磁盘使用率。

预防措施:监控消费延迟指标(Consumer Lag)、设置积压告警、做好容量规划、压测消费者吞吐上限。

【面试题】5. Kafka的零拷贝和顺序写是如何提升性能的?和传统IO相比差距在哪? ​

【标准答案】这是Kafka高吞吐的两个核心设计。

顺序写:Kafka消息追加写入CommitLog,不做随机写。磁盘顺序写性能接近内存随机写——机械硬盘顺序写约100-200MB/s,随机写只有几百KB/s;SSD差距小但仍有量级差异。核心是不需要磁头寻道(HDD)和减少写放大(SSD)。

零拷贝:传统读文件发送到网络要经过4次拷贝(磁盘→内核页缓存→用户空间→Socket缓冲区→网卡),还有4次上下文切换。Kafka用sendfile系统调用,数据直接从页缓存到网卡,省掉用户空间的两段拷贝和2次上下文切换。Java层面就是FileChannel.transferTo()。

两者配合:Kafka写入靠顺序写保证高吞吐,读取靠PageCache+零拷贝保证高效率。实际生产中消费者读的大多是最近的数据,命中PageCache概率很高,直接走内存到网卡,磁盘都不用碰。

面试可以补充:mmap是另一种零拷贝方式(RocketMQ用),适合小文件随机读;sendfile适合大文件顺序传输。Kafka选sendfile是因为消息读取场景是大块顺序传输。

【面试题】6. RabbitMQ的Exchange四种类型分别适合什么场景?Headers类型为什么用得少? ​

【标准答案】四种Exchange类型:

Direct:精确匹配routing key。适合点对点通信,比如日志按级别路由——error队列只绑routing key="error"。简单高效,最常用。

Fanout:广播到所有绑定队列,忽略routing key。适合广播场景,比如用户注册后同时发短信、发邮件、送新人券,三个队列绑定同一个Fanout Exchange就行。

Topic:通配符匹配routing key,支持(一个词)和#(零或多个词)。适合多维度路由,比如订单系统按"order.create.beijing"这种格式路由,绑order..beijing可以匹配所有北京订单事件。

Headers:根据消息头属性匹配,忽略routing key。匹配规则用x-match控制——all表示所有头都匹配,any表示任一匹配。理论上灵活,但实际用得少,原因:①性能差,要解析消息头做匹配,比routing key的字符串比较慢;②配置复杂,不如routing key直观;③调试困难,消息头不透明。绝大多数场景Topic就能覆盖,没必要用Headers。

面试加分:实际项目中Direct+Topic覆盖90%+的场景,Fanout做广播,Headers基本只在需要多字段组合路由且不能用routing key编码时才考虑。

【面试题】7. RocketMQ的CommitLog和ConsumeQueue存储模型是怎样的?为什么这么设计? ​

【标准答案】RocketMQ存储采用CommitLog+ConsumeQueue两层结构。

CommitLog:所有Topic的消息顺序写入同一个物理文件,每个文件默认1GB。消息按到达顺序追加,不区分Topic和Queue。这是物理存储层。

ConsumeQueue:逻辑消费队列,每个Topic的每个Queue对应一个ConsumeQueue。存的是消息在CommitLog中的偏移量、消息大小和Tag的HashCode,固定20字节一条。可以理解为CommitLog的索引。

为什么这么设计?核心好处是顺序写。如果按Topic分文件存储,多Topic并发写入就变成随机写,性能急剧下降。所有消息写同一个CommitLog,保证纯顺序写,吞吐量最大化。

读取时:消费者先从ConsumeQueue查到偏移量,再去CommitLog读消息体。虽然多了一次寻址,但ConsumeQueue本身也是顺序读,而且数据量小(20字节/条),几乎全命中PageCache。

对比Kafka:Kafka每个Partition独立文件,多Partition并发写入在机械盘上是随机写。RocketMQ用单CommitLog规避了这个问题,代价是ConsumeQueue多一层间接寻址。SSD时代两者差距缩小,但RocketMQ的设计在HDD上优势明显。

【面试题】8. 消费端如何保证幂等性?有哪些常见方案?各有什么优缺点? ​

【标准答案】消费端幂等性是消息可靠性保障的核心问题——消息可能因为重试、重平衡重复投递,消费逻辑必须能处理重复消息。

方案一:唯一ID+数据库去重表。每条消息带全局唯一ID(业务ID或消息ID),消费前查去重表,存在则跳过。优点:实现简单,强一致;缺点:每条消息都查库,吞吐受限,去重表会膨胀需定期清理。

方案二:唯一ID+Redis SETNX。用Redis的SETNX做去重判断,设置过期时间自动清理。优点:性能高,自动过期;缺点:Redis故障时可能漏去重,极端情况下SETNX不是绝对原子的(需要配合Lua脚本保证原子性)。

方案三:业务状态机。利用业务自身的状态判断,比如订单状态从"待支付"到"已支付"是单向流转,重复消费时状态已经变更,直接跳过。优点:不需要额外存储;缺点:只适合有明确状态流转的场景。

方案四:数据库唯一约束。用业务字段的唯一索引兜底,重复插入直接报错忽略。优点:数据库层面强保证;缺点:依赖异常判断逻辑,不够优雅,且只能防插入场景。

生产实践推荐组合:业务状态机优先(零成本),Redis SETNX做快速过滤(高性能),数据库唯一约束兜底(强保障)。三层防护,生产级可靠。

【面试题】9. Kafka Consumer Group重平衡有哪些协议?新版协调协议比旧版好在哪里? ​

【标准答案】Kafka消费者重平衡经历过三代协议演进。

第一代:Zookeeper协调。消费者直接在ZK上注册和监听,触发重平衡后所有消费者一起参与,整个消费组不可用。问题很明显——ZK的watch机制导致羊群效应,一个消费者变更触发全组重平衡。

第二代:Group Coordinator协议(Kafka 0.9+)。Coordinator从ZK迁到Broker内部,消费者向Coordinator汇报。但仍有两个痛点:①重平衡期间所有消费者停止消费(Stop-The-World);②任意消费者心跳超时就触发全组重平衡,频繁抖动。

第三代:增量协作协议(KIP-429,Kafka 2.4+)。核心改进是增量重平衡——不再全部撤销再重新分配,而是只迁移需要变动的分区。具体有三种策略:①Eager(全量,兼容旧版);②CooperativeSticky(增量,尽量保持现有分配不变);③Sticky(逻辑上全量但尽量保持粘性)。

新版好处:重平衡期间不停消费,只是增量调整;消费者抖动不再影响全局;重平衡速度更快。面试加分:配置group.protocol=consumer加partition.assignment.strategy=CooperativeSticky启用增量协议。

【面试题】10. 在技术选型时,RabbitMQ、Kafka、RocketMQ分别适合什么业务场景?给出你的选型理由。 ​

【标准答案】选型看三个维度:吞吐量、功能丰富度、运维复杂度。

RabbitMQ:适合中小规模、路由逻辑复杂、消息可靠性要求高的业务系统。比如订单状态变更通知(需要复杂路由)、微服务间解耦(需要灵活的Exchange)、延迟任务(TTL+DLX天然支持)。吞吐量万级,功能最丰富(多种Exchange、优先级队列、RPC),Erlang语言使得延迟极低(微秒级),但吞吐量上不去,集群扩展性差,消息堆积性能断崖式下降。

Kafka:适合大数据量、高吞吐、流式处理场景。比如日志收集(每天TB级)、实时数据管道(对接Flink/Spark)、事件驱动架构(事件溯源+流处理)。吞吐量百万级,但功能相对简单——没有延迟消息、没有事务消息(3.x虽有但不成熟)、路由能力弱。运维较重(依赖ZK或KRaft),但生态最好。

RocketMQ:适合电商交易、金融等对可靠性要求极高的业务。比如订单创建→支付→发货链路(事务消息)、延迟取消(18级延迟消息)、消息过滤(Tag/SQL92)。吞吐量十万级,功能最贴合业务(事务消息、延迟消息、消息过滤、消息回溯都是原生支持),阿里双十一验证过。缺点是社区生态不如Kafka,非Java客户端支持弱。

一句话总结:业务系统选RabbitMQ,数据管道选Kafka,电商交易选RocketMQ。但实际选型还要考虑团队技术栈、运维能力和公司基础设施,没有银弹。