一个流传很广的对比表长这样:
| 特性 | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|
| 吞吐量 | 极高 | 中等 | 高 |
| 延迟 | 毫秒级 | 微秒级 | 毫秒级 |
| 适用场景 | 日志、流处理、大数据 | 业务解耦、即时通信 | 金融交易、分布式事务 |
这张表有两处是错的,而且错得很有代表性。但比纠错更重要的是——就算把每个格子都填对,它依然回答不了”我该用哪个”。
因为”吞吐量极高”不是一个可以拿来决策的属性。它取决于你怎么用。
这篇按四层来讲:应用场景 → 底层原理 → 技术选型 → 实际用法。前两层是后两层的地基。
目录
一、应用场景:出身决定了主场
三个产品的设计目标不一样,这决定了它们擅长什么。
1.1 三家的出身
| 出身 | 原始问题 | |
|---|---|---|
| RabbitMQ | 2007 年,Erlang 写的,从 AMQP 标准出发 | “做一个标准化的通用消息中间件” |
| Kafka | LinkedIn 内部,2011 年开源 | “把日志高吞吐地收集起来喂给下游” |
| RocketMQ | 阿里内部(MetaQ → RocketMQ),2012 年开源 | “扛住电商交易的流量和一致性要求” |
出身决定了主场:
- RabbitMQ 是从协议标准倒推实现的,所以它的路由模型最丰富、消息语义最”标准”。主场是业务解耦和任务分发。
- Kafka 是从日志聚合长出来的。日志的特点是只追加、不删除、可重读——这个出身决定了它后来能变成”事件流平台”。主场是数据管道、流处理、CDC。
- RocketMQ 是从交易场景长出来的,所以它把事务消息和严格顺序做进了核心。主场是订单、支付、削峰。
1.2 消息队列要解决的三个模式
抛开产品,消息队列真正在解决的问题只有三类:
① 解耦 —— A 发出事件,B、C、D 各自决定怎么处理。A 不需要知道有谁在听。
订单服务 ──发布"订单已创建"──> MQ ──┬──> 库存服务
├──> 积分服务
└──> 通知服务
② 削峰 —— 前端瞬时 10 万请求,后端只有每秒 1 万的容量。中间放个队列当蓄水池,后端按自己的速度拉。
③ 事件流 —— 把”发生了什么”当作可重放的历史记录。新服务上线时可以从头读一遍历史,重建自己的状态。
1.3 关键分野
前两个模式,三个产品都能做。第三个只有 Kafka 和 RocketMQ 能好好做。
原因不在性能,在消息会不会被删掉。这正是下一章的主题。
二、底层原理:队列和日志,两种世界观
这一章是全文的核心。三个产品的所有差异——能不能重放、要不要 prefetch、吞吐量差在哪——都从这一个分歧长出来。
2.1 分歧:消息是”任务”还是”记录”
| 队列(Queue) | 日志(Log) | |
|---|---|---|
| 消息的隐喻 | 一个待办任务 | 一条历史记录 |
| 被消费之后 | 删除 | 保留(按时间/大小过期) |
| 能否重读 | ❌ | ✅ 把位置退回去即可 |
| 谁记住”读到哪了” | broker(ack 即删除) | 消费者自己(维护 offset) |
| 代表 | RabbitMQ | Kafka、RocketMQ |
RabbitMQ 是队列,Kafka 和 RocketMQ 是日志(RocketMQ 的形态更复杂,见 2.4)。
2.2 RabbitMQ:消息投出去就没了
RabbitMQ 的模型是”消息被投递出去、被 ack 之后,就从队列里删除”。
路由模型(这是 RabbitMQ 最强的地方):
graph LR
P[生产者] --> E{Exchange}
E -->|"binding: routing key"| Q1[队列 1]
E --> Q2[队列 2]
Q1 --> C1[消费者 A]
Q2 --> C2[消费者 B]
四种 Exchange:
| 类型 | 路由依据 | 典型用途 |
|---|---|---|
| direct | routing key 精确匹配 | 点对点、按业务类型分发 |
| fanout | 忽略 key,广播给所有绑定队列 | 广播通知 |
| topic | routing key 按通配符模式匹配 | 选择性订阅(order.*.created) |
| headers | 按消息头匹配(忽略 key) | 按属性路由 |
顺带说个容易踩的坑:默认 Exchange(名字是空字符串)是个特殊的 direct exchange,每个新队列会自动用队列名作为 routing key 绑定上去。所以”直接发到队列”这件事,实际是发到默认 exchange、再用队列名做 key。RabbitMQ 会拒绝对默认 exchange 做 bind/unbind 操作。
投递是 broker 推的。 官方文档把两种消费方式讲得很直白:
- Push(推荐):订阅后由 broker 把消息投递过来。
- Pull:轮询拉取——官方原话是 “highly inefficient”,“should be avoided”。
推模型带来一个直接问题:消费者慢了怎么办?
broker 主动推,如果消费者处理不过来,消息会堆在消费者这边。RabbitMQ 的流控手段是 prefetch:限制一个消费者在下次 ack 之前最多能收到多少条未确认消息。
这里有个实现细节值得记住:RabbitMQ 的 prefetch 只支持 channel 级别,不支持连接级别或按消息大小。所以调优的时候,你要按 channel 去算,而不是按连接。
2.3 Kafka:消息是日志,消费者自己记位置
Kafka 的 partition 本质上是一个只能追加(append-only)的日志文件。
partition-0: [msg0][msg1][msg2][msg3][msg4] ...
offset: 0 1 2 3 4
▲
消费者 A 读到这
三条关键性质:
- 每条消息有一个固定的 offset。 写进去就不会变。
- 消息不因为被消费而删除。 删除只按时间或大小策略来(默认按时间保留 168 小时)。
- 消费者自己维护”我读到哪了”——它提交的是 offset,不是 ack。
这三条合起来,就有了”重放”能力:把 offset 退回到 0,重新读一遍。新服务上线时重建状态、排查问题、做数据回算,全靠这个。
消费是拉的:消费者主动发 fetch 请求。
那 Kafka 的高吞吐从哪来?
这不是”它代码写得快”,而是几个设计叠出来的:
| 设计 | 作用 |
|---|---|
| 顺序写 | 磁盘顺序写的速度接近内存随机写——这是日志结构最大的红利 |
| 页缓存(page cache) | 数据留在操作系统缓存里,读写不都落盘 |
| 零拷贝(sendfile) | 数据从页缓存直接到网卡,不经过用户空间 |
| 批量 + 压缩 | 一次网络往返发一批,压缩摊薄 |
| 分区 | 水平扩展,突破单机上限 |
⚠️ 注意”极高吞吐”是有前提的:它建立在批量之上。如果把批量关掉、每条消息同步等确认、每个请求单独落盘,Kafka 一点也”不高”。拿”吞吐量极高”去选型,等于忽略了这个数字的成立条件。
2.4 RocketMQ:混合型存储
RocketMQ 的存储是数据和索引分离的混合结构。
核心是三个文件(以下数字来自官方设计文档):
| 文件 | 内容 | 结构 |
|---|---|---|
| CommitLog | 消息本体 + 元数据 | 所有 Topic 的消息都写进同一个文件,顺序写,单文件默认 1G |
| ConsumeQueue | 消费索引 | 每条 20 字节 = 8 字节 CommitLog 物理偏移 + 4 字节消息长度 + 8 字节 Tag 哈希 |
| IndexFile | 按 key / 时间查询 | 文件系统里实现的 HashMap,约 400M,可存 2000 万条索引 |
为什么要分成 CommitLog + ConsumeQueue? 官方给的理由很实在:
由于 RocketMQ 是基于 Topic 的订阅模式,消息消费是针对 Topic 进行的,如果要遍历 CommitLog 文件、根据 Topic 检索消息是非常低效的。
所以 Broker 起了一个后台线程(ReputMessageService)异步地把 CommitLog 的内容分发出去,构建 ConsumeQueue 和 IndexFile。消费时先从 ConsumeQueue 拿到索引(顺序读,几乎等于读内存),再按索引回 CommitLog 取消息本体(这一步是随机读)。
消费模型是拉 + 长轮询:拉不到消息时,Broker 允许把这个请求挂起最多 30 秒,期间只要有新消息到达就立刻返回。这样既避免了空轮询,又不像推模型那样需要 broker 主动维护推送状态。
最有意思的一点:“Push” 其实是假的
RocketMQ 的客户端 API 分 Push 和 Pull 两种。但官方设计文档里有一句原话:
RocketMQ 中 Consumer 端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,而 Push 模式只是对 Pull 模式的一种封装。其本质实现为:消息拉取线程从服务器拉取到一批消息后,提交到消息消费线程池,然后又”马不停蹄”地继续向服务器尝试拉取消息。
所以 RocketMQ 的 “Push” 是一个 API 层面的假象——底下是一个拉取线程在拼命拉。
对比 RabbitMQ:它的推是真的推。 这个区别很关键,因为它决定了背压从哪来:
| 谁决定推送节奏 | 消费者慢了怎么办 | |
|---|---|---|
| RabbitMQ | broker | 靠 prefetch 限制未 ack 数量 |
| Kafka | 消费者 | 消费者少拉几次,天然背压 |
| RocketMQ | 消费者(Push 是封装) | 长轮询 + 消费线程池 |
拉模型的天然好处是背压:消费者处理不过来时,“少拉一点”是免费的。推模型则需要 broker 主动做保护——这就是 prefetch 存在的理由。
2.5 RocketMQ 的事务消息
这是 RocketMQ 相对另两家最独特的能力,值得单独说。
它解决的是”本地事务和消息发送必须同时成功或同时失败”:
// 伪代码:扣款和发消息要原子
@Transactional
public void pay() {
deductBalance(); // 本地事务
mq.send("支付成功"); // 这个消息必须发出去,且只发一次生效
}
如果本地事务提交了但消息发失败,下游永远不知道;如果消息发出去了但本地事务回滚,下游收到一个假消息。
RocketMQ 的做法是两阶段:
第一阶段:发一个 half 消息(半消息)。做法是——把消息备份原 Topic 和队列,然后把 Topic 改成 RMQ_SYS_TRANS_HALF_TOPIC。因为没有消费组订阅这个 Topic,所以消费端根本看不到它。
第二阶段:本地事务执行完后,向 Broker 提交 commit 或 rollback。
关键在超时兜底:如果 Broker 迟迟收不到确认,会起一个定时任务去 RMQ_SYS_TRANS_HALF_TOPIC 里拉消息,然后反向回查生产者”这笔事务到底成没成”,再据此决定提交还是回滚。
这就是”事务消息”的完整闭环——本地事务状态不明时,不是猜,而是回查。
2.6 三家存储模型对比
| 消息本体 | 索引 | 消息会被删吗 | 能重放吗 | |
|---|---|---|---|---|
| RabbitMQ(队列) | 队列内 | 无独立索引层 | ✅ ack 后删除 | ❌ |
| Kafka(日志) | partition 日志 | offset 即位置 | ❌ 按时间/大小过期 | ✅ |
| RocketMQ(混合) | 单一 CommitLog | ConsumeQueue + IndexFile | ❌ 按时间过期 | ✅ |
三、技术选型:六个问题
把上一章的原理翻译成可操作的判据。
3.1 六个问题
① 消息需要重放吗?
- 需要(新服务重建状态、问题排查回放、数据回算、流处理)→ Kafka 或 RocketMQ
- 不需要(就是一问一答的任务分发)→ 三个都行
这一条是最硬的分界线。RabbitMQ 的消息 ack 之后就没了,这件事改不了,只能靠自己在业务层再存一份。
② 需要复杂的路由吗?
- 需要(一个消息按不同规则扇出到多个队列、按 header 路由、通配符订阅)→ RabbitMQ
- 只是简单的 topic 订阅 → 三个都行
③ 需要事务消息吗?
- 需要(本地事务和消息发送要原子)→ RocketMQ(Kafka 有事务 API,但语义不同,主要面向流处理场景的 exactly-once,不是”本地事务 + 消息”这个模式)
④ 延迟敏感,但消息量不大?
→ RabbitMQ。它是三个里最”轻”的——不需要为了一个通知消息去维护 partition 和 offset。
⑤ 吞吐量优先 / 要接大数据生态?
→ Kafka。Flink、Spark、各种 CDC 工具都原生对接它。
⑥ 顺序怎么保证?
| 顺序范围 | 怎么保证 | |
|---|---|---|
| Kafka | 分区内有序 | 相同 key 进同一分区 |
| RocketMQ | 队列内有序 | 顺序消费模式 |
| RabbitMQ | 单消费者才有 | 一个队列只能有一个活跃消费者 |
注意:三个产品都不能保证全局有序。 想要全局有序只能用一个分区/队列,那就放弃了并行——这是数学约束,不是产品缺陷。
3.2 关于那张表
回到开头那张表,两处错误值得单独讲:
错误一:「RabbitMQ 延迟 微秒级」
我去翻了 rabbitmq.com 的官方文档——全站没有任何延迟或性能数字。
“微秒级”意味着端到端延迟小于 1 毫秒。这个数字站不住,理由有三:
- 网络本身就不止。同机房的网络往返通常在几十到几百微秒;跨机房直接到毫秒级。一个”消息从生产者到消费者”的完整链路必然包含至少一次网络往返。
- 持久化会显著增加延迟。RabbitMQ 的 quorum queue 要写多副本 + 刷盘,这比内存队列慢一个数量级。
- 延迟的决定因素是配置,不是产品。是否批量、是否落盘、几个副本、是否 acks=all——这些的影响远大于选哪个产品。
错误二:「RabbitMQ 适用场景:业务解耦、即时通信」
“即时通信”(IM)用的是 WebSocket 长连接,不是消息队列。
一个消息队列给聊天做即时通信是很别扭的:IM 要求的是”连接在线时低延迟直投”,而 MQ 的模型是”消息在队列里等着被消费”。这可能是把”实时性”误写成了”即时通信”。
RabbitMQ 的经典场景是业务解耦、任务分发、RPC over MQ。
还有一处不算错但缺前提:「吞吐量 极高/中等/高」——见 2.3,Kafka 的高吞吐建立在批量之上。
四、实际用法:Java 里三者差在哪
这一节不重复讲可靠性、重复消费、死信队列这些通用话题——那些在《Java - 消息中间件篇》里已经写过了。这里只讲三家写法上真正不同的地方。
前提:都用 Spring Boot 的 starter。
| 依赖 | 发送模板 | 消费注解 | |
|---|---|---|---|
| RabbitMQ | spring-boot-starter-amqp | RabbitTemplate | @RabbitListener |
| Kafka | spring-kafka | KafkaTemplate | @KafkaListener |
| RocketMQ | rocketmq-spring-boot-starter | RocketMQTemplate | @RocketMQMessageListener |
4.1 发送:三家的”地址”概念不一样
这是最容易困惑的地方——同样是”发消息”,三个产品的寻址模型完全不同。
RabbitMQ 是”交换机 + routing key”:
// 发到名为 order.exchange 的交换机,routing key 是 order.created
rabbitTemplate.convertAndSend("order.exchange", "order.created", orderDto);
你发的是”routing key”,不是”队列”——消息最终进哪个队列,由绑定规则决定。这是 RabbitMQ 最强也最容易搞混的地方。
Kafka 是”topic + 可选 key”:
// key 决定进哪个分区;key 相同 → 同一分区 → 分区内有序
kafkaTemplate.send("order-created", order.getUserId(), orderDto);
key 不只是”分区选择器”——它是顺序保证的实现手段。同一个用户的订单进同一分区,就能保证这个用户的订单按序处理。
RocketMQ 是”topic + tag”:
// topic:order 是目的地,tag 是过滤标签
rocketMQTemplate.syncSend("order:created", orderDto);
tag 是 RocketMQ 特有的过滤维度。它的实现在 ConsumeQueue 里那 8 字节的 tag hash——服务端先按 hash 粗过滤,消费者拉回来之后再比对原始 tag 字符串做精确过滤。
这里有个细节值得知道:因为服务端只有 hash,同一个 hash 对应不同 tag 时会误投。所以消费端必须再做一次精确比对——这是存储结构的直接后果。
4.2 消费:三家的”确认”语义不一样
RabbitMQ:ack 一条删一条
@RabbitListener(queues = "order.queue")
public void handle(OrderDto dto, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
try {
businessService.process(dto);
channel.basicAck(tag, false); // 确认 → 消息从队列删除
} catch (Exception e) {
channel.basicNack(tag, false, false); // 拒绝 → 进死信队列
}
}
语义是”任务完成”。ack 之后就没了。
Kafka:提交 offset
@KafkaListener(topics = "order-created", groupId = "order-group")
public void handle(ConsumerRecord<String, OrderDto> record, Acknowledgment ack) {
businessService.process(record.value());
ack.acknowledge(); // 提交 offset → 只是"我读到这了"
}
语义是”我读到这了”。消息还在,把 offset 退回去就能重读。
RocketMQ:返回状态
@RocketMQMessageListener(
topic = "order",
consumerGroup = "order-group",
consumeMode = ConsumeMode.ORDERLY // 顺序消费
)
public class OrderConsumer implements RocketMQListener<OrderDto> {
@Override
public void onMessage(OrderDto dto) {
businessService.process(dto);
// 正常返回 = 消费成功;抛异常 = 触发重试
}
}
不需要手动 ack——正常返回即为成功,抛异常则重试。
4.3 三家真正不同的三个点
把上面的差异收敛成三条:
| 差异点 | RabbitMQ | Kafka | RocketMQ |
|---|---|---|---|
| 重试/失败 | 手动 nack → 死信队列 | 靠业务自己处理,offset 提交了就跳过 | 框架内置重试 + 死信 |
| 顺序 | 单活跃消费者(牺牲并行) | 同 key 进同分区 | ConsumeMode.ORDERLY |
| 重放 | 做不到 | 重置 offset | 重置消费位点 |
最需要记住的一条:Kafka 的失败处理要靠自己。 抛出异常不会自动”把消息放回去”——如果你在异常之前已经提交了 offset,这条消息就被跳过了。这是 Kafka 最容易出事的地方,也正好是”日志”模型(而不是”队列”模型)的直接后果。
五、自检清单
- 三个产品里,哪两个是”日志”、哪一个是”队列”?这个区别决定了什么能力?
- RabbitMQ 的消息在 ack 之后去哪了?为什么它做不到重放?
- 为什么 RabbitMQ 需要 prefetch,而 Kafka 不需要?
- RocketMQ 的 “Push 模式”底层是什么?官方文档怎么说的?
- RocketMQ 为什么要分 CommitLog 和 ConsumeQueue?不分会怎样?
- RocketMQ 事务消息里,half 消息为什么消费不到?超时了怎么兜底?
- Kafka 的”吞吐量极高”成立的前提是什么?
- “RabbitMQ 延迟微秒级”这个说法在哪站不住?
- 三个产品能不能保证全局有序?为什么?
- Kafka 里消费失败抛异常,消息会自动重投吗?
附:三个被传抄错的数字
| 说法 | 问题 | 事实 |
|---|---|---|
| RabbitMQ 延迟”微秒级” | 端到端 <1ms 不成立,且官方文档无任何性能数字 | 延迟由批量/刷盘/副本数决定,不由产品名决定 |
| RabbitMQ 场景含”即时通信” | IM 用 WebSocket,不是 MQ | RabbitMQ 的主场是解耦、任务分发、RPC over MQ |
| 吞吐量”极高/中等/高” | 缺少成立前提 | Kafka 的高吞吐依赖批量 + 顺序写 + 零拷贝;关掉批量就不成立 |
本节的证据来源:
- RabbitMQ 队列类型、协议、push/pull 语义:rabbitmq.com 官方文档(当前版本 4.3.6)。官方明确:classic mirrored queue 的复制能力已在 4.x 移除;非持久非独占的 classic queue 自 4.3.0 起默认不可声明;Pull API “highly inefficient”。
- Kafka 移除 ZooKeeper:官方 4.0 发布公告原话——“the first major release to operate entirely without Apache ZooKeeper”,并称 ZooKeeper “was the backbone of Kafka for more than 10 years”。背景见 KIP-500。
- RocketMQ 存储结构、Push/Pull 封装、事务消息流程:官方设计文档。当前版本 5.5.0。
另外两个容易搞混的点:
- AMQP 0-9-1 和 AMQP 1.0 共用名字,线缆层毫无关系。 RabbitMQ 官方原话是 “a very different protocol”,“sharing nothing at the wire level”。1.0 是 ISO/OASIS 标准,RabbitMQ 4.0 起才成为核心协议。
- Kafka 4.x 起没有 ZooKeeper 了。 如果你看到的部署文档还在讲”先起 ZK 再起 Kafka”,那是 3.x 及以前的做法。KRaft 模式下由三个 controller 节点组成 Raft 仲裁自己管元数据。
延伸阅读:
- 《Java - 消息中间件篇》 —— 可靠性、重复消费、死信队列、消息堆积的通用处理
- 《什么技术用什么通信》 —— 消息队列为什么不用 HTTP,以及通信协议的选型判据