【八股必备】消息队列常见面试题 第一部分基础概念与选型篇1. 面试官我看你项目里用过消息队列能先跟我聊聊你为什么要在项目里引入MQ吗或者说你觉得消息队列解决了什么痛点候选人好的。当时引入MQ主要是为了解决几个核心问题。 首先是系统解耦。比如在我们那个订单系统里用户下单后需要通知库存系统扣减库存、通知积分系统增加积分、给推荐系统发送用户行为日志。如果把这些逻辑都耦合在订单服务里一旦库存系统挂了订单就会失败这是不能接受的。引入MQ后订单服务只需要把“订单创建成功”这个事件扔到队列里下游服务自己去订阅订单服务不用关心它们的死活实现了削峰填谷式的解耦。 其次是异步提速。还是下单的例子如果同步调用一个下单请求可能要耗800ms。但把非核心步骤发短信、加积分异步化后核心流程只需要100ms用户体验会好很多。 最后是流量削峰。特别是在秒杀场景下瞬间流量可能是平时的100倍。如果让数据库直接抗肯定挂。我们让请求先涌入MQ然后后端服务以自己的最大能力去消费保证系统不会被冲垮。2. 面试官你提到了削峰填谷那如果现在流量峰值特别高MQ都快被打满了你会怎么做这就是典型的消息积压问题。候选人消息积压通常是因为生产速度 消费速度。 首先我会定位问题。是消费者出了Bug导致消费卡住还是确实是因为流量太大扛不住了如果是Bug导致积压比如几百万消息我会先修复Bug并上线。但此时如果直接重启消费速度依然很慢。我会采用临时紧急扩容的方案写一个临时分发程序把积压的消息全部捞出来。新建一个Topic分区数Partition设置成原来的10倍。临时征用10倍的机器或者容器部署消费者每个机器消费一个分区。等积压消息处理完再恢复原有架构。这本质上就是通过增加并行度来换取消费速度。如果确实是因为流量洪峰我会考虑优化消费逻辑比如原来是一条一条处理的能不能改成批量处理并开启多线程如果还不能解决那就只能水平扩容增加Topic的队列数分区数同时增加消费者实例。3. 面试官市面上MQ这么多RabbitMQ、RocketMQ、Kafka你是怎么做技术选型的当时为什么没选另外两个候选人选型主要看业务场景和技术栈。 当时我们团队技术栈主要是Java这一点上RocketMQ比用Erlang写的RabbitMQ更有优势遇到底层问题我们可以自己看源码。 另外我们当时的业务场景比较复杂有事务消息和延迟消息的需求而Kafka在这方面的功能相对单一。 从性能上看RocketMQ的吞吐量是10万级满足我们业务绰绰有余并且它支持分布式部署可用性很高。总结一下如果单纯做日志采集、需要极高吞吐量且允许少量数据丢失我会选Kafka。如果需要丰富的特性延迟、事务、强一致性、Java技术栈RocketMQ很适合。如果只是中小公司内部简单的业务解耦对并发要求不高追求轻量级和易用性RabbitMQ也不错。第二部分核心难点与常见问题篇4. 面试官你说的这些特性比如事务消息和幂等性能详细讲讲吗先从幂等性开始。在使用MQ的时候你怎么保证消息不会被重复消费候选人消息重复消费其实是MQ的一个常态尤其是在网络抖动或消费者宕机重启时。MQ为了保证消息不丢失可能会重试发送这就导致了重复。要解决这个问题关键不在MQ而在消费端自己做幂等处理。 我常用的几种方案数据库唯一约束如果是插入数据的操作可以利用数据库的唯一索引。比如插入订单流水利用流水号作为唯一键第二次插入会失败保证数据不会被重复插入。利用Redis实现幂等在消费前先去Redis查这个MessageID是否被处理过。如果没处理就处理并写入Redis设置过期时间如果处理过直接丢弃。这是最常用的方案。业务逻辑本身的幂等比如更新账户余额的操作如果是“100”这种是非幂等的。但如果改成“将余额设置为100”那即使执行多次结果也是一样的这就是SQL层面的幂等。状态机比如订单状态有“待支付 - 已支付”。如果消息重复消费发现订单已经是“已支付”了就不再做任何操作直接忽略。5. 面试官那关于事务消息呢如果我要保证“订单创建”和“扣减库存”这两个操作最终一致用RocketMQ的事务消息怎么实现假如扣库存失败了怎么办候选人这个问题很经典RocketMQ的事务消息就是为了解决这种分布式事务最终一致性的。 以订单服务A和库存服务B为例流程是这样的发送半消息订单服务A先发送一条“准备扣减库存”的半消息给RocketMQ Broker。此时消息对消费者库存服务B是不可见的。执行本地事务半消息发送成功后订单服务A执行本地事务也就是创建订单在本地数据库插入一条数据。提交/回滚如果本地事务执行成功订单服务A向Broker发送Commit指令此时消息对库存服务B可见B可以消费去扣库存。如果本地事务执行失败发送Rollback指令Broker删除那条半消息。事务回查如果第3步因为网络原因超时了或者订单服务A突然宕机了Broker会定期回调订单服务A的接口询问“你那个本地事务到底怎么样了”订单服务A根据数据库里的订单是否存在回复Commit或Rollback。B服务消费失败怎么办如果B服务因为代码Bug扣库存失败RocketMQ有重试机制。如果重试多次还是失败消息会进入死信队列。这时候需要人工介入处理比如手动补单或者回滚订单。这保证了数据最终是一致的只是中间会有延迟。6. 面试官你提到了死信队列那除了死信怎么保证消息不丢从生产、存储、消费三个阶段分别讲讲。候选人保证消息不丢失是一个系统工程必须三个环节都考虑到生产阶段生产者发消息给BrokerBroker需要返回ACK。生产者需要处理好这个ACK。如果没收到ACK或者收到异常必须重试。像RocketMQ的同步发送就是阻塞直到收到ACK否则抛出异常重试这就保证了消息到了Broker。Broker存储阶段如果Broker收到消息后还没存盘就宕机了消息也会丢。所以我们需要开启持久化机制比如RocketMQ的消息会刷盘到物理磁盘。另外集群要部署成主从模式开启同步复制。即Leader节点收到消息后等Follower节点也写成功了才返回ACK给生产者。这样即使Leader挂了Follower也能顶上数据不丢。消费阶段消费者拿到消息后不要先ACK再处理业务逻辑。必须是业务逻辑处理成功后再提交ACK/Offset。如果先ACK再处理万一处理过程中消费者宕机了消息就丢了。第三部分原理与对比篇7. 面试官Kafka为什么那么快很多人说它用了零拷贝你能解释一下什么是零拷贝吗候选人Kafka的高性能得益于它的设计哲学主要有几点顺序写入Kafka利用磁盘顺序读写的特性比随机读写快几个数量级。因为磁盘的顺序读写速度可以和内存媲美。批量处理Kafka在发送和消费时都不是单条处理的而是批量打包减少了网络开销。零拷贝这是Kafka性能优化的精髓。传统的文件读取并发送到网络需要经历磁盘 - 内核空间(PageCache) - 用户空间(应用程序) - Socket缓冲区 - 网卡。数据被拷贝了4次。而Kafka使用了sendfile系统调用数据直接从内核空间的PageCache拷贝到网卡跳过了用户空间。这样CPU就不用参与数据拷贝了大大提升了吞吐量降低了延迟。压缩Kafka支持批量压缩比如gzip、snappy减少了网络传输的数据量。8. 面试官你说Kafka快那RocketMQ和它比架构上有什么区别候选人最大的区别在于Broker的架构和对顺序写的利用程度。Kafka它的Broker是无状态的每个分区Partition可以分布在不同的Broker上。它所有的消息都是追加写入到文件末尾完全利用顺序写所以单机吞吐量极高。RocketMQ虽然RocketMQ也利用了顺序写但它的所有消息都写到同一个CommitLog文件里然后异步生成ConsumeQueue索引。这样做的优点是虽然写一个文件会有锁竞争但整体上利用了文件的局部性原理并且方便重启恢复。架构区别Kafka用分区做物理存储单元RocketMQ用CommitLog做物理存储ConsumeQueue做逻辑队列。另外RocketMQ有主从切换的完善机制而Kafka依赖Zookeeper进行Controller选举。9. 面试官假如现在老板让你从零开始设计一个简单的消息队列你会怎么设计核心组件有哪些候选人这个问题很有挑战性我觉得可以从这几个核心模块来思考通信协议模块首先要解决客户端Producer/Consumer和服务端Broker怎么通信。我会参考Dubbo或Netty设计一套基于TCP的自定义协议或者直接基于HTTP包含请求头魔数、消息长度、类型和请求体。存储模块消息不能只存在内存里要持久化。我会采用类似Kafka或RocketMQ的顺序写文件的方式。为了快速检索需要建立索引比如Offset索引或时间戳索引。路由与元信息管理需要知道每个Topic有哪些队列Partition这些队列分布在哪些Broker上。这需要一个注册中心类似Zookeeper或Namesrv来管理元数据。高可用模块主从复制数据不能只有一份需要多副本Leader-Follower模式。如果Leader挂了需要选一个新的Leader出来。消息确认需要实现ACK机制确保消息至少被消费一次At least once同时要处理重复消息的问题。消费模型支持点对点和发布订阅。需要维护消费进度Offset并且支持Consumer Group的Rebalance机制。新增进阶题目基于大厂高频考点补充10. 面试官Kafka的Rebalance了解吗什么时候会发生如果频繁Rebalance会有什么问题候选人了解。Rebalance本质上是一种协议规定了一个Consumer Group下的所有Consumer如何达成一致来分配订阅Topic的所有分区。发生时机组成员数量发生变化比如有新的Consumer加入或者旧的Consumer宕机、主动关闭。订阅的主题数量发生变化。分区数量发生变化比如增加了分区。危害 Rebalance期间整个Consumer Group会停止消费所有的Consumer都会暂停处理消息等待Rebalance完成。 如果Rebalance过于频繁比如因为消费者频繁超时会导致整个集群一直处于“停滞”状态影响消息处理的实时性。这通常是因为消费者处理消息时间太长导致心跳线程超时被Group协调者踢出从而触发Rebalance。11. 面试官Kafka 中的 ISR、OSR、AR 分别代表什么HW和LEO又是什么候选人这是Kafka保证数据一致性和高可用的核心概念。ARAssigned Replicas分区中的所有副本包括Leader和Follower。ISRIn-Sync Replicas与Leader保持同步的副本集合。这些副本的消息量和Leader相差不大通过replica.lag.max.messages或时间阈值控制。只有ISR里的副本才有资格被选举为Leader。OSROut-of-Sync Replicas与Leader同步滞后的副本通常是因为网络延迟或自身故障。AR ISR OSR。LEOLog End Offset每个副本的最后一条消息的偏移量。HWHigh Watermark高水位指的是消费者能看到的最大偏移量。HW取的是ISR中最小的LEO。所有副本的HW以下的数据才是所有副本“一致”的是已提交的。12. 面试官RocketMQ 的延迟消息是如何实现的比如我要发一个延迟10分钟的消息。候选人RocketMQ的延迟消息并不是任意时间的而是预设了多个延迟级别比如1s、5s、10s、30s、1m、2m等可以在服务端配置。 它的实现原理很巧妙生产者发送一个延迟级别为3假设对应10s的消息到Broker。Broker收到消息后发现是延迟消息它不会把消息放到原Topic的队列里而是将消息转存到一个内部的TopicSCHEDULE_TOPIC_XXXX中。这个Topic下有很多队列每个延迟级别对应一个队列。Broker里有一个定时任务ScheduleMessageService它会不断地扫描这些延迟队列。比如扫描“10s”那个队列如果发现某个消息的当前时间 - 存储时间 10s就认为它到期了。定时任务将这条到期的消息根据原始的Topic和Queue信息重新投递到原来的Topic中去。此时消费者就能正常消费到这条“延迟”消息了。面试官:RabbitMQ-如何保证消息不丢失候选人: 嗯!我们当时MYSQL和Redis的数据双写一致性就是采用RabbitMQ实现同步的这里面就要求了消息的高可用性我们要保证消息的不丢失。主要从三个层面考虑 第一个是开启生产者确认机制确保生产者的消息能到达队列如果报错可以先记录到日志中再去修复数据 第二个是开启持久化功能确保消息未消费前在队列中不会丢失其中的交换机、队列、和消息都要做持久化 第三个是开启消费者确认机制为auto由spring确认消息处理成功后完成ack当然也需要设置一定的重试次数我们当时设置了3次如果重试3次还没有收到消息就将失败后的消息投递到异常交换机交由人工处理面试官:RabbitMQ消息的重复消费问题如何解决的候选人:嗯这个我们还真遇到过是这样的我们当时消费者是设置了自动确认机制当服务还没来得及给MQ确认的时候服务宕机了导致服务重启之后又消费了一次消息。这样就重复消费了 因为我们当时处理的支付(订单|业务唯一标识)它有一个业务的唯一标识我们再处理消息时先到数据库查询一下这个数据是否存在如果不存在说明没有处理过这个时候就可以正常处理这个消息了。如果已经存在这个数据了就说明消息重复消费了我们就不需要再消费了面试官:那你还知道其他的解决方案吗?候选人: 嗯我想想~ 其实这个就是典型的幂等的问题比如redis分布式锁、数据库的锁都是可以的面试官:RabbitMQ中死信交换机?(RabbitMQ延迟队列有了解过嘛)候选人: 嗯!了解过! 我们当时的xx项目有一个xx业务需要用到延迟队列其中就是使用RabbitMQ来实现的。 延迟队列就是用到了死信交换机和TTL(消息存活时间)实现的。 如果消息超时未消费就会变成死信在RabbitMQ中如果消息成为死信队列可以绑定一个死信交换机在死信交换机上可以绑定其他队列在我们发消息的时候可以按照需求指定TTL的时间这样就实现了延迟队列的功能了。 我记得RabbitMQ还有一种方式可以实现延迟队列在RabbitMQ中安装一个死信插件这样更方便一些我们只需要在声明交互机的时候指定这个就是死信交换机然后在发送消息的时候直接指定超时时间就行了相对于死信交换机TTL要省略了一些步骤面试官:如果有100万消息堆积在MQ如何解决?候选人: 我在实际的开发中没遇到过这种情况不过如果发生了堆积的问题解决方案也所有很多的第一:提高消费者的消费能力可以使用多线程消费任务第二:增加更多消费者提高消费速度 使用工作队列模式设置多个消费者消费消费同一个队列中的消息第三:扩大队列容积提高堆积上限 可以使用RabbitMQ惰性队列惰性队列的好处主要是 1接收到消息后直接存入磁盘而非内存 2消费者要消费消息时才会从磁盘中读取并加载到内存3支持数百万条的消息存储面试官:RabbitMQ的高可用机制有了解过嘛候选人: 嗯熟悉的~ 我们当时项目在生产环境下使用的集群当时搭建是镜像模式集群使用了3台机器。 镜像队列结构是一主多从所有操作都是主节点完成然后同步给镜像节点如果主节点宕机后镜像节点会替代成新的主节点不过在主从同步完成前主节点就已经宕机可能出现数据丢失 面试官:那出现丢数据怎么解决呢? 候选人: 我们可以采用仲裁队列与镜像队列一样都是主从模式支持主从数据同步主从同步基于Raf协议强一致。 并且使用起来也非常简单不需要额外的配置在声明队列的时候只要指定这个是仲裁队列即可