为什么要用消息队列?Kafka、RabbitMQ、RocketMQ 怎么选?
一句话回答
消息队列的三个核心作用是解耦(生产者不需要知道谁在消费)、异步(非关键步骤不阻塞主流程)、削峰(突发流量先进队列,消费者按自己的能力处理)。代价是系统更复杂,要处理消息丢失、重复、顺序和积压,还多了一个要保证高可用的组件。选型上:Kafka 是分区日志模型,吞吐高、消息可重放,适合日志、埋点和流处理;RabbitMQ 用交换机灵活路由,功能丰富、延迟低,适合业务消息;RocketMQ 的事务消息、延时消息、重试和死信开箱即用,适合交易类业务。
详细解析
三个核心作用
同步调用:下单 ──> 扣库存 ──> 发优惠券 ──> 发短信 ──> 加积分 ──> 返回
任何一个下游慢或挂了,下单就慢或失败;新增下游要改下单代码
引入 MQ:下单 ──> 扣库存 ──> 发"订单已创建"消息 ──> 返回
│
┌──────────────┼──────────────┐
优惠券服务 短信服务 积分服务(各自订阅、各自重试)
- 解耦:新增一个下游只需要订阅主题,不用改上游代码
- 异步:用户只等核心步骤,响应更快
- 削峰:秒杀时请求先写入队列,后端按固定速度消费,数据库不会被瞬间打垮
引入的问题
| 问题 | 说明 | 详见 |
|---|---|---|
| 可用性 | MQ 挂了,依赖它的链路都受影响,MQ 本身要集群部署 | |
| 消息丢失 | 生产、存储、消费任何一个环节都可能丢 | 消息不丢失 |
| 重复消费 | 至少一次投递必然带来重复 | 重复消费与幂等 |
| 顺序与积压 | 多分区、多消费者天然乱序;消费慢会积压 | 顺序与积压 |
| 一致性 | 上游成功、下游失败,数据只能最终一致 | 分布式事务 |
所以不是越多越好:需要立即拿到结果的调用(比如查询库存)不适合走 MQ。
三者的模型差异
Kafka:主题分成多个分区,每个分区是一个只追加的日志文件,消息按 offset 顺序存储,按保留时间或大小删除,而不是消费后删除。消费者拉取消息,自己记录消费到哪个 offset,所以可以回退 offset 重新消费。同一个消费者组里,一个分区只分配给一个消费者。
RabbitMQ:实现 AMQP 0-9-1。生产者把消息发给交换机,交换机按类型(direct、topic、fanout、headers)和绑定规则把消息路由到队列,Broker 推送给消费者,消息被 ack 后从队列删除。路由灵活,支持优先级队列、TTL、死信交换机等,新版本也提供了可重放的 Stream 类型。
RocketMQ:同样是主题 + 队列的日志模型,消费者组机制和 Kafka 类似。内置事务消息(半消息 + 回查)、延时/定时消息、消费失败自动重试和死信队列、Broker 端按 Tag 或 SQL92 属性过滤,这些都是业务开发常用的功能。它的 Push 消费者底层也是长轮询拉取,只是 SDK 封装成了推的形式。
选型对比
| 维度 | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|
| 存储模型 | 分区日志,按保留策略删除 | 队列,ack 后删除 | 日志 + 消费队列 |
| 消费方式 | 拉 | 推(也支持拉) | 拉和推都有封装 |
| 吞吐 | 很高,批量和顺序写磁盘做得极致 | 中等 | 高 |
| 路由能力 | 只按主题和分区 | 最灵活 | Tag 或 SQL92 属性过滤 |
| 消息重放 | 支持(重置 offset) | 普通队列不支持,Stream 支持 | 支持(按时间重置位点) |
| 事务消息 | 有事务,但语义是跨分区原子写 | 无(AMQP 信道事务不是这个语义) | 有,专为本地事务 + 发消息设计 |
| 延时消息 | 无原生支持 | TTL + 死信,或插件 | 原生支持(早期版本只有固定的延时级别) |
| 生态 | 流处理生态最完整(Kafka Streams、Flink 等) | 多语言客户端成熟 | Java 生态最成熟 |
选型建议:
- 日志采集、埋点、CDC、流式计算:Kafka
- 业务消息、复杂路由、对延迟敏感、规模中等:RabbitMQ
- 电商交易、需要事务消息和延时消息:RocketMQ
- 云上项目也可以直接用托管服务,少一份运维成本
"Kafka 吞吐最高"是架构决定的(顺序写、批量、零拷贝,见 零拷贝),具体数字取决于消息大小、副本数、确认级别和硬件,不要背网上的数字,要在自己的环境里压测。
面试官可能追问
Kafka 为什么快?
主要有几点:消息顺序追加写磁盘,顺序写速度远高于随机写;读写大量利用操作系统的页缓存;消费时通过 sendfile 零拷贝把数据从页缓存直接发到网卡;生产和消费都按批处理,并支持批量压缩;主题分成多个分区,可以在多台 Broker 上并行读写。
RabbitMQ 怎么实现延时消息?
两种方式。一是给消息或队列设置 TTL,不设消费者,消息过期后通过死信交换机转到真正的处理队列,缺点是队列只检查队头消息是否过期,不同 TTL 的消息放在同一个队列会互相阻塞;二是延时消息交换机插件(rabbitmq-delayed-message-exchange),它的限制不少:待投递的消息只存在一个节点上,这个节点丢了消息就丢了;不支持 mandatory;不适合几十万以上的大量延时消息,官方现在也已停止维护它,新项目要先查当前版本有没有原生的替代方案。大量、长时间的延时任务也可以用数据库或 Redis 有序集合加定时扫描来做。
什么时候不该用消息队列?
调用方需要同步拿到结果时;业务量小、链路简单,多一个组件带来的运维和排障成本大于收益时;强一致要求的操作(比如扣款后立即要看到余额)也不适合拆成异步。另外,Node.js 服务内部的后台任务可以先考虑基于 Redis 的任务队列,见 任务队列。
易错点
- 把消息队列当万能的解耦工具,所有调用都改成异步,排查问题和保证一致性都会变难
- 认为 Kafka 消费完消息就删除,实际是按保留策略删除,与是否消费无关
- 背诵具体的吞吐量数字,这些数字和测试条件强相关
- 混淆 Kafka 的事务和 RocketMQ 的事务消息,两者解决的问题不同
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。