写在前面
本文是消息队列系列的第一篇,介绍消息队列的核心概念、常见模式和可靠性保证机制。这些知识是理解 RabbitMQ 和 Kafka 的基础。
一、为什么需要消息队列
1.1 同步调用的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
用户下单流程(同步):
1. 创建订单 200ms
2. 扣减库存 150ms
3. 发送短信通知 300ms
4. 发放积分 100ms
5. 更新搜索引擎 200ms
总耗时:950ms
问题:
- 用户等了近 1 秒才看到结果
- 短信服务挂了,下单也失败
- 新增下游业务需要改下单代码
- 秒杀时流量直接打到数据库
|
1.2 异步解耦
1
2
3
4
5
6
7
8
9
10
|
用户下单流程(异步):
1. 创建订单 200ms
2. 发送消息到 MQ 5ms
总耗时:205ms(用户感知)
MQ 下游各自消费:
- 短信服务 → 消费消息 → 发短信
- 积分服务 → 消费消息 → 发积分
- 搜索服务 → 消费消息 → 更新索引
|
1.3 三大核心作用
1
2
3
|
异步处理 — 非核心逻辑异步化,降低响应时间
系统解耦 — 上游不依赖下游,各自独立演进
流量削峰 — 突发流量堆积在 MQ,后端按能力消费
|
二、消息模型
2.1 点对点模式(Point-to-Point)
1
2
3
4
5
6
7
8
|
Producer → Queue → Consumer
特点:
- 一条消息只能被一个消费者消费
- 消费后从队列中移除
- 适用:任务分发(一个任务只执行一次)
示例:订单系统中,一个订单只被一个发货服务处理
|
2.2 发布订阅模式(Pub/Sub)
1
2
3
4
5
6
7
8
9
10
|
→ Consumer A(短信服务)
Producer → Topic → Consumer B(积分服务)
→ Consumer C(搜索服务)
特点:
- 一条消息被所有订阅者消费
- 每个消费者有自己的消费进度
- 适用:事件通知(一个事件触发多个动作)
示例:订单创建后,短信、积分、搜索各做各的
|
2.3 两种模型对比
1
2
3
4
5
|
点对点 — 一消息一消费,消费即删除,适合任务分发
发布订阅 — 一消息多消费,各自维护进度,适合事件广播
RabbitMQ — 以 Queue 为中心,通过 Exchange 实现两种模式
Kafka — 以 Topic/Partition 为中心,天然发布订阅
|
三、核心概念详解
3.1 Producer(生产者)
1
2
3
4
5
6
7
|
职责:创建消息并发送到 MQ
关键决策:
- 发到哪个 Topic/Queue?
- 消息格式是什么?(JSON、Protobuf、Avro)
- 发送失败怎么处理?(重试、记录、告警)
- 要保证消息不丢吗?(确认机制)
|
3.2 Consumer(消费者)
1
2
3
4
5
6
7
8
|
职责:从 MQ 拉取消息并处理
关键决策:
- 推模式(Push)还是拉模式(Pull)?
- Push:MQ 主动推给消费者(RabbitMQ 默认)
- Pull:消费者主动拉取(Kafka 默认)
- 处理失败怎么处理?(重试、死信)
- 消费进度怎么管理?(自动提交 vs 手动提交)
|
3.3 Broker(消息代理)
1
2
3
4
5
6
7
8
|
MQ 服务器本身,负责:
- 接收和存储消息
- 路由消息到正确的队列
- 管理消费者连接
- 持久化和副本
RabbitMQ:一个 Broker 就是一个 RabbitMQ Server
Kafka:一个集群有多个 Broker,每个 Broker 管理一部分 Partition
|
3.4 Queue 和 Topic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
Queue(队列):
- RabbitMQ 的核心实体
- FIFO 顺序(默认)
- 消息被消费后删除(默认)
- 可以设置 TTL、死信等
Topic(主题):
- Kafka 的核心实体
- 一个 Topic 分为多个 Partition
- 消息持久化,不会因消费而删除
- 可以被多个 Consumer Group 各自消费
Partition(分区):
- Topic 的物理分片
- 每个 Partition 内消息有序
- 不同 Partition 可以并行消费
- 是 Kafka 高吞吐的关键
|
3.5 消息结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
一条消息包含:
消息头(Metadata):
- Topic / Queue 名称
- Key(路由键)
- 时间戳
- 消息 ID
消息体(Payload):
- 实际的业务数据
- JSON / Protobuf / 二进制
属性:
- 优先级
- 过期时间(TTL)
- 持久化标志
|
四、消息路由模式
4.1 直连路由(Direct)
1
2
3
4
5
|
Producer → Exchange → Queue(routing key 完全匹配)
示例:
routing key = "order.created" → 订单队列
routing key = "order.cancelled" → 取消队列
|
4.2 主题路由(Topic)
1
2
3
4
5
6
7
8
9
10
|
Producer → Exchange → Queue(routing key 模式匹配)
支持通配符:
* 匹配一个单词
# 匹配零或多个单词
示例:
"order.*" 匹配 order.created、order.cancelled
"order.#" 匹配 order.created.payment、order.cancelled.refund
"*.created" 匹配 order.created、user.created
|
4.3 广播路由(Fanout)
1
2
3
4
|
Producer → Exchange → 所有绑定的 Queue(忽略 routing key)
示例:
订单创建事件 → 同时发送到短信队列、积分队列、统计队列
|
1
2
|
不基于 routing key,而是基于消息头部的键值对匹配
比 Topic 更灵活,但性能更低,实际使用较少
|
五、消息确认机制
5.1 为什么需要确认
1
2
3
4
5
6
7
8
9
10
11
12
|
消息在传输过程中可能丢失的场景:
生产者 → Broker:
网络抖动,消息没到 Broker
Broker 内部:
Broker 宕机,内存中的消息丢失
Broker → 消费者:
消费者拿到消息后崩溃,消息丢了
每一步都需要确认机制来保证不丢
|
5.2 生产者确认
1
2
3
4
5
6
7
8
9
|
RabbitMQ:Publisher Confirm
- 消息成功写入 Broker 后,Broker 回复 ACK
- 生产者收到 ACK 才认为发送成功
- 可以批量确认提高性能
Kafka:acks 配置
- acks=0 — 发出去就不管了(最快,可能丢)
- acks=1 — Leader 写入成功就返回(默认)
- acks=all — 所有副本都写入才返回(最安全)
|
5.3 消费者确认
1
2
3
4
5
6
7
8
9
10
|
RabbitMQ:Consumer ACK
- 自动确认(autoAck=true)— 消息发出就确认,可能丢
- 手动确认(autoAck=false)— 处理完业务后调用 basic.ack
- 拒绝(nack/reject)— 处理失败,可以重入队列或进死信
Kafka:Offset Commit
- 自动提交 — 定期提交 offset,可能重复消费
- 手动提交 — 处理完业务后提交 offset
- 同步提交 — 阻塞等待提交成功
- 异步提交 — 不阻塞,性能更好但可能丢提交
|
六、持久化
6.1 为什么需要持久化
1
2
3
|
MQ 默认把消息存在内存中:
- Broker 重启后消息全部丢失
- 生产环境必须考虑持久化
|
6.2 持久化层次
1
2
3
4
5
6
7
8
9
10
11
|
RabbitMQ:
- Exchange 持久化 — 声明时设置 durable=true
- Queue 持久化 — 声明时设置 durable=true
- Message 持久化 — 发送时设置 delivery_mode=2
- 三者缺一不可,少一个重启后就丢了
Kafka:
- 天然持久化到磁盘(追加写日志)
- 通过副本(Replica)保证高可用
- 副本因子(replication.factor)通常设为 3
- 数据保留策略:按时间(如 7 天)或按大小
|
七、重试和死信
7.1 消费失败的处理
1
2
3
4
5
6
7
8
9
10
|
消费者处理消息可能失败:
- 网络超时
- 数据库暂时不可用
- 业务校验失败
- 代码 Bug
处理策略:
1. 重试 — 失败后重新消费(适用于临时性故障)
2. 死信 — 重试多次后放入死信队列(永久性故障)
3. 丢弃 — 明确不需要的消息直接丢弃
|
7.2 重试策略
1
2
3
4
|
立即重试 — 适合瞬时故障(网络抖动)
延迟重试 — 指数退避(1s, 2s, 4s, 8s...)
固定次数 — 超过 N 次进入死信
无限重试 — 不推荐,可能阻塞消费
|
7.3 死信队列(DLQ)
1
2
3
4
5
6
7
8
9
10
|
死信产生条件:
1. 消息被拒绝(nack/reject)且不重入队列
2. 消息 TTL 过期
3. 队列满了,无法放入新消息
死信处理:
- 人工排查修复
- 定时任务重新投递
- 告警通知
- 永久记录用于审计
|
八、消息顺序性
8.1 为什么顺序重要
1
2
3
4
5
6
7
8
|
场景:订单状态变更
创建 → 支付 → 发货
如果消费乱序:
先消费"发货" → 订单状态变为"已发货"
再消费"创建" → 订单状态回退到"待支付"
业务出错!
|
8.2 保证顺序的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
|
RabbitMQ:
- 一个 Queue 只有一个 Consumer → 保证顺序但牺牲并发
- 或者用 priority + 去重 → 复杂但可并发
Kafka:
- 同一个 Partition 内消息有序
- 相同 Key 的消息发到同一个 Partition
- 每个 Partition 只被 Consumer Group 中一个 Consumer 消费
- 所以:相同业务 Key 的消息全局有序
最佳实践:
- 需要顺序的消息用相同 Key(如订单 ID)
- 不同 Key 的消息可以并行处理
|
九、消息幂等性
9.1 为什么需要幂等
1
2
3
4
5
6
7
8
9
|
消息可能被重复消费:
- 生产者重试导致重复发送
- 消费者处理完但 ACK 失败,MQ 重新投递
- Rebalance 后重复消费
如果消费逻辑不幂等:
发送短信 → 收到两条
扣减余额 → 扣了两次
发放积分 → 发了两次
|
9.2 幂等方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
方案1:数据库唯一约束
用消息 ID 作为唯一键,重复插入会失败
方案2:乐观锁/版本号
UPDATE account SET balance = balance - 100, version = version + 1
WHERE id = 123 AND version = 5
方案3:状态机
只有特定状态才能转换
订单是"待支付"才能执行"支付成功"
方案4:去重表
消费前先查去重表,处理过就跳过
用消息 ID + 业务类型作为唯一键
方案5:Kafka 事务 / RabbitMQ 幂等生产者
框架层面保证
|
十、消息投递语义
10.1 三种语义
1
2
3
4
5
6
7
8
9
10
11
|
At Most Once(最多一次) — 消息可能丢,但不会重复
先提交 offset,再处理消息
适用:日志、监控数据(丢了无所谓)
At Least Once(至少一次) — 消息不会丢,但可能重复
先处理消息,再提交 offset
适用:大部分业务场景(配合幂等使用)
Exactly Once(精确一次) — 消息不丢不重复
需要事务支持
适用:金融、支付(不能多也不能少)
|
10.2 各 MQ 支持
1
2
3
4
5
6
7
8
9
|
RabbitMQ:
默认 At Least Once
通过事务或 Publisher Confirm 实现可靠投递
Exactly Once 需要业务层配合(幂等)
Kafka:
默认 At Least Once
通过幂等生产者(enable.idempotence=true)实现 Exactly Once
通过 Kafka 事务实现跨分区的 Exactly Once
|
十一、常见消息模式
11.1 工作队列(Work Queue)
1
2
3
4
5
6
|
Producer → Queue → Consumer 1
→ Consumer 2
→ Consumer 3
多个消费者竞争消费,一条消息只被处理一次
适用:任务分发、耗时操作
|
11.2 发布订阅(Pub/Sub)
1
2
3
4
5
6
|
Producer → Topic → Consumer A
→ Consumer B
→ Consumer C
一条消息被所有消费者处理
适用:事件通知、数据同步
|
11.3 路由(Routing)
1
2
3
4
5
6
|
Producer → Exchange → Queue A(key=error)
→ Queue B(key=info)
→ Queue C(key=error,warning)
根据 routing key 分发到不同队列
适用:日志分级、按类型处理
|
11.4 延迟消息(Delayed Message)
1
2
3
4
|
Producer → Queue(TTL=30min)→ 死信 Queue → Consumer
消息在队列中等待 30 分钟后过期,进入死信队列被消费
适用:订单超时取消、定时提醒
|
11.5 RPC(远程过程调用)
1
2
3
4
5
6
7
8
9
|
Client → Request Queue → Server
Client ← Reply Queue ← Server
用消息队列实现 RPC:
1. 客户端发送消息到请求队列,带上 ReplyTo 和 CorrelationId
2. 服务端处理后将结果发到 ReplyTo 指定的队列
3. 客户端根据 CorrelationId 匹配响应
适用:跨服务的同步调用(不推荐,用 gRPC 更好)
|
十二、小结
本文学习了消息队列的核心概念:
- 消息队列的作用(异步、解耦、削峰)
- 两种消息模型(点对点、发布订阅)
- 核心概念(Producer、Consumer、Broker、Queue、Topic、Partition)
- 消息路由模式(Direct、Topic、Fanout)
- 确认机制和持久化
- 重试和死信
- 消息顺序性和幂等性
- 投递语义(At Most Once / At Least Once / Exactly Once)
- 常见消息模式
下一篇将深入 RabbitMQ:架构、Exchange、确认机制和 .NET 实战。