183 lines
7.5 KiB
Markdown
183 lines
7.5 KiB
Markdown
---
|
||
tags:
|
||
- MQ
|
||
- 基础概念
|
||
- 异步
|
||
- 架构
|
||
create time: 2026-05-24 19:52
|
||
---
|
||
|
||
# MQ 基础概念
|
||
|
||
## 概述
|
||
|
||
消息队列(Message Queue,简称 MQ)是一种异步通信机制,允许应用程序通过消息的生产和消费来实现解耦与协作。本文从生活类比出发,梳理核心术语、Pull/Push 两种消费模型、基本工作流程、MQ 的三大核心价值,以及引入 MQ 需要付出的代价。
|
||
|
||
## 正文
|
||
|
||
### 什么是消息队列
|
||
|
||
想象一下你去寄快递:你把包裹交给快递柜,快递柜暂存包裹,收件人有空时再来取件。你不需要知道收件人什么时候在家,快递柜也不关心包裹里装的是什么——三方各司其职。
|
||
|
||
消息队列做的事情完全一样:**Producer(生产者)把消息投递到 Broker(消息服务器),Broker 暂存消息,Consumer(消费者)按自己的节奏拉取并处理。** 生产和消费在时间上解耦,双方不需要同时在线。
|
||
|
||
> [!question]
|
||
> 生活中还有哪些场景天然就是"消息队列"的模式?食堂排队打饭算不算?
|
||
|
||
### 核心术语图解
|
||
|
||
| 术语 | 说明 | 类比 |
|
||
|------|------|------|
|
||
| **Producer** | 消息的发送方 | 寄件人 |
|
||
| **Broker** | 消息服务器,负责存储和转发 | 快递柜 / 邮局 |
|
||
| **Consumer** | 消息的接收方和处理方 | 收件人 |
|
||
| **Topic** | 消息的逻辑分类,Producer 按 Topic 发送 | 快递柜的格口编号 |
|
||
| **Queue** | 消息的物理存储单元 | 实际的格口 |
|
||
| **Message** | 传输的数据单元 | 包裹 |
|
||
| **Offset** | 消息在队列中的位置标识,用于消费进度追踪 | 快递单号 |
|
||
| **Partition** | Topic 的物理分片,实现并行消费 | 同一排快递柜的不同列 |
|
||
|
||
### 基本工作流程
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant P as "Producer"
|
||
participant B as "Broker"
|
||
participant C as "Consumer"
|
||
|
||
P->>B: 1. 发送消息到 Topic
|
||
B->>B: 2. 持久化消息,分配 Offset
|
||
C->>B: 3. 拉取消息(Pull)或接收推送(Push)
|
||
B-->>C: 4. 返回消息
|
||
C->>C: 5. 处理业务逻辑
|
||
C->>B: 6. 提交消费确认(Ack)
|
||
```
|
||
|
||
> [!question]
|
||
> 第 6 步的 Ack 确认机制为什么重要?如果没有 Ack 会怎样?
|
||
|
||
### Pull vs Push:两种消费模型
|
||
|
||
流程图第 3 步提到了"拉取"和"推送"两种方式,这其实是消息消费的两种根本模型:
|
||
|
||
| 维度 | **Pull(拉取)** | **Push(推送)** |
|
||
|------|-----------------|-----------------|
|
||
| 主动方 | Consumer 主动拉取 | Broker 主动推送 |
|
||
| 消费节奏 | Consumer 自己控制,想拉多少拉多少 | Broker 控制推送速率,Consumer 被动接收 |
|
||
| 适用场景 | 消费能力不均匀、需要精确控速 | 低延迟要求、实时性敏感 |
|
||
| 典型代表 | Kafka、RocketMQ(默认) | RabbitMQ、MQTT |
|
||
|
||
**简单理解**:Pull 是"我去超市买东西"(我决定什么时候买、买多少),Push 是"外卖送到家门口"(商家决定什么时候送)。
|
||
|
||
> [!tip]
|
||
> 大多数现代 MQ 默认采用 Pull 模式,因为 Consumer 更清楚自己的处理能力,天然具备背压(Backpressure)能力。详细的流控与背压机制见 [[31-MQ-背压与流控]]。
|
||
|
||
### MQ 的三大核心价值
|
||
|
||
#### 1. 解耦
|
||
|
||
没有 MQ 时,下单服务需要直接调用库存、积分、物流等多个下游服务,形成蜘蛛网般的依赖。引入 MQ 后,下单服务只需发一条消息,各下游自行订阅。
|
||
|
||
```go
|
||
// 解耦前: 下单服务直接依赖所有下游
|
||
func CreateOrder(order Order) {
|
||
db.Save(order)
|
||
inventorySvc.Deduct(order.ItemID, order.Qty) // 强依赖
|
||
pointsSvc.Add(order.UserID, order.Points) // 强依赖
|
||
logisticsSvc.Create(order) // 强依赖
|
||
// 新增通知服务?改代码、重新部署...
|
||
}
|
||
|
||
// 解耦后: 只管发消息,下游自己订阅
|
||
func CreateOrder(order Order) {
|
||
db.Save(order)
|
||
mq.Publish("order_created", order) // 一条消息,谁关心谁订阅
|
||
}
|
||
```
|
||
|
||
解耦的本质是把**"我知道谁需要"变成"谁需要谁知道"**,新增下游服务时生产者代码零改动。
|
||
|
||
#### 2. 异步
|
||
|
||
有些操作不需要用户等待结果。比如注册后发欢迎邮件、生成报表等,可以把这些耗时操作异步化。
|
||
|
||
```go
|
||
// 同步: 用户等所有操作完成,响应慢
|
||
func Register(user User) {
|
||
db.Save(user) // 50ms
|
||
emailSvc.SendWelcome(user) // 200ms
|
||
analyticsSvc.Track(user) // 100ms
|
||
// 总耗时 350ms,用户等到怀疑人生
|
||
}
|
||
|
||
// 异步: 用户只等核心操作,其余扔给 MQ
|
||
func Register(user User) {
|
||
db.Save(user) // 50ms
|
||
mq.Publish("user_registered", user) // 5ms
|
||
// 总耗时 55ms,邮件和埋点慢慢处理
|
||
}
|
||
```
|
||
|
||
#### 3. 削峰
|
||
|
||
秒杀场景下,瞬间流量可能打垮数据库。MQ 充当缓冲区,让下游按自己的处理能力消费消息。
|
||
|
||
```go
|
||
// 削峰前: 瞬时 1 万 QPS 直接打到数据库,DB 直接跪
|
||
func Seckill(userID, itemID string) {
|
||
if stock > 0 {
|
||
db.UpdateStock(itemID, -1) // DB 瞬间被打爆
|
||
}
|
||
}
|
||
|
||
// 削峰后: 先接住流量,慢慢消费
|
||
func Seckill(userID, itemID string) {
|
||
mq.Publish("seckill_request", SeckillReq{userID, itemID}) // 瞬时接住
|
||
}
|
||
|
||
// 消费者按 DB 承受能力匀速消费
|
||
func ConsumeSeckill() {
|
||
for msg := range mq.Subscribe("seckill_request") {
|
||
if stock > 0 {
|
||
db.UpdateStock(msg.ItemID, -1) // 匀速处理,DB 稳如老狗
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### MQ vs 直接调用
|
||
|
||
| 维度 | 直接调用 | 消息队列 |
|
||
|------|---------|---------|
|
||
| 耦合度 | 强耦合,调用方需感知下游 | 松耦合,生产者不关心消费者 |
|
||
| 响应时间 | 等待所有下游完成 | 只等核心链路,非核心异步处理 |
|
||
| 可靠性 | 下游挂了直接失败 | Broker 持久化,支持重试 |
|
||
| 吞吐能力 | 受限于最慢的下游 | 通过缓冲削峰,提升整体吞吐 |
|
||
| 复杂度 | 简单直观 | 引入额外组件,运维成本上升 |
|
||
| 一致性 | 容易保证强一致性 | 最终一致性,需要额外设计 |
|
||
|
||
> [!question]
|
||
> 如果不需要解耦和削峰,还有必要引入 MQ 吗?
|
||
|
||
### MQ 引入的代价
|
||
|
||
MQ 不是银弹,引入它就意味着接受以下挑战:
|
||
|
||
1. **运维复杂度上升**:多了一个需要监控、扩容、容灾的中间件。Broker 挂了怎么办?消息积压怎么告警?这些都是新增的运维命题。
|
||
2. **消息丢失与重复**:网络不可靠,消息可能丢、可能重复投递。你需要设计 ACK 机制、幂等消费、甚至 Exactly-Once 语义——这些都不简单。
|
||
3. **系统可用性转移**:原本你的可用性取决于自己,现在还取决于 Broker。Broker 成了新的单点风险。
|
||
4. **调试链路变长**:同步调用出了问题,顺着调用栈就能排查。引入 MQ 后,消息可能在队列里"躺"了很久才被消费,排查问题的难度显著增加。
|
||
|
||
> [!warning]
|
||
> 在决定引入 MQ 之前,先问自己:**我真正需要的是解耦、异步还是削峰?** 如果答案都是"不太需要",直接调用可能是更简单可靠的选择。何时该用 MQ 见 [[2-MQ-适用场景与选型原则]]。
|
||
|
||
> [!tip]
|
||
> **一句话总结**:MQ 通过引入"中间人"角色,实现了生产者与消费者之间的解耦、异步和削峰,但代价是更高的系统复杂度和最终一致性。理解这个 trade-off,是用好 MQ 的第一步。
|
||
|
||
## 关联笔记
|
||
|
||
- [[2-MQ-适用场景与选型原则]]
|
||
- [[3-MQ-消息模型]]
|
||
- [[12-MQ-消息确认与持久化]] — Ack 机制的详细设计
|
||
- [[28-MQ-Competing-Consumers-模式]] — Partition 并行消费的实战模式
|