MQ 选型最难的不是比吞吐,而是先判断你要的是事件日志、任务队列,还是业务消息
MQ 选型最难的不是比吞吐,而是先判断你要的是事件日志、任务队列,还是业务消息
一个订单系统准备引入 MQ。
架构评审刚开始,讨论很容易进入一张参数表:Kafka 吞吐高,RabbitMQ 路由灵活,RocketMQ 支持顺序、延迟和事务消息。有人说 Kafka 生态成熟,有人说 RocketMQ 更适合业务系统,也有人觉得 RabbitMQ 简单好用。
这些判断都没错,但如果只停在这里,选型很容易跑偏。
因为系统上线以后,真正让人头疼的问题往往不是“TPS 不够”,而是这些更具体的问题:
- 订单创建消息能不能重复消费?
- 支付成功消息要不要严格顺序?
- 库存扣减失败以后怎么重试?
- 消息已经发出去了,本地事务却失败了怎么办?
- 消费者上线晚了,历史消息还要不要重新跑?
- 消息堆积以后,是扩消费者、扩分区、拆队列,还是拆 Topic?
- 这条消息到底是一条业务事实,还是一项待处理任务?
到这一步才会发现,MQ 不是一个简单的“异步工具”。
它会决定系统如何记录事实、如何分发任务、如何推进状态、如何处理失败。
所以这篇文章不想问“Kafka、RabbitMQ、RocketMQ 谁更好”,而是换一个问题:
当你发出一条消息时,你到底希望系统记住什么、投递什么、重试什么、回放什么?
这才是 MQ 选型的起点。
1. 三者不是同一种 MQ,只是都叫消息中间件
很多人把 MQ 当成“发消息和收消息”的工具,但不同 MQ 对“消息”的理解完全不同。
如果只看 API,三者都可以发消息、收消息、异步解耦、削峰填谷。
但从架构模型看,它们解决的是三类不同问题:
| 中间件 | 更像什么 | 核心模型 | 最擅长回答的问题 |
|---|---|---|---|
| Kafka | 事件日志 | Topic、Partition、Offset、Consumer Group | 发生过什么,谁消费到哪里了,能不能重放 |
| RabbitMQ | 任务队列 | Exchange、Queue、Binding、Ack | 这条任务该投给谁,是否处理完成 |
| RocketMQ | 业务消息平台 | Topic、MessageQueue、Producer、Consumer Group | 业务消息如何可靠投递、重试、顺序、延迟和事务化 |
这张表比吞吐表更重要。
因为业务系统里所谓“消息”,其实可能是三种东西。
第一种是事件。
比如用户注册了、订单创建了、支付成功了、额度变化了。这类消息更像一条事实记录。事实一旦发生,后面可能会有很多系统关心它:风控要看,报表要看,推荐要看,数据仓库也要看。它的关键不是“交给谁处理完”,而是“发生过什么,以及后面能不能重新读取”。
第二种是任务。
比如发送短信、发送邮件、生成报表、调用三方接口、推送通知。这类消息更像一项待处理工作。它的关键不是长期保留历史,而是“交给哪个 worker,处理成功没有,失败了要不要重回队列或者进入死信”。
第三种是业务状态推进。
比如订单超时关闭、支付成功后入账、库存扣减后通知履约、还款成功后冲账。这类消息不只是异步通知,它参与了交易链路的状态变化。它的关键是顺序、延迟、事务、重试、死信和可恢复。
所以一个更准确的判断是:
Kafka 解决“事件流怎么存和消费”,RabbitMQ 解决“任务怎么路由和确认”,RocketMQ 解决“业务消息怎么可靠推进”。
如果一开始没有把这三类消息分清楚,后面所有参数对比都会变得很虚。
2. Kafka:核心不是队列,而是可回放的事件日志
如果你把 Kafka 只当成一个高吞吐队列,就会忽略它最重要的能力:保留事件历史,让不同消费者按自己的节奏读取。
Kafka 的核心不是“消息被消费者拿走”。
它更像是把消息追加到一条日志里。
Producer 把消息写入 Topic。Topic 会被拆成多个 Partition。每个 Partition 内部是一条有序追加的日志。Consumer 读取消息时,并不是把消息从 Broker 里删除,而是根据 Offset 记录自己读到了哪里。
这几个概念决定了 Kafka 的性格。
Producer -> Topic -> Partition 0: msg0, msg1, msg2, msg3 ... -> Partition 1: msg0, msg1, msg2, msg3 ... -> Consumer Group A: 记录自己的 offset -> Consumer Group B: 记录自己的 offsetKafka 为什么适合事件流和回放?
原因不在一句“吞吐高”,而在它的几个底层机制。
第一,消息消费和消息保留是分离的。
在很多队列模型里,消息被消费确认以后,就倾向于从队列里移除。Kafka 不是这个思路。Kafka 的消息会按照保留策略留在日志里,消费者只是记录自己的 Offset。
这意味着,一个消费者读完了,不代表其他消费者不能读。
也意味着,一个新系统晚几天上线,只要消息还在保留期内,它可以从过去的位置重新消费。
第二,消费进度属于 Consumer Group。
同一个 Topic 可以被多个 Consumer Group 消费。风控系统有自己的消费进度,报表系统有自己的消费进度,数据同步系统也有自己的消费进度。它们互不影响。
这对事件流非常关键。
因为一个事件发生以后,经常不是一个下游关心,而是一批下游按不同节奏关心。
第三,Partition 同时承担顺序边界和并发边界。
同一个 Partition 内部有序,不同 Partition 可以并行。
如果你按orderId、userId这类 key 分区,就可以让同一个业务对象的事件落到同一个 Partition,从而获得局部顺序。与此同时,多个 Partition 又能让整个 Topic 横向扩展。
这就是 Kafka 适合日志采集、CDC、用户行为流、订单事件流、实时计算的原因。
这些场景的核心问题不是:
这条任务交给谁处理?
而是:
这个事实发生过,后面很多系统可能都要读取它,有的现在读,有的以后读,有的还要重放。
举个例子,订单状态流可以这样进入 Kafka:
OrderCreated OrderPaid OrderShipped OrderFinished OrderRefunded风控系统只关心其中一部分事件。
数据仓库可能全量消费。
搜索系统可能消费订单创建和取消。
如果某个消费者代码有 bug,修复以后还可以把 Offset 调回去重新消费。
这就是事件日志模型的价值。
但也正因为 Kafka 的核心是日志,它并不是天然围绕“单条任务处理完成”设计的。
比如你想做下面这些事情:
- 复杂路由,把不同消息按规则投到不同队列;
- 单条任务失败以后马上进入某个死信队列;
- 每条消息有明确的 ack / nack 处理语义;
- 延迟 30 分钟以后再触发某个业务动作;
- 本地事务成功以后再确认消息可见。
Kafka 不是不能做,而是很多能力需要应用层、框架层或周边组件一起补。
所以对 Kafka 更准确的理解是:
当消息代表“系统发生过的事实”,并且这个事实以后还可能被多个系统重复读取,Kafka 往往更合适。
3. RabbitMQ:核心不是日志,而是灵活路由和任务确认
RabbitMQ 更关心的不是“历史事件能不能回放”,而是“这条消息应该被路由到哪个队列,并且有没有被处理完成”。
RabbitMQ 的模型和 Kafka 很不一样。
Producer 通常不是直接把消息写到某个 Queue,而是先把消息发到 Exchange。Exchange 根据类型、Routing Key 和 Binding 规则,把消息路由到一个或多个 Queue。Consumer 从 Queue 中取消息,处理完成以后通过 Ack 确认。
可以把它理解成:
Producer -> Exchange -> Binding rule A -> Queue A -> Consumer A -> Binding rule B -> Queue B -> Consumer B -> Binding rule C -> Queue C -> Consumer CRabbitMQ 为什么适合任务分发和复杂路由?
因为它的核心模型就是“先路由,再排队,再确认”。
第一,Exchange 负责路由。
RabbitMQ 的 exchange 可以有 direct、fanout、topic、headers 等类型。direct 适合精确匹配,fanout 适合广播,topic 适合基于模式的路由。
这让 RabbitMQ 很适合表达“不同消息交给不同队列”。
比如通知系统里:
notify.sms -> 短信队列 notify.email -> 邮件队列 notify.push -> App 推送队列 notify.audit.* -> 审计队列这类路由在 RabbitMQ 里是它的主模型,不是额外补出来的能力。
第二,Queue 负责排队。
消费者处理不过来,任务可以在队列里等。
如果短信服务暂时慢了,短信队列积压;如果邮件服务正常,邮件队列继续消费。不同任务可以被隔离在不同队列里。
第三,Ack / Nack 负责确认。
Consumer 处理成功以后 Ack,Broker 才知道这条消息可以从待处理集合中移除。Consumer 处理失败,可以 Nack、Reject、重回队列,或者通过死信交换机进入失败处理路径。
这件事对任务队列很重要。
因为任务系统最关心的是:
这个活到底干完了没有?
第四,Prefetch 可以控制消费者压力。
如果一个消费者处理很慢,不应该一下子塞给它太多未确认消息。RabbitMQ 可以通过 prefetch 限制单个消费者同时持有的未确认消息数量。这对后台任务、通知发送、三方接口调用都很实用。
所以 RabbitMQ 很适合这些场景:
- 邮件、短信、App 推送;
- 后台任务分发;
- 请求削峰;
- 多规则路由投递;
- 异步 RPC 或轻量服务解耦;
- 消费失败后需要明确 ack、nack、死信路径。
但 RabbitMQ 不应该被简单理解成“轻量版 Kafka”。
它的主模型不是长期事件日志。
消息一旦被确认消费,队列模型就倾向于把它从待处理集合中移除。虽然现代 RabbitMQ 也有 quorum queue、stream 等能力,可以覆盖高可用队列和部分流式场景,但 RabbitMQ 最自然的思路仍然是任务路由和处理确认。
如果你要做的是大规模用户行为日志、CDC 数据流、多个消费者独立回放历史事件,RabbitMQ 就会显得不够顺手。
它不是不能传这些消息,而是模型不贴。
对 RabbitMQ 更准确的判断是:
当消息更像一项“待处理任务”,并且系统重点是路由、确认、失败转移和任务分发,RabbitMQ 往往更合适。
4. RocketMQ:核心是面向业务系统的消息语义
RocketMQ 的价值不只是“能发消息”,而是把很多交易系统常见的消息语义做成了一等能力。
如果说 Kafka 更像事件日志,RabbitMQ 更像任务队列,那么 RocketMQ 更像一套面向业务系统的消息平台。
它也有 Topic、MessageQueue、Producer、Consumer Group 这些基本概念,但它更容易被业务系统记住的,往往是这些能力:
- 普通消息;
- FIFO / 顺序消息;
- 延迟消息;
- 事务消息;
- 消费重试;
- 死信消息。
这些能力为什么重要?
因为订单、支付、库存、信贷这类系统里,消息经常不是简单通知,而是在推进业务状态。
顺序消息:同一个业务对象的状态不能乱
订单状态不能乱序。
你不能先消费OrderPaid,再消费OrderCreated。也不能库存先释放,再扣减。对同一个业务对象来说,状态推进有前后关系。
RocketMQ 的 FIFO 消息可以通过消息组等机制,把同一组消息按顺序投递和消费。这里的关键不是全局有序,而是业务对象级别的局部有序。
在真实系统里,我们通常也不需要全局有序。
全局有序代价太高,也没有必要。
更常见的需求是:
orderId = 1001 的消息按顺序 orderId = 1002 的消息按顺序 orderId = 1001 和 1002 之间可以并行这正好是交易系统最常见的顺序诉求。
延迟消息:不是现在处理,而是到点再处理
很多业务消息不是立刻消费,而是到某个时间点再触发。
比如:
- 订单创建 30 分钟未支付,自动关单;
- 支付发起后 10 分钟查询最终结果;
- 还款日前一天发送提醒;
- 优惠券领取后到期前提醒;
- 风控审核超时后触发补偿。
如果没有延迟消息,业务系统通常会自己维护一张定时任务表,再跑定时扫描。
这当然能做,但系统会多出一套补偿调度逻辑。
RocketMQ 把延迟消息作为消息类型支持,业务系统可以更自然地表达:
这条业务消息现在产生,但应该未来某个时间点再被消费。
事务消息:本地事务和消息发送不能各说各话
交易系统里最常见的问题之一是:
数据库事务成功了,但消息没发出去怎么办?
消息发出去了,但本地事务回滚了怎么办?
比如订单创建成功以后要发送OrderCreated消息。
如果先写库再发消息,写库成功但发消息失败,下游就不知道订单创建了。
如果先发消息再写库,消息已经出去了,但订单事务失败,下游就会看到一条不存在的订单。
RocketMQ 的事务消息用半消息、提交、回滚、事务回查这套机制来缓解这个问题。
它不是让分布式事务从此没有成本,而是给业务系统一个更标准的模式:
发送半消息 -> 执行本地事务 -> 本地事务成功:提交消息 -> 本地事务失败:回滚消息 -> 状态未知:Broker 回查事务状态这对订单、支付、库存这类系统很有价值。
因为这些场景最怕的不是失败,而是本地状态和消息状态不一致。
重试和死信:失败以后还要能恢复
业务消费失败很常见。
下游系统超时、数据库短暂不可用、三方接口失败、业务状态还没准备好,都可能导致消费失败。
RocketMQ 的消费重试和死信机制,本质上是在回答:
这条消息没处理成功,系统接下来怎么办?
它可以根据消费结果触发重试。多次重试仍然失败后,进入死信队列,等待后续人工或专门任务处理。
这对交易系统很关键。
因为交易系统里的失败通常不是“丢掉算了”,而是要留下恢复入口。
所以 RocketMQ 适合订单、支付、库存、信贷这类业务系统,不是因为它只能做这些,而是因为这些系统最常见的难点正好是:
- 顺序;
- 延迟;
- 事务;
- 重试;
- 死信;
- 状态推进。
反过来,如果你的场景只是海量日志采集、用户行为流分析、CDC 到数据湖,RocketMQ 也能传消息,但它的优势不会像在交易型业务消息里那么突出。
对 RocketMQ 更准确的判断是:
当消息直接参与业务状态推进,并且顺序、延迟、事务、重试是主问题,RocketMQ 往往更贴近业务系统。
5. 不同业务场景,真正要问的问题不一样
MQ 选型不能从产品名开始,而要从消息在业务链路中的职责开始。
同样叫“订单消息”,它可能代表完全不同的东西。
如果这条消息是订单事实流,后面有风控、报表、搜索、数据仓库多个系统消费,它更像事件。
如果这条消息是“请发送订单通知短信”,它更像任务。
如果这条消息是“订单 30 分钟未支付要关闭”,它就是业务状态推进。
所以不要问:
订单消息用 Kafka、RabbitMQ 还是 RocketMQ?
要问:
这条订单消息在系统里到底承担什么职责?
可以先用这张表判断。
| 场景 | 消息本质 | 更推荐 | 原因 |
|---|---|---|---|
| 用户行为日志采集 | 事件流 | Kafka | 高吞吐、可保留、可被多个下游消费 |
| 订单状态变更广播 | 业务事件 | Kafka / RocketMQ | 偏数据流用 Kafka,偏交易推进用 RocketMQ |
| 邮件短信发送 | 异步任务 | RabbitMQ / RocketMQ | RabbitMQ 路由和 ack 清晰,RocketMQ 适合业务重试 |
| 延迟关单 | 延迟业务消息 | RocketMQ | 延迟消息语义更贴近业务 |
| 支付成功后多系统通知 | 业务消息 | RocketMQ / Kafka | 交易链路推进用 RocketMQ,事件分发和回放用 Kafka |
| 数据库变更同步 | 数据变更日志 | Kafka | CDC 和流式处理生态更成熟 |
| 复杂路由投递 | 路由任务 | RabbitMQ | exchange 和 binding 模型直接匹配 |
| 秒杀削峰 | 异步缓冲 | RocketMQ / Kafka | 看是交易消息还是事件流削峰 |
| 风控实时特征流 | 实时事件流 | Kafka | 多下游、可回放、流式计算友好 |
| 本地事务后发送消息 | 事务业务消息 | RocketMQ | 事务消息模型更贴近业务一致性 |
这里故意没有给所有场景唯一答案。
因为很多场景本来就有分叉。
比如“支付成功后通知多系统”。
如果它表达的是交易链路推进:通知履约、积分、账务继续处理,那么 RocketMQ 更贴近。
如果它表达的是一条支付事实流:风控、报表、数据分析、审计系统都要订阅,那么 Kafka 更贴近。
再比如“秒杀削峰”。
如果只是把请求流量削成可消费的事件流,Kafka 可以很好地承接。
如果后面要处理订单创建、库存扣减、超时取消、失败重试,RocketMQ 的业务消息能力就更有价值。
所以:
同样叫“订单消息”,如果它是事件流,可能适合 Kafka;如果它是待处理任务,可能适合 RabbitMQ;如果它要推进交易状态,可能适合 RocketMQ。
6. 选错 MQ 后,问题通常不是马上爆炸,而是慢慢变成架构债
MQ 选错最麻烦的地方,不是第一天不能用,而是后面每加一个场景都要绕过原来的模型。
很多错误选型短期看都能跑。
但它会把复杂度慢慢推到业务代码里。
用 RabbitMQ 当长期事件日志
RabbitMQ 可以传事件,但如果你把它当长期事件日志,就会越来越别扭。
因为它的主模型是队列投递和消费确认。
你会遇到这些问题:
- 历史消息回放不自然;
- 多个消费者独立进度管理不自然;
- 新下游晚加入以后补历史数据不自然;
- 大规模数据管道场景下,队列和绑定会变得复杂。
最后你可能会发现,自己在 RabbitMQ 外面又补了一套事件存储。
用 Kafka 做复杂任务路由
Kafka 可以做异步任务,但如果任务路由很复杂,也会不顺手。
比如不同类型任务要进不同队列,不同失败原因要走不同死信路径,不同消费者处理能力差异很大,还要精细 ack / nack。
这些在 Kafka 里不是完全不能做,但经常要靠 Topic 拆分、消费者逻辑、重试 Topic、死信 Topic、调度任务来拼。
模型会变得比较重。
用 Kafka / RabbitMQ 硬拼交易消息语义
交易系统最常见的问题是:
- 顺序怎么保证?
- 延迟怎么触发?
- 本地事务和消息发送怎么对齐?
- 消费失败怎么重试?
- 重试多次仍失败怎么进入人工处理?
如果所选 MQ 没有把这些能力做成相对标准的模型,业务系统就会自己补。
补着补着,就会出现很多表:
- 消息发送表;
- 消息补偿表;
- 延迟任务表;
- 重试记录表;
- 死信处理表;
- 人工修复表。
这些表不是不能有,但如果每个业务系统都自己设计一套,后面维护成本会很高。
为了统一技术栈,让所有场景共用一个 MQ
还有一种常见错误是:公司已经有 Kafka,所以所有消息都进 Kafka;或者公司已经有 RocketMQ,所以所有异步都用 RocketMQ。
这看起来降低了运维复杂度,但可能增加业务复杂度。
日志流、任务队列、交易消息本来就是三种模型。
如果强行用一个 MQ 承接所有场景,系统很容易变成:
- 事件流里混着任务;
- 任务队列里混着交易状态;
- 交易消息里混着日志采集;
- 每个消费者都在用自己的方式解释消息语义。
这时问题不是 MQ 不够强,而是模型被混在了一起。
所以不要把“技术统一”当成唯一目标。
技术统一不是目标,语义匹配才是目标。一个系统里同时存在两类 MQ,并不一定是坏设计。
7. 架构选型应该看七个问题
真正成熟的 MQ 选型,不是问“用哪个”,而是连续问七个业务语义问题。
可以用下面这张清单做第一轮判断。
| 问题 | 如果答案是“是” | 倾向 |
|---|---|---|
| 消息是否需要长期保留并可回放? | 是 | Kafka |
| 是否有多个下游按不同节奏消费同一类事件? | 是 | Kafka |
| 是否需要复杂路由,把不同消息投递到不同队列? | 是 | RabbitMQ |
| 是否强调单条任务处理完成确认? | 是 | RabbitMQ |
| 是否需要顺序消息推进业务状态? | 是 | RocketMQ |
| 是否需要延迟消息、事务消息、消费重试、死信治理? | 是 | RocketMQ |
| 消息是业务事实、异步任务,还是交易状态推进? | 先分类 | 再选型 |
还可以再压缩成一个决策树:
这条消息以后要不要回放? 是 -> Kafka 否 -> 是否需要复杂路由和任务确认? 是 -> RabbitMQ 否 -> 是否需要顺序、延迟、事务、业务重试? 是 -> RocketMQ 否 -> 三者都能做,按团队经验和运维成本选当然,真实选型不会只看这一张表。
还要看团队经验、运维能力、现有基础设施、云厂商支持、语言栈、监控体系、容量模型、故障恢复能力。
但这些是第二层问题。
第一层问题永远是:
这条消息到底是什么?
如果第一层错了,后面参数选得再漂亮,系统也会别扭。
8. 结尾:选 MQ,本质上是在选择系统如何表达事实
MQ 不是把同步调用改成异步调用这么简单,它决定了系统如何记录事实、分发任务和推进状态。
Kafka、RabbitMQ、RocketMQ 的差异,可以收成一句话:
Kafka 关注“发生过什么”,RabbitMQ 关注“交给谁处理”,RocketMQ 关注“业务状态如何可靠推进”。
所以选型时不要先问:
哪个 MQ 性能最好?
先问:
我这条消息是事件日志、任务队列,还是业务消息?
如果它是事件日志,你要关心 Topic、Partition、Offset、Consumer Group、保留策略、回放和多下游消费。
如果它是任务队列,你要关心 Exchange、Queue、Binding、Ack、Nack、Prefetch、死信和失败转移。
如果它是业务消息,你要关心顺序、延迟、事务、重试、死信和状态推进。
很多系统的 MQ 问题,不是出在“消息没发出去”,而是出在“系统从来没定义清楚这条消息代表什么”。
只要这个问题没想清楚,后面就会出现各种症状:
- 消费者不知道能不能重放;
- 重试以后不知道会不会重复入账;
- 死信以后不知道谁来处理;
- 延迟任务不知道有没有漏扫;
- 顺序消息不知道按什么维度保证;
- 本地事务和消息状态各说各话。
所以 MQ 选型的真正起点,不是中间件产品,而是业务事实建模。
最后可以用这张表收束:
| 你真正需要的是什么 | 更贴近的模型 | 代表中间件 |
|---|---|---|
| 记录事实,允许多个系统读取和回放 | 事件日志 | Kafka |
| 分发任务,确认处理完成和失败转移 | 任务队列 | RabbitMQ |
| 推进业务状态,处理顺序、延迟、事务和重试 | 业务消息 | RocketMQ |
如果把这件事想清楚,Kafka、RabbitMQ、RocketMQ 的选型就不会只是参数比较,而会变成一次系统模型设计。