本轮 消息中间件面试题库(带答案)
【面试题】1. Kafka的ISR机制是什么?为什么需要ISR而不是简单地等所有副本同步完成?
【标准答案】ISR全称In-Sync Replicas,就是和Leader保持同步的副本集合。核心思路是:不需要等所有Follower都确认,只要ISR里的副本确认了就算写入成功。
为什么不等全部副本?因为分布式环境下总会有慢节点或临时掉线的机器。如果等所有副本,一个节点卡住整条写入链路就堵死了,可用性极差。ISR做了一个很好的折中——既保证可靠性(ISR内的副本数据是完整的),又不牺牲可用性(不需要等最慢的那个)。
关键细节:Follower如果落后太多(超过replica.lag.time.max.ms),会被踢出ISR,等它追上来再重新加入。这样Leader选举时只从ISR里选,保证选出来的新Leader不会丢数据。如果ISR里只剩Leader自己,可以配置unclean.leader.election.enable=false来拒绝非ISR副本成为Leader,宁可不可用也不丢数据。
【面试题】2. RabbitMQ的死信队列(DLX)是什么?在什么场景下消息会进入死信队列?
【标准答案】死信队列就是消息的"停尸房"——消息无法被正常消费时,会被路由到这个特殊队列,方便后续排查或重处理。
消息进入死信队列有三种触发条件:
- 消费者显式NACK或Reject,且requeue=false
- 消息TTL过期(队列级别TTL或单条消息TTL)
- 队列达到最大长度,新消息挤掉最老的
实际应用中,DLX最常见的玩法是配合TTL实现延迟队列——消息先到一个带TTL的队列,过期后自动进入死信队列,消费者监听死信队列就实现了延迟消费。另外,死信队列也是异常处理的重要兜底,线上遇到消费失败的消息不会直接丢失,而是进DLX等待人工介入或自动重试。
【面试题】3. RocketMQ的CommitLog和ConsumeQueue是什么关系?为什么要这样设计存储模型?
【标准答案】RocketMQ的存储是一个"一写多读"的精妙设计:
- CommitLog:所有Topic的消息顺序写入同一个物理文件,append-only,顺序写性能极高
- ConsumeQueue:每个队列的逻辑消费队列,存储的是消息在CommitLog中的偏移量、大小和Tag的hash,相当于CommitLog的索引
为什么要分两层?因为CommitLog的顺序写保证了极高的写入吞吐量,但消费时需要按Topic+QueueId检索,如果直接在CommitLog上遍历效率极低。ConsumeQueue作为索引层,让消费者可以快速定位到感兴趣的消息,同时CommitLog的顺序写不受影响。
这和Kafka不同——Kafka是每个Partition一个独立文件,RocketMQ把所有消息混在一个CommitLog里,好处是顺序写效率更高(没有多文件切换),坏处是读取时多一次索引查找。但总体上,RocketMQ这个设计在消息量极大的场景下写入性能更优。
【面试题】4. 消费端如何保证消息的幂等性?请给出至少三种方案并对比优缺点。
【标准答案】消息中间件一般至少保证"至少一次"投递,所以消费端必须做幂等。三种主流方案:
唯一消息ID + 数据库去重表:消费前先查去重表,消息ID存在则跳过。优点是强一致、可靠;缺点是每次消费多一次DB查询,可优化为消费成功后异步写入去重表。
Redis的SETNX:用消息ID做key,SETNX成功才处理,失败则跳过。优点是性能极高,毫秒级;缺点是Redis宕机可能失效,需要配合过期时间+兜底机制。
业务状态机:利用业务自身的状态判断是否重复。比如订单支付消息,只有"待支付"状态才处理,"已支付"直接忽略。优点是完全融入业务逻辑,不需要额外组件;缺点是通用性差,每个业务都要单独设计。
实战建议:优先用业务状态机(零成本),辅以Redis去重(高性能兜底),关键业务再加数据库去重表(强一致保底)。三层防御,生产环境最稳。
【面试题】5. Kafka的Exactly-Once语义是如何实现的?和At-Least-Once有什么区别?
【标准答案】Exactly-Once就是消息恰好被处理一次,不丢不重。Kafka通过两个机制实现:
幂等Producer(enable.idempotence=true):给每个Producer分配PID,每条消息带Sequence Number,Broker端对<PID, Partition, SeqNum>去重。解决了单Partition内的重复写入问题,但跨Partition和跨会话不保证。
事务(transactional.id):跨多个Partition的原子写入。Producer先开启事务,发送多条消息到不同Partition,最后Commit或Abort。Consumer端设置isolation.level=read_committed只消费已提交的事务消息。
和At-Least-Once的区别:At-Least-Once是"至少一次",可能重复消费;Exactly-Once是"恰好一次",不丢不重。但要注意,Kafka的Exactly-Once是"Kafka内部"的恰好一次,即从Kafka读到、处理完、再写到Kafka这个链路是原子的。如果处理过程涉及外部系统(如写MySQL),还是需要消费端自己做幂等。
【面试题】6. 消息积压了怎么处理?请给出完整的应急方案。
【标准答案】消息积压是线上最常见的故障之一,核心思路是"快速消费+扩容+降级"三步走:
第一步,紧急扩容:快速增加Consumer实例数。但要注意不能超过Partition数(Kafka)或Queue数(RabbitMQ),否则多余的Consumer空转。如果Partition不够,先紧急扩Partition。
第二步,临时消费者方案:写一个简易的Consumer程序,只做消息转储——从积压队列读出来写到新的更多Partition的临时Topic,然后用大量Consumer消费临时Topic。相当于"扇出"消费能力。
第三步,跳过非关键消息:如果消息有优先级,低优先级的可以先跳过或转存到别的存储后慢慢消费。比如日志类消息可以先落到HDFS。
第四步,事后复盘:积压根因通常是消费端Bug、下游DB慢查询、或突发流量。修Bug、加索引、做限流。
一个关键指标:监控消费者Lag(Kafka的consumer lag / RabbitMQ的消息堆积数),设置阈值告警,别等用户反馈才发现积压。
【面试题】7. RabbitMQ的四种Exchange类型分别适用于什么场景?
【标准答案】四种Exchange类型本质是四种路由策略:
Direct Exchange:精确匹配routing key。场景:点对点通信,比如订单服务只接收order队列的消息。最简单直接,性能最好。
Fanout Exchange:广播到所有绑定的队列,忽略routing key。场景:广播通知,比如用户注册后同时发短信、发邮件、送优惠券,三个服务各绑一个队列,消息全收到。
Topic Exchange:通配符匹配routing key,匹配一个词,#匹配零或多个词。场景:多维度路由,比如日志系统用"app.error.db"这种格式,绑定".error.*"的队列收到所有error级别日志。
Headers Exchange:根据消息头属性匹配,忽略routing key。场景:多条件组合路由,但实际用得很少,因为性能差且调试不直观。
面试重点:实际开发90%的场景用Direct和Topic就够了。Fanout在需要广播时用。Headers基本是遗留设计,新项目不推荐。
【面试题】8. RocketMQ的事务消息原理是什么?和本地消息表方案相比有什么优劣?
【标准答案】RocketMQ事务消息通过"半消息+回查"实现分布式事务:
- Producer发送半消息(Half Message)到Broker,此时消息对Consumer不可见
- Broker确认半消息后,Producer执行本地事务
- 本地事务成功→发送Commit,Broker将消息投递给Consumer
- 本地事务失败→发送Rollback,Broker删除半消息
- 如果Broker没收到二次确认(网络抖动等),启动回查机制:定时扫描半消息,回调Producer的checkLocalTransaction方法确认本地事务状态
对比本地消息表方案:
- 事务消息优势:不需要额外的消息表,不侵入业务数据库,不需要定时任务扫描消息表
- 事务消息劣势:依赖RocketMQ,回查有延迟(默认15秒起),不适合超高并发场景(半消息先存特殊Topic有开销)
- 本地消息表优势:不依赖特定MQ,和业务事务同库保证一致性,方案通用
- 本地消息表劣势:业务表和消息表耦合,定时扫描有延迟,消息表可能膨胀
选型建议:用RocketMQ就优先事务消息,其他MQ或跨团队场景用本地消息表。
【面试题】9. Kafka为什么这么快?请从存储、网络、架构三个维度分析。
【标准答案】Kafka的高性能是多个优化叠加的结果,不是单一技巧:
存储维度:
- 顺序写:消息追加到日志文件末尾,避免随机写。顺序写在机械盘上比随机写快几千倍,SSD上也快几十倍。
- 零拷贝:消费者读取时用sendfile()系统调用,数据从磁盘直接到网卡,跳过用户态拷贝。传统方式是磁盘→内核缓冲区→用户空间→Socket缓冲区→网卡,4次拷贝;零拷贝只有2次。
- 页缓存(PageCache):Kafka不自建缓存,直接利用OS的PageCache,写入时先到PageCache,由OS异步刷盘。读取时如果命中PageCache直接从内存返回。
网络维度:
- 批量发送:Producer将多条消息打包成batch再发送,减少网络IO次数。batch.size和linger.ms配合使用。
- 压缩:Producer端压缩(支持gzip/snappy/lz4/zstd),整个batch压缩后传输,Broker端保持压缩存储,Consumer端解压。网络带宽省几倍。
- Reactor网络模型:Broker用1个Acceptor线程+N个Processor线程+业务线程池,高并发下网络IO不成为瓶颈。
架构维度:
- Partition并行:Topic分成多个Partition,不同Partition在不同Broker上,天然分布式并行。
- 消费者组并行消费:一个Partition同一时刻只被一个Consumer消费,无锁竞争。
【面试题】10. 在百万级TPS的消息系统中,你会如何设计消息不丢失的完整链路?
【标准答案】消息不丢失要从生产端、Broker端、消费端三个环节全链路保障:
生产端:
- Kafka:acks=all(等ISR全部确认)+ retries合理配置 + enable.idempotence=true防重复
- RabbitMQ:开启Publisher Confirm机制,异步确认每条消息到达Broker
- RocketMQ:用同步发送(syncSend),配合重试机制
Broker端:
- Kafka:min.insync.replicas=2(至少2个副本确认)+ replication.factor≥3 + unclean.leader.election.enable=false
- RabbitMQ:消息持久化(delivery_mode=2)+ 队列持久化(durable=true)+ 交换器持久化 + 镜像队列或仲裁队列
- RocketMQ:同步刷盘(SYNC_FLUSH)+ 主从同步复制(SYNC_MASTER)
消费端:
- 关闭自动提交Offset,业务处理成功后手动提交
- 消费失败进入重试队列,重试多次后进死信队列
- 消费端做幂等(唯一ID去重/业务状态机),即使重复消费也不出错
监控兜底:
- 生产端确认超时告警
- Broker副本不足告警
- 消费Lag超阈值告警
- 定期对账:生产者发消息时写一条记录到对账表,消费者处理完也写一条,定时跑对账任务补差
核心原则:没有银弹,每一层都要防。生产端防发送失败,Broker防存储丢失,消费端防漏消费。任何一环断了都可能丢消息。