--- tags: - MQ create time: 2026-05-24 19:52 --- # MQ 消息模型 ## 概述 消息模型决定了消息如何从生产者流向消费者。本文对比队列模型与发布订阅模型,深入剖析 Consumer Group 机制,比较 Push/Pull 两种消费模式,并梳理主流 MQ 的消息模型设计。 ## 正文 ### 队列模型 vs 发布订阅模型 消息队列领域有两种基本模型,理解它们的区别是掌握所有 MQ 的基础。 **队列模型(Point-to-Point)**:一条消息只能被一个消费者消费。就像排队取号,每个号码只能被一个窗口叫到。 **发布订阅模型(Pub/Sub)**:一条消息可以被多个消费者组各自消费一次。就像广播,每个家庭都能听到同一条新闻。 ```mermaid graph TD subgraph "Queue Model" P1["Producer"] --> Q1["Queue"] Q1 --> C1["Consumer A"] Q1 --> C2["Consumer B"] Q1 --> C3["Consumer C"] end ``` ```mermaid graph TD subgraph "Pub/Sub Model" P2["Producer"] --> T1["Topic"] T1 --> SUB1["Consumer Group A"] T1 --> SUB2["Consumer Group B"] SUB1 --> CA1["Consumer A1"] SUB1 --> CA2["Consumer A2"] SUB2 --> CB1["Consumer B1"] end ``` > [!question] > 如果队列模型中,你想让同一条消息被两个不同的服务分别处理,有什么办法? ### Consumer Group 机制详解 **为什么需要 Consumer Group?** 假设一个 Topic 有 100 万条消息需要处理,单个消费者处理太慢。你需要多个消费者并行消费,但又不想同一条消息被重复处理——Consumer Group 就是为此而生的。 同一个 Consumer Group 内的消费者**分摊**消息(每条消息只被组内一个消费者处理),不同 Consumer Group **各自独立**消费全量消息。 **如何分配?** 以 Kafka 为例,分配策略的核心逻辑: ```mermaid graph TD T["Topic: 6 Partitions"] --> P0["Partition 0"] T --> P1["Partition 1"] T --> P2["Partition 2"] T --> P3["Partition 3"] T --> P4["Partition 4"] T --> P5["Partition 5"] P0 --> C1["Consumer 1"] P1 --> C1 P2 --> C2["Consumer 2"] P3 --> C2 P4 --> C3["Consumer 3"] P5 --> C3 ``` 常见的分配策略: - **Range**:按范围连续分配,简单但容易不均衡 - **RoundRobin**:轮询分配,更均匀但要求 Topic 数一致 - **Sticky**:粘性分配,Rebalance 时尽量保持原有分配,减少不必要的重分配 **Rebalance 触发条件** 当以下事件发生时,Consumer Group 会触发 Rebalance(重新分配 Partition): 1. 新消费者加入组 2. 消费者离开组(主动退出或心跳超时) 3. Topic 的 Partition 数量变化 Rebalance 期间所有消费者暂停消费,因此**Rebalance 的速度和频率直接影响系统可用性**。 > [!question] > 如果一个消费者处理消息特别慢,导致心跳超时被踢出组,会触发 Rebalance。Rebalance 又会导致更多消息积压——这是一个恶性循环,你有什么解决思路? ### Push 模式 vs Pull 模式 | 维度 | Push 模式 | Pull 模式 | |------|----------|----------| | 实时性 | 高,消息到达即推送 | 取决于拉取频率 | | 消费者压力 | 被动接收,容易被打爆 | 主动拉取,按自身能力控制速率 | | 吞吐量 | 低(频繁推送开销大) | 高(批量拉取) | | 实现复杂度 | Broker 端复杂(需维护推送状态) | Consumer 端复杂(需实现拉取循环) | | 典型代表 | RabbitMQ | Kafka | Push 模式就像外卖送到家门口——方便,但高峰期外卖员不停按门铃你也会崩溃。Pull 模式就像自己去快递柜取件——按自己的时间来,但需要你主动去"查"。 ### 主流 MQ 的消息模型 #### Kafka:Consumer Group + Pull Kafka 的核心设计哲学是"消费者自己来拉": - 每个 Topic 分为多个 Partition,是并行消费的基本单元 - Consumer Group 内的消费者与 Partition 一一绑定(一个 Partition 只能被组内一个消费者消费) - 消费者通过 Offset 追踪消费进度,支持回溯消费 ```go // Kafka 消费者组: Pull 模式 func main() { consumer, _ := kafka.NewConsumer(&kafka.ConfigMap{ "bootstrap.servers": "localhost:9092", "group.id": "order-service-group", // Consumer Group "auto.offset.reset": "earliest", // 从最早消息开始消费 }) consumer.SubscribeTopics([]string{"order_created"}, nil) for { msg, _ := consumer.ReadMessage(-1) // 主动拉取,阻塞等待 processOrder(msg.Value) } } ``` #### RabbitMQ:Exchange + Push RabbitMQ 使用 Exchange(交换器)实现灵活的消息路由: - Producer 将消息发送到 Exchange,而非直接发送到 Queue - Exchange 根据 Binding Rule(绑定规则)将消息路由到一个或多个 Queue - Broker 主动将消息推送给订阅了 Queue 的 Consumer Exchange 的四种类型:Direct(精确匹配)、Topic(通配符匹配)、Fanout(广播)、Headers(头部匹配)。 #### RocketMQ:ConsumerGroup + Pull RocketMQ 的模型介于 Kafka 和 RabbitMQ 之间: - 采用 ConsumerGroup 概念,与 Kafka 类似 - 默认使用长轮询 Pull 模式(Push 的底层实现其实是 Pull 封装) - 支持顺序消费和延迟消息等高级特性 ### Go 代码示例:消费者组消费逻辑 下面是一个简化的消费者组实现,演示了 Partition 分配和消费的核心逻辑: ```go type ConsumerGroup struct { GroupID string Consumers []string Partitions []int } // Rebalance: 将 Partition 均匀分配给消费者 func (cg *ConsumerGroup) Rebalance() map[string][]int { assignment := make(map[string][]int) sort.Strings(cg.Consumers) for i, partition := range cg.Partitions { // 轮询分配: Partition i 分配给 Consumer i % len consumer := cg.Consumers[i%len(cg.Consumers)] assignment[consumer] = append(assignment[consumer], partition) } return assignment } // 消费循环: 拉取消息并处理 func Consume(broker Broker, topic string, groupID string, handler func(Message)) { partitions := broker.GetPartitions(topic) assignment := assignPartitions(groupID, partitions) for _, partition := range assignment { go func(p int) { offset := broker.GetOffset(groupID, p) for { msgs := broker.Pull(p, offset, batchSize) // 批量拉取 for _, msg := range msgs { handler(msg) } offset += len(msgs) broker.CommitOffset(groupID, p, offset) // 提交消费进度 } }(partition) } } ``` 这段代码展示了三个关键设计: 1. **轮询分配**保证 Partition 均匀分布到各消费者 2. **批量拉取**提升吞吐量,减少网络往返 3. **手动提交 Offset** 确保消息不丢失(处理完才提交) > [!question] > Push 模式看起来更实时,为什么 Kafka 选择了 Pull? Kafka 选择 Pull 模式有三个核心考量: 1. **消费者速率控制**:消费者按自己的处理能力拉取,天然避免被打爆。Push 模式下 Broker 不知道消费者的处理速度,需要额外的流控机制。 2. **批量消费**:Pull 模式允许消费者一次拉取一批消息,显著提升吞吐量。Push 模式逐条推送,网络开销大。 3. **消息回溯**:Pull 模式下消费者可以自由控制 Offset,实现"回放"历史消息。Push 模式下消息被推送后就消费掉了,回溯困难。 当然 Pull 模式的代价是实时性不如 Push——但 Kafka 通过**长轮询(Long Polling)**弥补了这个短板:消费者发起拉取请求,如果没有新消息就阻塞等待,直到有消息到达或超时。 ## 关联笔记 - [[1-MQ-基础概念]] - [[2-MQ-适用场景与选型原则]]