Kafka、RabbitMQ、RocketMQ:场景、原理、选型与实战

一个流传很广的对比表长这样:

特性KafkaRabbitMQRocketMQ
吞吐量极高中等高
延迟毫秒级微秒级毫秒级
适用场景日志、流处理、大数据业务解耦、即时通信金融交易、分布式事务

这张表有两处是错的,而且错得很有代表性。但比纠错更重要的是——就算把每个格子都填对,它依然回答不了”我该用哪个”。

因为”吞吐量极高”不是一个可以拿来决策的属性。它取决于你怎么用。

这篇按四层来讲:应用场景 → 底层原理 → 技术选型 → 实际用法。前两层是后两层的地基。

目录


一、应用场景:出身决定了主场

三个产品的设计目标不一样,这决定了它们擅长什么。

1.1 三家的出身

出身原始问题
RabbitMQ2007 年,Erlang 写的,从 AMQP 标准出发“做一个标准化的通用消息中间件”
KafkaLinkedIn 内部,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)
代表RabbitMQKafka、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:

类型路由依据典型用途
directrouting key 精确匹配点对点、按业务类型分发
fanout忽略 key,广播给所有绑定队列广播通知
topicrouting 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 读到这

三条关键性质:

  1. 每条消息有一个固定的 offset。 写进去就不会变。
  2. 消息不因为被消费而删除。 删除只按时间或大小策略来(默认按时间保留 168 小时)。
  3. 消费者自己维护”我读到哪了”——它提交的是 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:它的推是真的推。 这个区别很关键,因为它决定了背压从哪来:

谁决定推送节奏消费者慢了怎么办
RabbitMQbroker靠 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(混合)单一 CommitLogConsumeQueue + 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 毫秒。这个数字站不住,理由有三:

  1. 网络本身就不止。同机房的网络往返通常在几十到几百微秒;跨机房直接到毫秒级。一个”消息从生产者到消费者”的完整链路必然包含至少一次网络往返。
  2. 持久化会显著增加延迟。RabbitMQ 的 quorum queue 要写多副本 + 刷盘,这比内存队列慢一个数量级。
  3. 延迟的决定因素是配置,不是产品。是否批量、是否落盘、几个副本、是否 acks=all——这些的影响远大于选哪个产品。

错误二:「RabbitMQ 适用场景:业务解耦、即时通信」

“即时通信”(IM)用的是 WebSocket 长连接,不是消息队列。

一个消息队列给聊天做即时通信是很别扭的:IM 要求的是”连接在线时低延迟直投”,而 MQ 的模型是”消息在队列里等着被消费”。这可能是把”实时性”误写成了”即时通信”。

RabbitMQ 的经典场景是业务解耦、任务分发、RPC over MQ。

还有一处不算错但缺前提:「吞吐量 极高/中等/高」——见 2.3,Kafka 的高吞吐建立在批量之上。


四、实际用法:Java 里三者差在哪

这一节不重复讲可靠性、重复消费、死信队列这些通用话题——那些在《Java - 消息中间件篇》里已经写过了。这里只讲三家写法上真正不同的地方。

前提:都用 Spring Boot 的 starter。

依赖发送模板消费注解
RabbitMQspring-boot-starter-amqpRabbitTemplate@RabbitListener
Kafkaspring-kafkaKafkaTemplate@KafkaListener
RocketMQrocketmq-spring-boot-starterRocketMQTemplate@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 三家真正不同的三个点

把上面的差异收敛成三条:

差异点RabbitMQKafkaRocketMQ
重试/失败手动 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,不是 MQRabbitMQ 的主场是解耦、任务分发、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 仲裁自己管元数据。

延伸阅读: