消息队列(一):核心概念

写在前面

本文是消息队列系列的第一篇,介绍消息队列的核心概念、常见模式和可靠性保证机制。这些知识是理解 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)

示例:
  订单创建事件 → 同时发送到短信队列、积分队列、统计队列

4.4 头部路由(Headers)

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 实战。