消息中间件面试题库
生成时间:2026-05-21 22:11
【面试题】1. Kafka的ISR机制是什么?为什么需要ISR而不是直接用所有副本?
【标准答案】ISR是In-Sync Replicas的缩写,指的是与Leader保持同步的副本集合。
核心设计思路是这样的:Kafka采用副本机制保证高可用,但不是所有副本都能参与选举。只有ISR中的副本才有资格被选为新Leader。为什么?因为只有这些副本的数据是最新的,切换时不会丢数据。
ISR是动态变化的——如果一个Follower长时间没同步数据(replica.lag.time.max.ms),就会被踢出ISR;等它追上来了,再重新加入。这样设计的好处是:既保证了可靠性(ISR内副本都有完整数据),又兼顾了性能(不需要等所有副本都确认)。
实际生产中,我们通常配置acks=all或acks=-1,要求ISR中所有副本都确认才算写入成功。配合min.insync.replicas参数,比如设置为2,就能保证至少2个副本有数据,即使一台机器宕机也不会丢消息。
【面试题】2. RabbitMQ的死信队列(DLX)是什么?在什么场景下消息会进入死信队列?
【标准答案】死信队列就是存放"无法被正常消费的消息"的队列。消息变成死信有三种情况:
第一,消费者NACK或Reject且requeue=false;第二,消息TTL过期;第三,队列达到最大长度,消息被挤出。
配置方式是在队列上设置x-dead-letter-exchange和x-dead-letter-routing-key参数,指定死信路由规则。消息进入死信队列后,原消息的headers会带上死亡原因(x-death)、时间、队列名等元信息,方便排查。
典型应用场景:延迟队列(消息设TTL,过期后进入DLX由消费者处理)、消费失败重试(设置重试次数,超过后进DLX做人工处理)、异常消息隔离(不让坏消息阻塞正常消费流程)。
生产建议:死信队列一定要监控堆积量,否则容易成为隐蔽的问题盲区。
【面试题】3. RocketMQ的事务消息是怎么实现的?和本地消息表方案比有什么优劣?
【标准答案】RocketMQ事务消息采用半消息(Half Message)+ 回查机制,分三步走:
第一步,Producer发半消息到Broker,这条消息对Consumer不可见;第二步,执行本地事务,根据结果Commit或Rollback,Commit后Consumer才能看到消息;第三步,如果Broker没收到二次确认(比如Producer挂了),会定期回查Producer的本地事务状态,根据回查结果决定投递还是丢弃。
回查最多15次,超过就Rollback。回查接口需要业务方实现。
和本地消息表对比:本地消息表方案更通用,不依赖MQ的事务能力,但需要额外维护一张消息表和定时扫描任务,实现更重。RocketMQ事务消息更优雅,MQ原生支持,不需要额外存储,但耦合了MQ选型,换MQ就得重写。
实际选型:如果已经用了RocketMQ,优先用事务消息;如果MQ选型可能变化,或者用的是不支持事务消息的MQ,本地消息表更稳。
【面试题】4. 消费端如何保证幂等性?说几种实际落地方案。
【标准答案】幂等性就是同一条消息消费多次和消费一次的效果一样。MQ一般都至少保证at-least-once,重复消费不可避免,所以消费端必须幂等。
三种主流方案:
第一,唯一ID+数据库去重。消息带全局唯一ID(业务ID或消息ID),消费前先查数据库有没有这条记录,没有就处理并记录ID,有就跳过。适合业务本身就有数据库的场景,成本最低。
第二,唯一ID+Redis去重。用Redis的SETNX或SET NX EX命令,以消息ID为key,设置成功才处理,设置失败说明已消费。比数据库快,适合高并发场景。注意设置合理的过期时间,避免Redis内存膨胀。
第三,业务状态机。利用业务本身的天然幂等性,比如订单状态从"待支付"到"已支付",用CAS更新(UPDATE order SET status='PAID' WHERE id=? AND status='UNPAID'),状态已变则不重复执行。
生产中通常是1+3组合:Redis做第一层快速拦截,数据库状态机做第二层兜底。不要只依赖一层。
【面试题】5. Kafka的零拷贝和顺序写是怎么回事?为什么Kafka吞吐量这么高?
【标准答案】Kafka高吞吐的核心在存储层和传输层两方面的优化。
存储层:顺序写磁盘。磁盘顺序写速度可达600MB/s,而随机写只有100KB/s左右,差了几千倍。Kafka消息不断追加到CommitLog末尾,正是顺序写。而且Kafka充分利用PageCache,写入时先到页缓存,由OS异步刷盘;读取时如果命中PageCache直接从内存拿,不经过磁盘IO。
传输层:零拷贝。传统数据发送要经历4次拷贝(磁盘→内核缓冲→用户空间→Socket缓冲→网卡),Kafka用sendfile系统调用,数据直接从内核页缓存到网卡,跳过了用户空间拷贝,只有2次拷贝。配合批量发送和压缩(Producer端批量攒消息+压缩,Broker端存压缩数据,Consumer端解压),网络IO也大幅降低。
再加上分区并行(不同Partition在不同Broker上并行读写)、批量操作(批量发送、批量拉取、批量刷盘),整体就实现了百万级TPS。
【面试题】6. 消息积压了怎么处理?线上紧急情况怎么办?
【标准答案】消息积压是生产环境最常见的故障之一,分两步走:紧急止血+根因修复。
紧急处理三板斧:
第一,快速扩容消费者。如果消费者处理能力不足,最快的方式是增加消费者实例数。但要注意:Kafka中一个Partition只能被一个Consumer消费,所以消费者数不能超过Partition数,否则多了也白搭。这时候需要同步扩Partition数。RabbitMQ可以动态增加消费者,没有这个限制。
第二,临时转发。如果原Topic消费不动,新建一个临时Topic,把积压消息快速转发过去,用更多的消费者并行处理。处理完再切回来。
第三,降级非核心消费。把非核心逻辑(比如发通知、写日志)临时关掉,只保留核心业务逻辑,加快消费速度。
根因排查:是消费端太慢(代码bug、下游依赖超时)、还是生产端突然暴增(大促、异常重试风暴)、还是消费者挂了(OOM、GC问题)。找到根因才能彻底解决。
预防措施:监控消费延迟(Consumer Lag)、设置积压告警、压测验证消费能力上限。
【面试题】7. RabbitMQ的Exchange类型有哪些?分别适合什么场景?
【标准答案】四种Exchange类型:
Direct Exchange:精确匹配routing key。消息的routing key和Binding的routing key完全一致才路由。适合点对点场景,比如任务分配、RPC响应路由。
Fanout Exchange:广播模式,不看routing key,绑定到这个Exchange的所有队列都收到消息。适合广播场景,比如群发通知、缓存更新广播。
Topic Exchange:模式匹配routing key,支持(匹配一个词)和#(匹配零或多个词)。比如order.匹配order.create,order.#匹配order.create.success。适合多维度路由场景,比如日志分类(app.error、app.warn按级别订阅)、事件总线。
Headers Exchange:不看routing key,根据消息headers属性匹配,性能差,用得少,了解即可。
实际项目中90%的场景用Direct和Topic就够了。Fanout用于广播,Headers基本不用。
Spring AMQP中,@Exchange注解可以声明Exchange类型,@QueueBinding完成绑定,非常方便。
【面试题】8. RocketMQ的CommitLog和ConsumeQueue是什么关系?为什么这样设计?
【标准答案】这是RocketMQ存储层的核心设计,采用CommitLog+ConsumeQueue的两级结构。
CommitLog是物理存储文件,所有Topic的消息顺序写入同一个CommitLog,每条消息占一条记录,包含消息体、Topic、QueueId、Key等全部信息。文件大小默认1GB,写满后创建新文件。这就是顺序写的性能基础。
ConsumeQueue是逻辑消费队列,相当于CommitLog的索引。每个Topic的每个Queue对应一个ConsumeQueue,每条记录固定20字节:8字节CommitLog偏移量+4字节消息大小+8字节Tag的hashCode。消费者先读ConsumeQueue拿到偏移量,再从CommitLog读完整消息。
为什么这样设计?第一,CommitLog顺序写,写性能极高;第二,ConsumeQueue记录极小(20字节),可以全部缓存在内存中,读性能极高;第三,CommitLog共享写入,不同Topic的消息混合存储,避免了多Topic时的随机写问题。
对比Kafka:Kafka每个Partition独立文件,Topic和Partition多时随机写严重。RocketMQ所有消息共享CommitLog,无论多少Topic都是顺序写,在Topic数量多的场景下优势明显。
【面试题】9. 如何保证消息的顺序性?全局有序和分区有序分别怎么做?
【标准答案】消息顺序性分两种需求:全局有序和分区有序。
全局有序:所有消息严格按发送顺序消费。做法是Topic只设一个Partition/Queue,生产者单线程发送,消费者单线程消费。代价极大,吞吐量严重受限,一般不建议用。只有像数据库Binlog同步这种严格要求顺序的场景才考虑。
分区有序:同一业务键的消息有序即可。做法是发送时用相同的业务ID(比如订单ID)作为分区键,保证同一业务ID的消息路由到同一个Partition/Queue,单个Partition内消息天然有序。消费端每个Partition/Queue只用一个线程消费,就能保证顺序。
三种MQ的实现:Kafka通过Producer指定partition或自定义Partitioner实现;RabbitMQ通过一致性哈希Exchange或单Queue实现;RocketMQ最方便,直接用MessageQueueSelector按orderId选Queue。
踩坑点:消费失败重试可能导致乱序。如果消息3消费失败被重试,消息4已经消费了,顺序就乱了。RocketMQ的顺序消息消费失败会阻塞当前Queue,不会继续消费下一条,直到重试成功。Kafka则需要业务自己处理,比如用状态机保证。
【面试题】10. RabbitMQ、Kafka、RocketMQ怎么选型?说下你的实际经验。
【标准答案】三个维度看:功能丰富度、吞吐量、可靠性。
RabbitMQ:功能最丰富,支持各种Exchange路由、延迟队列、优先级队列、消息追踪,管理界面好用。但吞吐量上限低(万级TPS),集群扩展性差(镜像队列同步复制开销大),Erlang技术栈排查问题难。适合业务系统内部的消息通信,比如订单状态变更通知、异步任务派发,中小规模够用。
Kafka:吞吐量之王(百万级TPS),分布式架构天然支持横向扩展,配合大数据生态(Kafka Streams、Kafka Connect、Flink)做流计算和日志收集无敌。但功能偏少,没有延迟消息、没有事务消息(虽然有事务但语义不同)、消费模型较重(分区和消费者强绑定)。适合日志收集、实时数据管道、大数据场景。
RocketMQ:功能最全(延迟消息、事务消息、消息过滤、顺序消息全部原生支持),吞吐量十万到百万级,Java技术栈排查问题方便,阿里双11验证过。但社区不如Kafka活跃,外部生态弱一些。适合电商、金融等对消息可靠性要求极高的业务场景。
我的经验:日志和数据管道选Kafka,业务系统选RocketMQ,小项目或需要快速上手选RabbitMQ。如果团队没有JVM运维经验,RabbitMQ是更好的起点。Pulsar目前看还在成长期,除非有明确需求否则不推荐作为首选。