消息中间件 列表全部文章
消息中间件

消息队列选型与通识笔记:什么时候用 MQ、三大中间件怎么选、必踩的六个通用问题

不局限某一种 MQ,先把消息队列的通用命题讲清楚:削峰、解耦、异步、最终一致四大价值,RocketMQ/Kafka/RabbitMQ 的选型对比,以及消息丢失、重复消费、顺序性、堆积、幂等、事务消息这六个绕不开的问题与通用解法。

更多

用过 RocketMQ、RabbitMQ,也读过 Kafka 的设计。这篇先不谈具体产品,把所有 MQ 都要面对的问题整理出来 —— 这些才是选型和排障的真正依据。

一、MQ 的四大价值

削峰 洪峰流量先入队, 下游按能力消费 解耦 生产者不关心 谁消费、几个消费 异步 主链路不等 非关键步骤 最终一致 本地事务 + 消息 替代分布式事务 代价:系统复杂度上升(幂等、顺序、堆积、可观测),不要为了「架构好看」而引入 MQ 判断标准:这一步失败了能不能重做?能重做且不影响主流程 → 适合异步化;不能 → 留在同步链路里。

二、三大 MQ 怎么选

维度 RocketMQ Kafka RabbitMQ
吞吐量 十万级 TPS 百万级 TPS(最高) 万级 TPS
延迟 毫秒级 毫秒级(批处理后略高) 微秒–毫秒(最低)
消息模型 Topic + Tag + 队列 Topic + Partition Exchange + Queue(路由灵活)
顺序消息 支持(分区顺序) 支持(同 Partition) 单队列顺序
事务消息 原生支持(半消息) 支持(较弱) 需自己实现
延迟消息 原生 18 个等级 需自己实现 死信 + TTL 或插件
消息回溯 支持(按时间) 支持(按 offset) 不支持(消费即删除)
生态/运维 Java 友好,控制台好用 大数据生态强 Erlang,运维相对复杂
典型场景 业务消息、订单、交易 日志、埋点、流计算 企业内部业务、路由复杂

我的选择习惯:

  • 业务链路(下单、支付、通知) → RocketMQ。事务消息和延迟消息是刚需,Java 栈也顺手。
  • 日志/埋点/行为数据 → Kafka。吞吐和生态最强,接 Flink / 数仓方便。
  • 内部系统间小规模解耦、路由规则复杂 → RabbitMQ。Exchange 的灵活路由很好用。

三、六个绕不开的问题

1. 消息丢失

三个环节都可能丢:

环节 风险 保障
生产端 发送失败、网络抖动 同步发送 + 失败重试;RocketMQ 用事务消息
Broker 刷盘前宕机 同步刷盘(flushDiskType=SYNC_FLUSH)或至少同步复制
消费端 自动 ack 后业务失败 业务处理成功再 ack(手动提交)

最容易被忽略的是消费端:用了自动提交 offset,业务抛异常但消息已经算消费成功了 —— 消息等于丢了。

2. 重复消费

MQ 只保证至少一次(at least once),重复消费是必然发生的,不要指望靠 MQ 避免。

幂等方案(按推荐顺序):

  1. 唯一键 + 数据库约束:消息带业务唯一 ID,插入时冲突即忽略。最可靠。
  2. Redis SETNX:set(msgId, 1, NX, EX 24h),已存在则跳过。
  3. 状态机:if (order.status != PENDING) return;,天然幂等。
  4. 去重表:专门一张 msg_consumed 表,带唯一索引。

3. 顺序性

只有局部顺序是可行的:把需要保序的消息(同一订单)发到同一个队列/分区,且消费者单线程消费。用 MessageQueueSelector 按订单 ID 取模分片。

全局顺序 = 一个分区 = 没有并发 = 没有吞吐,生产上不要追求。

4. 消息堆积

现象:消费速度跟不上生产速度,队列长度持续上涨。

排查顺序:

  1. 消费者是不是挂了 / 卡住了(看消费 TPS 曲线、看线程栈)。
  2. 消费逻辑是不是变慢了(下游接口慢、慢 SQL)。
  3. 消费端有没有报错导致反复重试。

应急扩容:增加消费者实例,但前提是队列数足够 —— RocketMQ 中消费者数量 > 队列数时,多出来的消费者分不到队列,等于白加。所以队列数一开始就得多配(比如 32/64)。

兜底:堆积严重时,把消息转存到「死信/备用 Topic」,先让主链路恢复,再慢慢消费积压数据。

5. 消费失败与重试

  • 重试要有次数上限和退避,不要无限重试(会把 MQ 打满)。
  • 重试 N 次仍失败 → 进死信队列 / 人工补偿表,并告警。
  • 区分可重试异常和不可重试异常:参数错误重试一万次也没用,直接进死信;网络超时才值得重试。

6. 事务消息(本地事务 + 消息的一致性)

典型场景:下单成功要发一条消息,但「订单入库」和「发消息」是两个操作,不能保证同时成功。

RocketMQ 事务消息流程:

1. 发送半消息(对消费者不可见)
2. 执行本地事务(下单入库)
3. 本地事务成功 → Commit(消息可见);失败 → Rollback(消息丢弃)
4. 若第 3 步没响应,Broker 回查本地事务状态(需实现回查接口)

轻量替代方案(我们更常用的):本地消息表 —— 在业务库建一张 mq_message 表,和业务数据同一个本地事务写入,再由定时任务扫描发送,发送成功标记完成。简单可靠,缺点是有一定的延迟。

四、工程规范(团队约定)

  1. Topic 命名有规范:业务域_事件_版本,如 order_created_v1。
  2. 消息体必带字段:msgId(唯一键,用于幂等)、traceId(链路追踪)、occurTime(业务发生时间)、bizKey(业务主键,用于排查)。
  3. 消息体要向后兼容:只增字段不删字段,用 JSON 且反序列化时忽略未知字段。
  4. 消费端必须可重放:业务逻辑写成幂等的,这样出问题时可以「重置 offset 重跑」。
  5. 监控必配四件套:生产 TPS、消费 TPS、堆积量、消费失败率。堆积量告警是生命线。

五、一次真实的堆积事故

脱敏自某营销中台的批量任务场景。

现象:营销活动批量发券,MQ 堆积从 0 涨到 300 万,活动效果延迟了 2 小时才生效。

原因:消费端在处理每条消息时都会调用一次用户服务的 HTTP 接口查画像,用户服务当时正慢(RT 2s),消费能力从 2000 TPS 掉到 50 TPS。

处理:

  • 紧急扩容消费者(队列数 32 够用,扩到 32 个实例)。
  • 更重要的一步:把每条消息的远程调用改成批量 —— 消费端攒批 500 条再一次性查画像,网络往返从 1 次/条降到 1 次/500 条。
  • 长期:画像数据本地缓存 + 变更时推送,消费端不再依赖实时查询。

结论:消费端单条消息里的远程调用是吞吐杀手。优化 MQ 的时候别只盯着 MQ 参数,往往瓶颈在消费逻辑本身。

— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人