消息中间件面试题库
生成时间:2026-05-23 16:22
【面试题】1. Kafka的ISR机制是什么?为什么需要ISR而不是简单地用所有副本确认?
【标准答案】ISR是In-Sync Replicas,即与Leader保持同步的副本集合。它不等于所有副本,而是那些"跟得上"Leader的副本——具体判断标准是: replica.lag.time.max.ms(默认10秒)内没有拉取过最新数据的副本会被踢出ISR。
为什么不全用所有副本?核心原因:可用性与一致性的平衡。如果要求所有副本都确认(acks=all等价于ISR全部确认),用ISR机制意味着只要ISR里的副本确认就行,慢副本或宕机副本不会拖垮写入性能。假如某个副本因为网络抖动落后了,它被移出ISR,Leader继续正常服务,等它追上来再加回ISR。
面试加分点:说清楚ISR的动态伸缩机制——副本落后就移出,追上就加回。配合acks=all使用时,Kafka保证的是"ISR内所有副本确认",不是"所有副本确认",这是很多候选人的误区。另外可以提一嘴,min.insync.replicas参数控制ISR最少需要几个副本才能写入,低于这个值直接拒绝写入,这是保底的一致性兜底。
【面试题】2. RabbitMQ的死信队列(DLX)是什么?哪些情况消息会进入死信队列?如何利用死信队列实现延迟队列?
【标准答案】死信队列就是"无法被正常消费的消息的归宿"。消息变成死信有三种情况:①消息被消费者basic.reject或basic.nack且requeue=false;②消息TTL过期;③队列达到最大长度,消息被挤掉。
利用DLX实现延迟队列的经典方案:TTL + DLX组合。创建一个普通队列A,设置TTL和DLX绑定但不设消费者;消息到A后过期,自动转发到DLX绑定的消费队列B,消费B就实现了延迟。但要注意坑:RabbitMQ只会检查队列头部的消息是否过期,如果前面的消息TTL长,后面的短消息会被"堵住"。解决方案是每个TTL等级用单独的队列,或者用rabbitmq_delayed_message_exchange插件,插件方案是在Exchange层做延迟,消息暂存在内置的Mnesia表中,到时间再路由,不存在头部阻塞问题。
【面试题】3. RocketMQ的CommitLog和ConsumeQueue分别是什么?为什么要这样设计存储模型?
【标准答案】CommitLog是所有消息的物理存储文件,所有Topic的消息顺序追加写入同一个CommitLog,每个文件默认1GB。ConsumeQueue是消息的逻辑队列,每条记录固定20字节(8字节CommitLog偏移量+4字节消息大小+8字节Tag哈希),相当于CommitLog的索引。
为什么这么设计?核心是顺序写性能最大化。所有消息统一顺序写CommitLog,不管是哪个Topic,磁盘顺序写能到600MB/s以上,远超随机写。消费时通过ConsumeQueue快速定位到CommitLog偏移量,再读取实际消息体。这就是经典的"写时顺序追加、读时随机定位"思路。
对比Kafka:Kafka是每个Partition一个独立文件,多Topic多Partition会导致大量随机写。RocketMQ的方案用一套CommitLog解决了随机写问题,代价是读的时候多一次ConsumeQueue→CommitLog的间接寻址,但PageCache会缓存热点ConsumeQueue,实际影响很小。
【面试题】4. 消息消费端的幂等性如何保证?请给出至少三种方案并对比其优缺点。
【标准答案】消费幂等是"同一条消息消费多次结果一致"。三种主流方案:
①唯一ID+数据库去重表:消费前用消息ID(或业务唯一键)查去重表,不存在则插入并处理,存在则跳过。优点:强一致、可靠;缺点:每次消费多一次DB查询和写入,去重表会持续增长需定期清理。
②唯一ID+Redis的SETNX:消费前用SETNX设置消息ID为key,设置成功则处理,失败则跳过,设过期时间防内存无限增长。优点:性能高,适合高并发场景;缺点:Redis宕机或key过期后可能重复消费,不是强一致。可以用Redisson的分布式锁加强。
③业务层天然幂等:设计本身支持重复操作,比如"UPDATE account SET balance = 100 WHERE id = 1"是天然幂等的;或者用乐观锁"UPDATE account SET balance = balance - 10, version = version + 1 WHERE id = 1 AND version = 5"。优点:不依赖外部组件,性能最好;缺点:不是所有业务都能设计成天然幂等。
实战建议:优先用方案③,不行用方案①保底,高并发场景叠加方案②做快速拦截层。RocketMQ和Kafka都支持业务key,建议消费时把业务唯一键作为去重依据而非消息ID,因为消息ID在不同重试场景下可能不同。
【面试题】5. Kafka的Exactly-Once语义是如何实现的?生产者和消费者分别怎么保证?
【标准答案】Kafka的Exactly-Once分两层:
生产端:①幂等Producer(enable.idempotence=true),通过ProducerID+SequenceNumber去重,Broker收到重复的序号直接丢弃,保证单分区内不重复。但幂等只能保证单Producer单Session单分区的Exactly-Once。②事务(transactional.id),跨分区的原子写入,配合Consumer端的read_committed隔离级别,只有事务提交的消息才可见。
消费端:Kafka本身不提供Exactly-Once消费,需要业务侧配合。标准做法是消费+处理+提交Offset放同一个事务里(比如数据库事务),要么全部成功要么全部回滚。Kafka提供了consume-transform-produce模式的事务API,适用于"从Kafka读、处理、再写回Kafka"的流处理场景。
面试重点:说清楚Exactly-Once不是Kafka单独能保证的——生产端Kafka能搞定,消费端必须配合外部存储(DB事务)才能实现端到端Exactly-Once。另外提一下,幂等Producer的原理是Broker端维护每个ProducerID的最近5个SequenceNumber进行去重判断,超过窗口范围的重复无法检测。
【面试题】6. RocketMQ的事务消息原理是什么?和本地消息表方案相比有什么优劣?
【标准答案】RocketMQ事务消息流程:①Producer发送半消息(Half Message)到Broker,半消息对Consumer不可见;②Broker存储半消息后返回确认;③Producer执行本地事务;④根据本地事务结果发送Commit或Rollback给Broker;⑤如果Broker没收到二次确认(比如Producer挂了),会定时回查Producer的本地事务状态,根据回查结果决定提交或回滚。
回查机制是关键:最多回查15次(默认),超过则回滚。回查通过checkLocalTransaction接口实现,Producer需要记录本地事务状态供回查使用。
对比本地消息表方案:
- 事务消息优势:不需要额外建消息表、不需要定时任务扫描、不需要维护消息表状态,Broker原生支持回查,架构更简洁。
- 事务消息劣势:依赖MQ的可靠性,Broker回查有延迟(默认15秒起),极端情况下半消息长时间不决;本地消息表方案更通用,不依赖特定MQ功能,任何MQ都能用。
- 选型建议:用RocketMQ就优先事务消息,用其他MQ或跨团队协作就本地消息表。
【面试题】7. 消息积压了怎么办?请给出完整的应急处理方案。
【标准答案】消息积压是线上高频问题,处理分三步:
第一步:止血——快速消费。增加消费者实例数(前提是分区/队列数足够,Kafka一个分区只能被一个消费者消费,分区数是消费并行度上限);如果分区数不够,先紧急扩分区(Kafka支持,但要评估影响)。消费者内部增加线程数提升并发。
第二步:临时方案——快速转储。如果消费端有bug或下游服务扛不住,直接新起一个简单消费者把消息快速转储到别的Topic或存储(比如Redis/ES/文件),先让积压不再恶化,后面再慢慢处理转储数据。
第三步:根治——定位原因。常见原因:消费逻辑慢(DB慢查询、外部接口超时)→优化消费逻辑或异步化处理;消费者异常重启导致重复消费→修复幂等;分区数太少→扩分区;突发流量→限流+削峰。
关键指标监控:ConsumerLag(Kafka)、队列消息堆积数(RabbitMQ/RocketMQ),设告警阈值。平时就要做容量规划,Consumer处理能力要留余量。
【面试题】8. RabbitMQ的Exchange四种类型分别是什么?路由规则有何区别?各自适合什么场景?
【标准答案】四种Exchange类型:
①Direct:精确匹配routing key。消息的routing key必须完全等于队列绑定的binding key才路由。场景:点对点精确投递,比如日志级别路由(error队列只绑error)。
②Fanout:广播,忽略routing key,消息发到所有绑定队列。场景:广播通知,比如用户注册后同时触发发邮件、发短信、发积分,多个服务并行处理同一事件。
③Topic:模式匹配routing key,支持通配符——*匹配一个单词,#匹配零或多个单词。比如绑order.*匹配order.create和order.cancel,绑order.#匹配所有order开头的。场景:多维度分类路由,比如日志系统按app.env.severity路由。
④Headers:不靠routing key,而是匹配消息头(headers)属性。绑定规则指定headers的键值对匹配条件(x-match=all表示全部匹配,x-match=any表示任一匹配)。场景:多属性组合路由,实际用得少,因为性能差且调试不直观,面试知道就行。
面试加分:默认Exchange是空字符串名的Direct类型,每个队列自动以队列名为binding key绑定到默认Exchange,所以直接发消息到队列名就能路由。
【面试题】9. Kafka零拷贝和顺序写是如何提升性能的?请从操作系统层面解释原理。
【标准答案】顺序写:Kafka消息追加写入CommitLog尾部,不修改已有数据。磁盘顺序写速度接近内存写入(600MB/s+),而随机写只有100KB/s左右。操作系统层面,顺序写能充分利用PageCache的预读(readahead)和批量刷盘机制,脏页按顺序聚合后一次性刷盘,极大减少磁盘寻址开销。
零拷贝:传统数据发送路径是 磁盘→内核缓冲区→用户缓冲区→Socket缓冲区→网卡,4次拷贝+2次上下文切换。Kafka用sendfile系统调用,数据从磁盘直接到内核缓冲区(PageCache),然后通过DMA直接传到网卡,全程不经过用户空间,只有2次拷贝+0次上下文切换。
Java层面:Kafka用FileChannel.transferTo()映射到sendfile,Consumer读取时消息直接从PageCache经网卡发出,根本不经过JVM堆内存,避免了GC压力。
配合mmap:生产者写入时虽然不是严格零拷贝,但通过mmap映射文件到用户空间,减少了一次内核到用户的拷贝。综合来看,Kafka的写入靠顺序写+PageCache+mmap,读取靠零拷贝sendfile,两端都做到极致。
【面试题】10. 在百万级TPS的消息系统架构中,你会如何做分区规划和消费者扩缩容?
【标准答案】百万级TPS的分区规划核心原则:
分区数规划:分区数 = 目标TPS / 单分区TPS上限。Kafka单分区写入TPS约1-2万(SSD),消费TPS约2-5万。百万TPS写入至少50-100个分区。但要平衡:分区过多增加Leader选举开销、文件句柄数、Broker内存占用。建议单Broker分区数不超过1000-2000。
消费者扩缩容:Kafka消费者组扩容上限是分区数,超过分区数的消费者会空闲。所以分区数要预留扩容空间。推荐初始分区数按峰值TPS的1.5-2倍规划。RocketMQ的队列(ConsumeQueue)可以设更多,默认每个Broker每个Topic 16个队列,可以调到64甚至更多。
实际架构:多Topic分担流量,不同业务用不同Topic,避免单Topic过热。热点Key用一致性哈希打散到不同分区。消费者用K8s HPA根据ConsumerLag自动扩缩容,但缩容要优雅——等当前消息消费完再下线,避免重平衡风暴。
反模式警告:不要为了扩容频繁改分区数(Kafka改分区数不支持缩减);不要让单分区承载超过2万TPS;不要让消费者数远超分区数造成资源浪费。RocketMQ可以用Dledger模式自动选主,Kafka用KRaft替代ZooKeeper提升Controller稳定性。