













下单成功后,要发短信、加积分、发优惠券、通知物流……如果一件件同步串着做,用户得干等到全部做完;更糟的是,只要积分服务挂了,整个下单就跟着失败——明明钱已经付了。
消息队列(MQ)就是用来拆掉这种"强耦合 + 同步等待"的。这篇讲清楚:MQ 到底解决什么、怎么用、以及绕不开的三个问题——消息丢失、重复、顺序怎么破。配套一个零依赖 demo,
node demo.js一跑就能看到每个机制的效果。
很多人对 MQ 的第一印象是"一个传消息的中间件",但说不清它到底带来什么。其实就三个核心价值,对着真实场景看一遍就懂了。
回到开头的下单。同步串行的写法是这样:
下单 → 调短信服务 → 调积分服务 → 调发券服务 → 返回成功
(任意一步挂,整个下单失败)
下单这段代码,得知道下游有哪些服务、按什么顺序调、每个怎么调。哪天新增一个"下单送成长值",还得回来改下单的代码。这就是强耦合。
换成 MQ:下单做完核心的事(扣库存、生成订单),只往队列里发一条"订单已创建"消息,然后立刻返回成功。短信、积分、发券各自去队列里订阅这条消息,自己处理自己的。
下单服务从此不认识任何下游。 新增一个下游?让它去订阅就行,下单代码一个字不用改;某个下游挂了?消息还堆在队列里,等它恢复了接着消费,下单完全不受影响。
一句话点破:解耦的本质,是把"我要主动去调用谁"变成"我只发一个事件,谁关心谁来订"。
秒杀开始的那一秒,可能涌进十万个下单请求。如果每个请求都同步地把短信、积分、发券都做完,这些下游(尤其是发短信这种依赖外部网关的)根本扛不住瞬时洪峰,直接被打挂。
MQ 在中间当了一个蓄水池:洪峰来了,消息先快速堆进队列(入队很轻,几乎不耗时),下游消费者按自己能处理的速度慢慢取、慢慢做。十万条消息可能要几分钟才消费完,但下游始终在安全水位运行,不会被冲垮。
这就是削峰填谷:把"一瞬间的高峰"摊平成"一段时间的平稳流量"。代价是下游处理有延迟(发短信可能晚几秒到),但对这类非核心、能容忍延迟的操作,完全划算。
同一条"订单已创建",可能有一堆系统都关心:积分要加分、风控要记录、数据团队要统计、推荐系统要更新画像……
同步写法里,下单得挨个去调它们,列表越来越长。用 MQ 的发布/订阅模式:下单只发一次,所有订阅了这个 topic 的系统各收到一份,各干各的,互不影响。新增一个消费方,对生产者透明。
这三个价值往往是一起来的:一条订单消息,既解耦了下单与下游(①),又削峰扛住了洪峰(②),还广播给了多个系统(③)。
MQ 的基本模型就三个角色,很好记:
生产者(Producer) → broker(队列/服务端) → 消费者(Consumer)
发消息 存消息、转发 取消息、处理
| 概念 | 是什么 | 一句话理解 |
|---|---|---|
| topic(主题) | 消息的分类 | 一类业务一个 topic,如 order.created |
| partition(分区) | 一个 topic 拆成的多条并行队列 | 分区越多,能并行消费的吞吐越高 |
| consumer group(消费组) | 一组协作消费同一 topic 的消费者 | 组内分摊消息(每条只给组里一个),不同组各收一份(广播) |
partition 和 consumer group 是理解吞吐与顺序的关键。 一个 topic 分成多个 partition,就能让多个消费者并行处理(每人盯几个分区),吞吐随之提升;而"同一组内每条消息只投给一个成员、不同组各收一份"——前者实现了负载分摊,后者实现了广播(第一节的 ③ 就靠它)。
选型时大致这么记(都能做基本的队列,差异在侧重点):
| MQ | 一句话定位 | 典型场景 |
|---|---|---|
| Kafka | 高吞吐的分布式日志流 | 海量日志 / 数据管道 / 流处理 |
| RabbitMQ | 灵活路由、协议成熟 | 复杂的路由分发、传统业务异步化 |
| RocketMQ | 强在事务消息、金融级 | 电商交易、要"消息和本地事务一致"的场景 |
记不住也没关系——它们的核心模型和下面要讲的三个问题是相通的,搞懂一个,换另一个主要是 API 和运维细节的差异。
三个里 Kafka 最主流,也最该单独理解——因为上面讲的 topic / partition / consumer group 这套术语,本就源自 Kafka(所以你搞懂这篇,Kafka 的骨架已经会了一大半)。
它和传统队列最大的不同:Kafka 本质是一份"日志(log)",不是"用完即焚的队列"。
这带来一个杀手锏——可回放:想重读历史,把 offset 往回拨就行;新接入一个消费方,能把过去的消息从头重放一遍。所以 Kafka 特别适合海量日志 / 埋点收集、数据管道(源源不断把数据送进数仓)、事件流、配 Flink / Spark 做实时计算。
它为什么能扛到每秒几十万上百万条(高吞吐从哪来):
一句话:Kafka 把消息当"可重读的日志流"来存,所以它高吞吐、能回放,是大数据 / 流处理场景的事实标准;而 RabbitMQ 那种"确认后即删"的队列,更适合传统业务的异步解耦、灵活路由。两者模型不同,别套混。
绝大多数情况你不用自己从零搭 MQ 集群,按规模大致是:
应用层接入,各主流后端语言(Java、Go 等)都有成熟的客户端库,配好接入点、主题、消费组就能收发。两种模型接入时的关注点不同:
一句话:搭法看规模(本地 Docker / 云托管 / 公司平台),接入看模型(Kafka 面向 topic + key,RabbitMQ 面向 exchange + 路由规则)——但底层要解决的,还是前面那三个问题。
用 MQ,这三个问题躲不掉:消息会不会丢、会不会重复、能不能保证顺序。面试常问,线上更常踩。一个个拆。
一条消息从生产者到被消费,要走三段路,每一段都可能丢:
生产者 ──①──> broker ──②──> (持久化) ──③──> 消费者
对策,正好对着三个环节:
| 环节 | 怎么防丢 |
|---|---|
| ① 生产端 | 开启 confirm / ack:broker 真正收下并落盘后才回确认,没收到确认就重发 |
| ② broker | 持久化(消息落盘)+ 多副本:存到多台机器,挂一台不丢 |
| ③ 消费端 | 处理完再 ack:业务真正做完了,才告诉 broker"这条我消费好了";没 ack 的消息会被重新投递 |
第三点最容易写错。很多人图省事用"自动 ack"(取到消息就自动确认),消费逻辑还没跑完就 ack 了,一崩就丢。正确姿势是手动 ack:处理 → 成功 → 再 ack。
一句话点破:防丢失的核心,是把"确认"卡在"真正做完"之后——生产端等 broker 落盘的确认,消费端等业务处理完才确认。
demo 场景二演示的就是消费端这一环:消费者"处理中崩溃"(没 ack),消息被重投,直到处理成功才 ack——这就是"消息不丢"。但它带来下一个问题。
注意上面防丢的代价:消息没 ack 就重投。可"没收到 ack"有两种可能——消费者真没处理,或者处理完了但 ack 在回去的路上丢了。broker 分不清这两种,为了不丢,它只能选择重投。
于是主流 MQ 的投递语义是 at-least-once(至少一次):宁可重复,绝不丢失。这意味着同一条消息可能被消费多次(网络重试、ack 丢失、消费者重启都会触发)。
如果消费逻辑是"加 100 积分",重复消费两次就加了 200——错了。
对策:幂等。 让消费逻辑"执行一次和执行多次,结果一样"。最常用的做法:
数据库的唯一索引是最稳的兜底:插入去重记录时,重复的那条会因为唯一键冲突直接失败,天然挡住重复。
一句话点破:既然 MQ 给的是"至少一次",消费端就必须做幂等——这不是可选项,是 at-least-once 的配套义务。
demo 场景三把这事说透了:同一条消息投递两次,不做幂等积分加成了 100(错,应是 50);用去重表记下业务 id 后,第二次直接跳过,只生效一次(对)。
"创建订单 → 支付 → 发货"这三条消息,必须按这个顺序处理,不然就乱套(还没创建就发货?)。但 MQ 默认不保证全局顺序——为什么?
因为吞吐。一个 topic 拆成多个 partition,就是为了让多个消费者并行处理。可一旦并行,不同分区谁先处理完就不确定了:三条消息散到三个分区,各分区的消费者各跑各的,合起来的完成顺序自然可能乱。顺序和并行,本质上是矛盾的。
对策:不追求全局有序,只保证"需要有序的那一类"有序。
代价是:同一个 key 的消息只能串行处理,牺牲了这一个 key 内部的并行度。但不同 key(不同订单)之间仍然是并行的,整体吞吐基本不受影响——因为你只对"同一订单"要求有序,不同订单本来就互不相干。
一句话点破:顺序的解法不是"让全局有序"(那等于放弃并行),而是"把要有序的归到同一分区,串行消费",其余照常并行。
demo 场景四直接对比:三条消息散到多个分区并行消费,顺序乱成了"发货→支付→创建";用同一个订单号当 key 路由到同一分区后,严格还原成"创建→支付→发货"。
MQ 不是免费的。引入它,等于给系统多加了一个要独立运维的中间件,还把原本"一步到位"的同步调用,变成了最终一致(下游晚一会儿才处理完),并且强制你处理上面三个问题(丢失、重复、顺序)。这些复杂度都是实打实的。
所以这些情况别急着上 MQ:
上 MQ 前先问三句:有下游需要解耦吗?有瞬时洪峰要削峰吗?这步操作能接受异步(晚点做完)吗?——有 yes 才值得引入。
一句话点破:MQ 解决的是"解耦、削峰、广播";如果你的系统没有这三个痛点,它带来的复杂度就是纯负担。
把全文对到真实场景,一张表带走:
| 业务场景 | 用 MQ 的哪个能力 | 为什么 |
|---|---|---|
| 下单后发短信 / 加积分 / 发券 | 解耦 | 下单不该被非核心下游拖累、绑死 |
| 秒杀瞬时十万下单 | 削峰 | 把洪峰堆进队列,下游按自己节奏消化 |
| 一条订单事件,多系统都要 | 广播 | 发一次,风控 / 积分 / 数据各收一份 |
| 上传视频后转码 / 截图 / 审核 | 解耦 + 削峰 | 耗时操作异步做,不阻塞上传响应 |
| 同一订单"创建→支付→发货"保序 | 解耦 + 同 key 分区 | 用订单号当 key,同分区串行,保证有序 |
| 需实时返回结果的查询 | 别用 MQ | 同步调用更直接,异步反而别扭 |
配套 demo:backend-notes/02-features/message-queue/mq-demo ——
node demo.js一跑,四个场景直观看到:解耦(下单发完即返回)、防丢(崩溃没 ack 则重投,最终不丢)、幂等(同消息投两次,有无去重表差出一倍积分)、顺序(多分区乱序 vs 同 key 同分区严格有序)。零依赖,跑完即退出。本文属《研发都要懂的事》·「功能怎么实现」专题。完整代码与系列在 GitHub · backend-notes。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。