消息队列概述与选型
零基础第一课:消息队列解决什么问题、三种投递语义、Kafka/RabbitMQ/Pulsar 对比与选型。
0. 一句话理解
消息队列(MQ)就是”生产者把消息放进信箱,消费者有空再取”:它让系统之间解耦、削峰、异步,是分布式系统的标配中间件。
1. 消息队列解决什么问题
1.1 解耦
下单后要通知库存、积分、短信三个系统。直接调用接口,每加一个系统就要改代码;改成”下单消息发到队列”,新系统自己订阅即可。
1.2 削峰
秒杀瞬间 10 万请求打到数据库会挂。先把请求写进队列,消费端按数据库能承受的速度慢慢处理——“排队打饭”而不是”一起挤窗口”。
1.3 异步
发邮件、生成报表等耗时操作从请求链路里挪到后台异步执行,接口响应从 3 秒降到 100 毫秒。
1.4 MQ 在一次下单链路中的位置
flowchart LR
U[用户下单] --> API[订单服务<br/>只写订单库+发一条消息]
API -->|publish 订单事件| MQ((消息队列))
MQ -->|订阅| INV[库存服务]
MQ -->|订阅| PTS[积分服务]
MQ -->|订阅| SMS[短信服务]对比同步调用:订单服务不再知道库存/积分/短信的存在,任何下游挂了都不影响下单, 新下游上线只需要”多一个订阅者”。
2. 核心概念
| 概念 | 说明 | 生活类比 |
|---|---|---|
| Producer 生产者 | 发消息的一方 | 寄信人 |
| Consumer 消费者 | 收消息处理的一方 | 收信人 |
| Broker 代理 | 消息暂存与转发的服务器 | 邮局 |
| Topic / Queue | 消息的分类容器 | 信箱 |
| 消费组 | 一组消费者分担消息 | 多个邮递员分工 |
3. 三种投递语义
| 语义 | 含义 | 代价 |
|---|---|---|
| At-most-once 至多一次 | 消息可能丢,但绝不重复 | 最低 |
| At-least-once 至少一次 | 不丢,但可能重复 | 需消费端幂等 |
| Exactly-once 精确一次 | 不丢不重 | 最高,通常配合事务/幂等实现 |
企业默认选择 At-least-once + 消费端幂等:既不丢消息,又用”去重表/唯一键”把重复消费变成无害操作。
4. 主流中间件对比
| 维度 | Kafka | RabbitMQ | Pulsar |
|---|---|---|---|
| 定位 | 分布式事件流平台 | 传统消息代理 | 云原生流平台 |
| 模型 | Topic + 分区 | Exchange + Queue | Topic + 分区 |
| 顺序保证 | 分区内有序 | 单队列有序 | 分区内有序 |
| 吞吐 | 极高 | 中 | 高 |
| 延迟 | 毫秒级 | 微秒级 | 毫秒级 |
| 重放 | 消息可保留重读 | 消费后删除 | 可保留重读 |
| 典型场景 | 日志、指标、事件流 | 任务分发、RPC 解耦 | 混合场景、多租户 |
5. 什么时候不要用消息队列
- 调用方必须立刻知道结果(改用同步 API);
- 系统只有一个模块、没有峰值压力(引入 MQ 徒增复杂度);
- 需要强事务跨系统一致性(MQ 只能最终一致,账务场景要慎重)。
6. 推模式与拉模式:消费端的两条路线
| 维度 | 推(Push):RabbitMQ | 拉(Pull):Kafka |
|---|---|---|
| 触发方式 | Broker 主动送上门 | 消费者主动来取 |
| 优点 | 低延迟,消费端简单 | 消费端按能力取,天然背压 |
| 缺点 | 需要 prefetch 限流防”撑死” | 有轮询延迟,长轮询优化后毫秒级 |
| 类比 | 食堂阿姨帮你打菜 | 自己端着餐盘去窗口 |
Kafka 的”长轮询”(long polling)让拉模式在没消息时也不空转——这点知道即可, 重要的是记住:RabbitMQ 的积压治理靠 prefetch,Kafka 的积压治理靠消费组扩容。
7. 选型决策树
flowchart TD
Q{需要的是日志/事件流<br/>还是要回放历史?} -->|是| K1{吞吐是海量级<br/>或要多消费组各自读全量?}
Q -->|否,是任务分发/复杂路由| R1{需要灵活路由规则<br/>与低延迟?}
K1 -->|是| K[Kafka]
K1 -->|否,量不大| K2{团队已熟某套技术栈?}
K2 -->|是| K2Y[沿用现有即可<br/>别为选型而选型]
K2 -->|否| R[RabbitMQ 也完全够用]
R1 -->|是| R
R1 -->|否| R
Q -->|云原生多租户/存算分离| P[Pulsar]三个常识校准:
- 量不大就别上 Kafka:Kafka 的运维成本(分区、副本、控制器、滚动升级)显著高于 RabbitMQ,日均百万级以下的消息 RabbitMQ 更省心。
- Kafka 消息默认保留 7 天可重放,RabbitMQ 消费确认后即删——这是两种世界观, 决定了”历史回放”能力只有流平台才有。
- 云厂商托管版优先于自建:自建 MQ 集群是运维深坑,除非有特殊合规要求, 优先评估托管服务(MSK/Amazon MQ/阿里云 RocketMQ 等)。
8. 常见误区
| 误区 | 现实 |
|---|---|
| “MQ 能保证消息绝对不丢” | 默认配置都可能丢(发送无确认、无持久化);不丢是靠整套机制堆出来的,见可靠消息篇 |
| “Exactly-once 开箱即用” | 各家实现都限定范围(如 Kafka 事务只在 Kafka Streams/事务 API 内有效),跨系统仍要靠消费端幂等 |
| “加了 MQ 系统就解耦了” | 消息格式成了隐式契约,不治理 schema 就是新的强耦合 |
| “消息队列 = 降级仓库,堆着慢慢消费” | 积压是有成本的:内存/磁盘、消息过期、时效性承诺全部受损,积压必须监控与治理 |
9. 实践:动手与自检
判断题(5 分钟):下列三个场景,哪些适合引入 MQ?(a)支付回调后通知积分系统发分; (b)网关查询用户余额决定放行与否;(c)每晚批量生成报表。验收:能对每个场景说出 「调用方是否关心结果」「是否有峰值」两条判断依据。
提示(思路方向):判断标准就是第 1 节的三大价值——解耦、削峰、异步;反过来第 5 节 「什么时候不要用」是排除法。参考答案:a 适合(通知类,调用方不关心结果,异步即可); b 不适合(同步链路必须立刻知道结果,引 MQ 纯增延迟);c 不适合(无并发压力,定时任务 直接做更简单)。
观察题(15 分钟,衔接后两章):分别用 Docker 启动 Kafka 与 RabbitMQ(命令见后两章), 各跑通「发一条、收一条」,然后对照本篇填一张表:消息消费后还能再读到吗?消费者挂了 未确认的消息去哪了?验收:能用自己的话复述「可重放」与「消费即删」两种世界观的差异。
提示:Kafka 用 --from-beginning 重新消费验证可重放;RabbitMQ 观察管理后台队列深度
归零。两边跑完再回看第 4 节对比表,每一行都应该有了体感。
设计题(20 分钟):把第 1.4 节的下单链路画成时序图(Mermaid sequenceDiagram),标出 每一步的失败后果:消息发送失败会怎样?库存服务消费失败会怎样?验收:图里能看出 「订单库已写入但消息没发出去」这个不一致窗口——它就是可靠消息篇本地消息表要解决的问题。
提示(思路方向):先画正常路径,再对每一步加「失败分支」。先自己画,再对照骨架:
sequenceDiagram
participant U as 用户
participant O as 订单服务
participant MQ as 消息队列
participant I as 库存服务
U->>O: 下单
O->>O: 写订单库(事务内)
O->>MQ: 发送订单事件(若这里失败:库有单、下游永远不知道)
MQ->>I: 投递(若消费失败:需重试与死信兜底)
I->>I: 扣库存复盘题(5 分钟):回想一次你遇到的「下游系统拖垮上游」或「峰值打挂数据库」的事故 (没有就虚构一个秒杀场景),用本篇的语言重新诊断:事故链里哪一步本该由 MQ 承接? 验收:诊断里至少用到「解耦、削峰、异步」中的一个词,且说清为什么用 MQ 能切断事故链。
提示:削峰对应「打挂」类事故——请求先落队列,消费端按数据库承受速度处理;解耦对应 「拖垮」类事故——下游故障时上游只管发消息,不被同步调用卡死。想不清时回到第 1.2 节 的「排队打饭」类比检查推理。
10. 一句话记住
MQ 的三大价值是解耦、削峰、异步;选型看场景:海量事件流选 Kafka,灵活任务分发选 RabbitMQ,云原生多租户选 Pulsar;投递语义默认 At-least-once + 消费端幂等。