MQ 选型最难的不是比吞吐,而是先判断你要的是事件日志、任务队列,还是业务消息

📅 2026/7/2 11:26:59 👁️ 阅读次数 📝 编程学习
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: 记录自己的 offset

Kafka 为什么适合事件流和回放?

原因不在一句“吞吐高”,而在它的几个底层机制。

第一,消息消费和消息保留是分离的。

在很多队列模型里,消息被消费确认以后,就倾向于从队列里移除。Kafka 不是这个思路。Kafka 的消息会按照保留策略留在日志里,消费者只是记录自己的 Offset。

这意味着,一个消费者读完了,不代表其他消费者不能读。

也意味着,一个新系统晚几天上线,只要消息还在保留期内,它可以从过去的位置重新消费。

第二,消费进度属于 Consumer Group。

同一个 Topic 可以被多个 Consumer Group 消费。风控系统有自己的消费进度,报表系统有自己的消费进度,数据同步系统也有自己的消费进度。它们互不影响。

这对事件流非常关键。

因为一个事件发生以后,经常不是一个下游关心,而是一批下游按不同节奏关心。

第三,Partition 同时承担顺序边界和并发边界。

同一个 Partition 内部有序,不同 Partition 可以并行。

如果你按orderIduserId这类 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 C

RabbitMQ 为什么适合任务分发和复杂路由?

因为它的核心模型就是“先路由,再排队,再确认”。

第一,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 / RocketMQRabbitMQ 路由和 ack 清晰,RocketMQ 适合业务重试
延迟关单延迟业务消息RocketMQ延迟消息语义更贴近业务
支付成功后多系统通知业务消息RocketMQ / Kafka交易链路推进用 RocketMQ,事件分发和回放用 Kafka
数据库变更同步数据变更日志KafkaCDC 和流式处理生态更成熟
复杂路由投递路由任务RabbitMQexchange 和 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 的选型就不会只是参数比较,而会变成一次系统模型设计。