别再背各种MQ的区别了,你只需要知道"它就是个中间人,A把消息给它,它转交给B"
写在前面
消息队列(Message Queue,简称MQ)是分布式系统里最常用的解耦工具 。面试里问到MQ,核心就考一件事:消息怎么不丢、怎么不重、怎么不积压。
MQ的本质是什么?
就是两个系统之间的**"信箱"** 。A系统把消息放进信箱,B系统从信箱里取。A不用管B在不在、能不能处理,B也不用管A什么时候发。
就这么简单。所有复杂的功能(持久化、集群、事务消息)都是围绕"信箱怎么更可靠"做的增强。
面试时MQ要掌握到什么程度? 你不需要懂所有MQ的源码,但你要能说清:选型理由 + 怎么保证消息不丢 + 怎么保证不重复消费 + 消息积压怎么处理。
这篇文章就按这个思路来讲。
一、MQ解决了什么问题?(先搞懂"为什么需要")
面试官如果问"你们项目里为什么用MQ",你要回答三个核心价值,而不是只说"解耦"。
① 解耦(服务之间不直接依赖)
A系统完成订单后,需要通知B(短信)、C(积分)、D(物流)。如果A直接调用B、C、D的接口,B挂了A也跟着报错,C改接口A要跟着改代码。引入MQ后,A只管发消息,B/C/D自己去订阅。A不知道B的存在,B挂了不影响A。
② 异步(非阻塞,提升响应速度)
用户下单后,A系统要做:扣库存 → 生成订单 → 发短信 → 加积分。如果同步执行,用户要等好几秒。引入MQ后,A只做核心操作(扣库存+生成订单),然后发一条消息到MQ就返回"下单成功",短信和积分异步执行。用户体验大幅提升。
③ 削峰填谷(流量洪峰时保护下游)
双11零点,每秒10万下单请求,数据库根本扛不住。MQ就像一个缓冲池 ,把请求先存下来,下游数据库按自己能承受的速度(比如每秒5000条)慢慢消费。高峰流量被"削"平了,数据库不会被打死。
二、核心链路(面试就考这一条)
面试官问MQ的所有问题,本质上都围绕着这条链路:
生产者 → Broker(MQ服务器) → 消费者
每个环节都要保证两件事:消息不丢 、消息不重。
2.1 消息不丢(怎么保证可靠性)
① 生产者 → Broker
- 生产者发消息时,Broker收到后返回ACK(确认)。
- 如果生产者没收到ACK,就重试(最多重试N次,超过记录日志人工介入)。
- RocketMQ/Kafka都支持
acks参数:acks=0(不等待确认,最快但可能丢)、acks=1(Leader确认,默认)、acks=-1/all(所有副本确认,最安全但最慢)。
② Broker 存储消息
- 消息到了Broker,先刷盘再返回ACK(或异步刷盘折中)。
- RocketMQ的刷盘方式:
SYNC_FLUSH(同步刷盘,安全但慢)和ASYNC_FLUSH(异步刷盘,快但断电可能丢)。 - Kafka用分区多副本保证:消息写入Leader后,等ISR(同步副本集)里的follower都同步完了才返回ACK。
③ Broker → 消费者
- 消费者拉取消息,处理完业务逻辑后,再手动提交offset(位移)或ACK。
- 绝对不能:拉取到消息就自动提交ACK,然后业务处理失败了------消息就丢了。
- 正确做法:业务处理成功 → 手动提交offset;业务处理失败 → 不提交,消息会被重新消费或进死信队列。
2.2 消息不重(幂等性)
消息队列保证"至少一次(At Least Once)"投递,即消息可能重复,但不会丢失。
所以消费者必须自己做幂等,即同一条消息处理多次和一次效果一样。
常见幂等方案:
- 数据库唯一索引:订单号作为唯一键,重复插入报错(简单有效)。
- Redis缓存处理状态 :处理前
SETNX,如果key已存在说明已处理过,直接跳过。 - 业务本身幂等:比如"将库存减1"不幂等(执行两次变减2),但"将库存设为某个值"幂等。
三、消息积压(消费者扛不住了怎么办?)
场景:生产者发消息太快,消费者太慢,MQ里消息越堆越多。
常见原因:
- 下游数据库慢(SQL没优化、锁表)
- 消费者单线程处理,没充分利用资源
- 消费者逻辑太重(比如每条消息要调多个外部接口)
解决思路:
- 临时扩容消费者:增加消费者实例,同时增加Topic的分区数(Kafka中分区是并行度的上限,一个分区只能被一个消费者消费)。这是最快速的应急方案。
- 优化消费逻辑:批量处理(一次拉取100条,批量插入数据库)、异步化(消费时只做最核心的逻辑,非核心的异步处理)。
- 降级:如果积压太严重,先丢弃非核心消息,保证核心业务能正常消费。
- 排查上游:是不是生产者的流量突增?能不能上游限流?
注意:队列满了一般不会影响生产者,因为MQ有反压机制(生产者会阻塞或超时)。但积压持续下去,磁盘会被写满,造成更严重的问题。
四、常见MQ选型对比(面试必问)
面试官一定会问:"Kafka、RocketMQ、RabbitMQ你们怎么选的?"
你不需要把三者所有功能都背下来,记住核心场景就行:
| 对比维度 | Kafka | RocketMQ | RabbitMQ |
|---|---|---|---|
| 吞吐量 | 极高(百万级/秒) | 高(十万级) | 中等(万级) |
| 延迟 | 毫秒级(非极端场景) | 毫秒级 | 微秒级(最低) |
| 消息可靠性 | 高(副本机制) | 极高(同步刷盘+事务) | 高 |
| 事务消息 | 不支持 | 支持 | 不支持(但有事务机制) |
| 顺序消息 | 分区内有序 | 支持 | 支持 |
| 延迟消息 | 不支持 | 支持 | 支持 |
| 语言 | Scala/Java | Java | Erlang |
| 适用场景 | 日志收集、大数据、流处理 | 业务消息、交易场景、需要事务 | 低延迟业务、灵活路由 |
选型建议(面试里你就这么说) :
- 日志收集、大数据、流处理 → Kafka(吞吐量是王道,丢一点日志也能接受)。
- 金融交易、订单、库存等核心业务 → RocketMQ(支持事务消息,同步刷盘保证数据绝对不丢,Java技术栈好维护)。
- 低延迟、复杂路由、中小企业 → RabbitMQ(延迟最低,路由灵活,但吞吐量不如前两者)。
五、高级特性(面试加分项)
5.1 RocketMQ的事务消息(分布式事务解决方案)
这是RocketMQ独有的能力,面试里提到会很加分。
问题:A扣款成功了,发消息给B扣库存。如果消息发失败了,A的扣款怎么回滚?如果消息发成功了但B扣库存失败了,怎么办?
RocketMQ事务消息的流程:
- A发送一条半消息(Half Message) 到RocketMQ------对消费者不可见,相当于"我要开始事务了,先占个坑"。
- A执行本地扣款事务(
update account set money = money - 100 where id = 1),绑在同一个本地事务里。 - 根据本地事务结果执行二次确认 :
- 扣款成功 → A向MQ发送
Commit指令 → 半消息变为可见,B才能消费到这条消息去扣库存。 - 扣款失败 → A向MQ发送
Rollback指令 → MQ直接删除半消息,B永远收不到。
- 扣款成功 → A向MQ发送
- 兜底反查机制 :如果A在执行本地扣款后、发送
Commit/Rollback之前突然宕机了怎么办?RocketMQ会定时回查A系统:"你那条扣款到底成功了没?"A根据本地事务状态告诉MQ是Commit还是Rollback。
面试官追问:"本地事务怎么和发送半消息绑定的?"
RocketMQ的事务消息本质上把"发送半消息"和"执行本地事务"放在了一个事务里。具体实现:生产者定义TransactionListener,重写executeLocalTransaction和checkLocalTransaction方法。第一个方法是执行本地事务并返回事务状态(COMMIT/ROLLBACK/UNKNOW),第二个方法是事务状态未知时的回查逻辑。
核心价值 :这是一种最终一致性方案,用消息的可靠投递 + 本地事务 + 反查机制,保证A扣款和B扣库存最终一致。它牺牲了强一致性(允许短暂不一致),换来了高可用和高性能。
5.2 顺序消息(局部有序)
有些场景要求消息按顺序消费(比如订单状态流转:创建→支付→发货,必须按顺序)。
Kafka/RocketMQ的实现方式:
- 把需要有序的消息发到同一个分区(Partition/Queue)。
- 一个分区只能被一个消费者消费(串行),所以消息在分区内是严格有序的。
- 注意:这只能保证"局部有序"(同一个订单的消息有序),不能保证"全局有序"(所有订单的所有消息有序)。因为不同订单可以走不同分区,并行消费。
场景 :订单创建、支付、发货三条消息,按orderId做分区键,同一个orderId的消息永远进同一个分区,串行消费。不同orderId的消息可以并行消费,互不影响。
5.3 死信队列(处理消费失败的消息)
场景:消息消费重试了N次(比如16次)还是失败,不能再重试了(否则一直占着队列)。
处理 :进入死信队列(DLQ,Dead Letter Queue) ,人工介入处理(比如修复数据后手动补发消息、或者单独写修复脚本)。
RocketMQ和RabbitMQ自带死信机制,Kafka没有死信概念,需要业务自己实现(失败的消息打到另一个Topic)。
5.4 消息幂等 + 去重(高并发必备)
除了前面说的幂等,在高并发场景下还要考虑去重:
- 消费者拿到消息后,先往Redis里存一个处理标志 (
SETNX key value EX 300),key =业务ID + 消息ID。 - 如果
SETNX返回1,说明这条消息没处理过,执行业务逻辑。 - 如果
SETNX返回0,说明已经处理过了(或者在处理中),直接跳过(返回ACK,不重试)。 - 注意 :
SETNX和业务逻辑如果拆成两步,可能存在并发问题(A线程查不到标志→去执行业务,B线程也查不到→也去执行业务)。要在Lua脚本或分布式锁里保证原子性,或者直接用数据库唯一索引作为去重手段,把"判断是否存在 + 插入"交给数据库的INSERT IGNORE或唯一约束来处理。
六、面试官真正想听什么
场景1:"你们项目里用的什么MQ?为什么?"
普通回答:"RocketMQ。"
加分回答:
"我们做的是订单交易系统,对数据一致性要求很高。RocketMQ的事务消息能帮我们保证订单和库存的最终一致性,而且它是Java写的,出问题了团队能看懂源码。Kafka虽然吞吐量更大,但不太适合事务场景。RabbitMQ的Erlang栈维护成本高,我们没选。"
场景2:"RocketMQ事务消息你们怎么用的?"
加分回答(真实感):
"下单场景:订单服务先发半消息,然后在本地事务里扣库存、生成订单。本地事务提交后就发Commit,库存服务消费到消息扣减库存。如果订单服务挂了,RocketMQ会回查我们提供的
checkLocalTransaction接口,我们根据订单号查数据库状态返回Commit或Rollback。这套方案保证了最终一致性,上线后没出现过数据不一致的问题。"
场景3:"遇到过消息积压吗?怎么处理的?"
加分回答:
"有次大促,下游的积分服务消费太慢(每次都要调外部接口),消息积压了几十万条。我们紧急做了两件事:一是给积分服务扩容了3个实例,同时给Topic加了分区;二是在消费逻辑里把批量调接口改成了异步+缓存。半小时内积压清空了。"
场景4:"消息重复消费过吗?"
加分回答:
"有一次消费者在业务处理完成后、提交offset之前宕机了,重启后消息被重新消费,导致数据重复(比如积分加了两次)。后来我们引入了幂等机制:每条消息带唯一ID,处理前用Redis的
SETNX做去重,业务逻辑里也用数据库唯一索引兜底。之后就再没出现过重复问题。"
小结
| 核心概念 | 大白话 | 面试考点 |
|---|---|---|
| 解耦 | A不知道B的存在 | MQ解决的核心问题之一 |
| 异步 | 核心逻辑先做完,非核心慢慢来 | 用户体验提升 |
| 削峰 | 把高峰流量存起来慢慢放 | 保护下游数据库 |
| 消息不丢 | ACK + 持久化 + 手动提交 | 各环节怎么保证可靠 |
| 消息不重 | 消费者幂等 | 唯一ID、Redis去重 |
| 消息积压 | 消费太慢,队列堆满了 | 扩容消费者 + 优化消费逻辑 |
| Kafka | 高吞吐日志场景 | 选型理由 |
| RocketMQ | 事务消息交易场景 | 半消息 + 本地事务 + 反查 |
| RabbitMQ | 低延迟路由灵活 | 选型理由 |
| 死信队列 | 重试N次失败后的归宿 | 人工介入处理 |
记住一句话:MQ就是个带存储的异步通信管道,面试围绕的核心就是------怎么让消息安全地从A到B,不丢、不重、不怕压。
下一篇我们讲Spring Boot------自动配置的原理到底是什么?IoC容器就是个Map?AOP的代理是怎么生成的?把Spring的底裤也扒干净。下期见。