系列第三篇:存储与原理篇(下)
你好,我们又见面了。
前两篇我们聊了消息怎么存(CommitLog + ConsumeQueue)、怎么发(三种发送方式 + 负载均衡 + 重试机制)。现在,消息已经到了 Broker 的"仓库"里,安安静静地躺在 CommitLog 里等着被处理。
接下来就是最关键的一环------消息是怎么被消费的?
你可能已经听过"推模式"和"拉模式"这两个词,也可能听说过 Rebalance、消费位点、重试队列这些概念。今天我们就从 Consumer 启动的那一刻开始,把消息消费的每个环节彻底讲透。配合流程图,咱们把"消费"这回事看得明明白白。
七、消息的消费
Consumer 启动流程与路由获取
Consumer 的启动和 Producer 非常相似,但多了几个消费特有的步骤:
flowchart TD Start(创建 Consumer 实例) --> Step11. 设置 ConsumerGroup 名称\
(同一业务/应用的消费者属于同一个 Group) Step1 --> Step22. 设置 NameServer 地址列表 Step2 --> Step33. 配置消费模式\
(集群消费/广播消费)和消费位点(从哪开始消费) Step3 --> Step44. 订阅 Topic\
(指定 Topic 和 Tag 过滤表达式) Step4 --> Step55. 注册消息监听器\
(MessageListener,处理收到的消息) Step5 --> Step66. 调用 start 方法启动 Consumer Step6 --> Step77. 初始化 MQClientInstance\
(和 Producer 共用同一个实例) Step7 --> Step88. 启动 Netty 客户端\
(连接 NameServer 和 Broker) Step8 --> Step99. 从 NameServer 拉取订阅 Topic 的路由信息 Step9 --> Step1010. 触发首次 Rebalance\
(分配 Queue 给当前 Consumer) Step10 --> Step1111. 启动定时任务\
(每 30 秒更新路由 + 定时 Rebalance) Step11 --> End(Consumer 就绪,开始消费消息) style Step10 fill:#ffcdd2 style Step11 fill:#c8e6c9
和 Producer 启动的关键区别:
- 订阅 Topic:Consumer 启动时需要明确告诉 Broker"我要消费哪个 Topic"
- 注册监听器 :Consumer 需要注册
MessageListener,处理消息的业务逻辑就写在这里面 - 触发 Rebalance:这是消费特有的环节------Consumer 启动后会立即进行一次 Rebalance,确定自己负责消费哪些 Queue
- 消费位点:Consumer 需要知道从哪个位置开始消费(从最新的开始?还是从头开始?还是从上次消费的进度继续?)
Pull 模式与 Push 模式的本质区别
这是初学者最容易混淆的概念。表面上 RocketMQ 支持两种消费模式------Pull 和 Push,但在底层实现上,它们都是 Pull 模式。
本质区别:谁来控制拉取频率?
| 模式 | 控制权 | 实现方式 | 用户感知 |
|---|---|---|---|
| Pull | 用户自己控制 | 用户主动调用 pull() 方法 |
用户完全掌控拉取时机和批量大小 |
| Push | Broker"推" | 底层依然是不断拉取,但被封装成"推送"的感觉 | 用户只需实现监听器,消息来了就被调用 |
flowchart TB subgraph PullModePull 模式 U1用户代码 -->|主动调用 pull| C1Consumer C1 -->|拉取请求| B1Broker B1 -->|返回消息| C1 C1 -->|返回消息| U1 NotePull"用户自己控制:\
什么时候拉、拉多少条、间隔多久" end subgraph PushModePush 模式(底层也是 Pull) U2用户代码 -->|注册监听器| C2Consumer C2 -.->|后台线程<br>循环拉取| B2Broker B2 -.->|返回消息| C2 C2 -->|自动回调| U2 NotePush"用户无感知:\
消息来了自动回调监听器\
底层是长轮询或定时拉取" end style PullMode fill:#fff3e0 style PushMode fill:#e3f2fd
Push 模式的底层实现:
RocketMQ 的 Push 模式(DefaultMQPushConsumer)在底层其实是一个不断循环的 Pull 线程:
- Consumer 启动后,内部有一个
PullMessageService线程 - 这个线程不断从 Broker 拉取消息(每次拉取一批)
- 拉到的消息提交到线程池处理
- 处理完成后,继续拉取下一批
所以本质上,Push 模式 = 自动化的 Pull + 多线程消费。
长轮询机制的原理与实现
既然底层是 Pull,那 Broker 怎么做到"消息一来就推送给消费者"呢?答案就是长轮询(Long Polling)。
什么是长轮询?
传统的 Pull 是消费者定时去 Broker 问"有新消息吗?"------如果一直没有消息,每次请求都是空跑,浪费资源。
长轮询的优化是:如果 Broker 没有新消息,请求不立即返回,而是"挂起"等待一段时间(默认 15 秒)。如果在这段时间内有新消息到达,立即返回;如果超时了还没消息,返回空响应。
sequenceDiagram participant C as Consumer participant B as Broker Note over C,B: 有消息时的长轮询 C->>B: 拉取请求(offset = 100) B->>B: 检查 ConsumeQueue<br>发现 offset=100 处有消息 B-->>C: 立即返回消息 Note over C: ✅ 快速响应 Note over C,B: 没有消息时的长轮询 C->>B: 拉取请求(offset = 200) B->>B: 检查 ConsumeQueue<br>offset=200 处暂无消息 B->>B: 将请求挂起,放入 pending 队列 Note over B: 等待新消息(最长 15 秒) alt 15 秒内有新消息到达 B->>B: 新消息写入,触发唤醒 B-->>C: 返回新消息 else 15 秒超时 B-->>C: 返回空响应 C->>C: 继续发起下一次长轮询 end
长轮询的好处:
- 实时性:消息一到达,挂起的请求立即被唤醒,Consumer 几乎实时收到消息
- 节省资源:相比短轮询(每秒一次),长轮询大大减少了空请求的数量
- 保护 Broker:通过超时机制避免请求无限挂起导致资源泄漏
💡 小贴士 :长轮询的超时时间由
pullRequestHoldService控制,默认 15 秒。这个值不是越大越好------太大会占用 Broker 的线程资源,太小又会增加空请求的频率。
消费者组(Consumer Group)的概念与作用
什么是消费者组?
消费者组是一组具有相同消费逻辑的 Consumer 实例的集合。它们共同消费同一个 Topic 的消息。
graph TB subgraph ConsumerGroup消费者组:order_consumer_group C1Consumer 实例 1\
部署在服务器 A C2Consumer 实例 2\
部署在服务器 B C3Consumer 实例 3\
部署在服务器 C end subgraph TopicTopic: order_topic Q1Queue 0 Q2Queue 1 Q3Queue 2 Q4Queue 3 end Q1 --> C1 Q2 --> C1 Q3 --> C2 Q4 --> C3 Note1"同一个 Group 内的 Consumer 实例\
共同消费 Topic 的所有消息(集群消费)" Note2"不同实例之间负载均衡\
每个 Queue 只分配给一个实例"
消费者组的核心作用:
- 水平扩展:增加 Consumer 实例,就能线性提升消费吞吐量
- 消费进度管理:同一个 Group 共享一份消费进度(Offset),任意实例宕机后,其他实例可以接替继续消费
- 负载均衡:Group 内的实例自动分配 Queue,协同工作
💡 核心约束 :同一个消费者组内的 Consumer,必须订阅相同的 Topic 和 Tag。如果订阅不同,会导致 Rebalance 混乱和消息丢失。
集群消费与广播消费
RocketMQ 提供了两种消费模式,它们的区别非常核心:
集群消费(Clustering)
- 一个 Topic 下的消息,只被同一个 Group 中的一个 Consumer 消费一次
- 多个 Consumer 实例之间负载均衡分配 Queue
- 这是 RocketMQ 的默认模式
flowchart LR subgraph TopicTopic: order_topic M1消息1 --> Q1Queue0 M2消息2 --> Q2Queue1 M3消息3 --> Q3Queue2 M4消息4 --> Q4Queue3 end subgraph Group消费者组 C1Consumer A -->|消费| Q1 C2Consumer B -->|消费| Q2 C1 -->|消费| Q3 C2 -->|消费| Q4 end NoteCluster"每条消息只被消费一次\
多个 Consumer 分摊负载"
广播消费(Broadcasting)
- 一个 Topic 下的消息,被同一个 Group 中的每个 Consumer 都消费一次
- 每个 Consumer 独立消费所有 Queue 的消息
- 每个 Consumer 独立管理自己的 Offset
flowchart LR subgraph TopicTopic: order_topic M1消息1 --> Q1Queue0 M2消息2 --> Q2Queue1 end subgraph Group消费者组 C1Consumer A -->|消费| Q1 C1 -->|消费| Q2 C2Consumer B -->|消费| Q1 C2 -->|消费| Q2 end NoteBroadcast"每条消息被每个 Consumer 都消费一次\
每条消息被消费 N 次(N = 消费者数量)"
| 对比维度 | 集群消费 | 广播消费 |
|---|---|---|
| 消息投递 | 每条消息只投递给一个 Consumer | 每条消息投递给所有 Consumer |
| Offset 管理 | Group 共享一份 | 每个实例各自独立 |
| 适用场景 | 负载均衡、水平扩展 | 每个节点都需要全量数据(如缓存刷新) |
| 是否默认 | ✅ 默认 | ❌ 需显式配置 |
Rebalance 机制的触发时机与算法实现
Rebalance 是什么?
Rebalance(重平衡)是指消费者组内的 Queue 分配发生变化时,重新分配 Queue 给各个 Consumer 实例的过程。
触发时机:
flowchart TD Start(触发 Rebalance) --> T1{消费者实例<br>启动或关闭?} T1 -->|是| Do执行 Rebalance T1 -->|否| T2{Topic 的 Queue 数量<br>发生变化?} T2 -->|是| Do T2 -->|否| T3{消费者组<br>订阅信息变化?} T3 -->|是| Do T3 -->|否| T4{Broker 变化<br>(新增/宕机)?} T4 -->|是| Do T4 -->|否| End(不触发) Do --> Result(重新分配所有 Queue)
具体的触发场景包括:
- Consumer 启动或关闭:Group 内成员变化,需要重新分配
- Topic 的 Queue 数量变化:新增或减少 Queue,需要重新分配
- Consumer 订阅信息变化:订阅的 Topic 或 Tag 变化
- Broker 变化:某个 Broker 宕机或新增,影响 Queue 的可用性
Rebalance 的触发频率 :Consumer 内部有一个定时任务,每隔 20 秒 检查一次是否需要 Rebalance。
Rebalance 的算法:
RocketMQ 支持多种队列分配策略,通过 AllocateMessageQueueStrategy 接口来定义:
flowchart LR subgraph Strategy队列分配策略 A1平均分配\
AllocateMessageQueueAveragely A2环形分配\
AllocateMessageQueueAveragelyByCircle A3自定义\
实现接口 end subgraph Example分配示例\
4 个 Queue + 3 个 Consumer E1平均分配 E2环形分配 end E1 --> E1R"C1: Q0,Q1 \| C2: Q2 \| C3: Q3\
(尽量均匀)" E2 --> E2R"C1: Q0,Q3 \| C2: Q1 \| C3: Q2\
(轮流分配,类似发牌)" style A1 fill:#c8e6c9 style A2 fill:#e3f2fd
Rebalance 过程中的队列分配策略详解
我们来详细看两种主要策略:
1. 平均分配策略(默认)
AllocateMessageQueueAveragely------这是 RocketMQ 的默认策略,核心思想是尽可能让每个 Consumer 分配的 Queue 数量相等。
算法逻辑:
平均分配算法(伪代码):
1. 将所有 Queue 排序,将所有 Consumer 排序
2. 计算每个 Consumer 的平均 Queue 数:avg = queueCount / consumerCount
3. 计算余数:mod = queueCount % consumerCount
4. 前 mod 个 Consumer 各多分 1 个 Queue
5. 按顺序分配
| Consumer | Queue 分配 | 数量 |
|---|---|---|
| C1 | Q0, Q1, Q2 | 3 |
| C2 | Q3, Q4 | 2 |
| C3 | Q5 | 1 |
(假设 6 个 Queue,3 个 Consumer)
2. 环形分配策略
AllocateMessageQueueAveragelyByCircle------按"发牌"的方式轮流分配。
环形分配算法(伪代码):
1. 将所有 Queue 和 Consumer 排序
2. 从第 1 个 Consumer 开始,依次分配第 1, 2, 3... 个 Queue
3. 到达最后一个 Consumer 后,回到第 1 个继续
| Consumer | Queue 分配 | 数量 |
|---|---|---|
| C1 | Q0, Q3 | 2 |
| C2 | Q1, Q4 | 2 |
| C3 | Q2, Q5 | 2 |
(假设 6 个 Queue,3 个 Consumer------环形分配在均匀分布的场景下表现更好)
Rebalance 过程中的消息重复与位点重置风险
这是 Rebalance 最关键、也最容易出问题的地方。Rebalance 过程中,消息可能被重复消费,也可能被跳过。
flowchart TD subgraph Risk1⚠️ 消息重复风险 R1aConsumer A 消费 Queue0\
进度已更新到 Offset 100 R1bRebalance:Queue0 分配给 Consumer B R1cConsumer B 从 Offset 95 开始消费\
(因为进度还没同步到最新的 100) R1d结果:Offset 95-100 的消息被重复消费 end subgraph Risk2⚠️ 消息跳过风险 R2aConsumer A 消费 Queue0\
拉取了 Offset 100-120 的消息\
但还没提交消费进度 R2bRebalance:Queue0 分配给 Consumer B R2cConsumer B 从 Offset 100 开始消费 R2d但 Offset 100-120 的消息已经被 A 拉取\
如果 A 处理失败或被关闭,这些消息可能丢失 R2e结果:消息未被确认就被重新分配\
可能导致消息丢失或重复 end style Risk1 fill:#ffcdd2 style Risk2 fill:#ffcdd2
为什么会发生重复?
因为消费进度(Offset)的提交是异步的。Consumer 拉取了一批消息,可能在业务处理完之前,Rebalance 就触发了,Queue 被分配给了别的 Consumer。新的 Consumer 从上次提交的 Offset 开始消费,就会重复消费那些已经拉取但还没提交进度的消息。
如何规避风险?
- 幂等消费:这是最基本的防御手段------业务层保证同一消息被多次消费时,效果和消费一次相同
- 合理设置超时:避免消费超时导致 Offset 提交延迟
- 使用同步提交 :在关键场景下,可以配置
commitOffset为同步模式 - 监控 Rebalance 频率:频繁 Rebalance 是系统不稳定的信号
💡 小贴士 :RocketMQ 的设计哲学是 "至少一次(At Least Once)" 。它宁愿消息被重复消费,也不愿消息丢失。所以业务方一定要做好幂等处理。
消费位点(Offset)的管理与存储
什么是 Offset?
Offset 是 Consumer 消费进度的"书签",记录了当前消费到了哪个位置。
在 RocketMQ 中,Offset 指的是 ConsumeQueue 中的逻辑偏移量(第几条消息),而不是 CommitLog 中的物理偏移量。
flowchart LR subgraph ConsumeQueueConsumeQueue 内容 E1Entry 0\
消息 A E2Entry 1\
消息 B E3Entry 2\
消息 C E4Entry 3\
消息 D E5Entry 4\
消息 E E6Entry 5\
消息 F end subgraph Offset消费进度 O1"已消费到 Offset = 3\
(Entry 3 之前都已消费)" end E1 -->|已消费| D1✅ E2 -->|已消费| D2✅ E3 -->|已消费| D3✅ E4 -->|未消费| D4⏳ 当前位置 E5 -->|未消费| D5⏳ E6 -->|未消费| D6⏳ O1 -.-> E4 style O1 fill:#e3f2fd
Offset 的数值含义:
offset = 0:从头开始消费offset = N:从第 N 条消息开始消费offset = -1(实际是-1的补码):从最新消息开始消费
Offset 的存储位置演变
RocketMQ 中 Offset 的存储位置经历了从"本地"到"远端"的演变。
早期(4.x 及之前):
Offset 存储在 Broker 端 (默认是 ~/store/config/consumerOffset.json),通过 定期持久化 来保存。
5.x 的变化:
RocketMQ 5.x 引入了 Pop 消费模型 ,Offset 的管理更加灵活,支持将 Offset 存储到 独立的存储系统(如 Raft 集群),实现了 Offset 的高可用管理。
flowchart TB subgraph Offset存储方式 A传统方式\
Broker 本地存储 B5.x 新方式\
远端存储 end subgraph A_detailBroker 本地存储 A1Consumer 提交 Offset A2Broker 写入 consumerOffset.json A3每隔 5 秒持久化一次 A4⚠️ Broker 宕机可能丢失最近 5 秒的 Offset end subgraph B_detail远端存储(5.x) B1Consumer 提交 Offset B2通过 gRPC 提交到 Proxy B3Proxy 写入 Raft 集群 B4✅ 高可用,不丢失 end A --> A1 --> A2 --> A3 --> A4 B --> B1 --> B2 --> B3 --> B4 style A fill:#fff3e0 style B fill:#c8e6c9
集群消费 vs 广播消费的 Offset 存储:
| 消费模式 | Offset 存储位置 | 说明 |
|---|---|---|
| 集群消费 | Broker 端 | Group 共享一份 Offset |
| 广播消费 | 客户端本地 | 每个实例独立存储 |
消息拉取的流程与线程模型
Consumer 的消息拉取流程是一个精心设计的多线程协作模型:
flowchart TB subgraph Main主线程 M1Consumer.start --> M2启动 PullMessageService M2 --> M3启动 RebalanceService M3 --> M4启动 Consumer 线程池 end subgraph PullPullMessageService 线程 P1不断从 pullRequestQueue\
取出 PullRequest --> P2拉取消息 P2 --> P3收到消息 P3 --> P4提交到 Consumer 线程池 P4 --> P5创建新的 PullRequest\
继续拉取 end subgraph ConsumeConsumer 线程池 C1执行消息监听器 --> C2{消费成功?} C2 -->|是| C3提交 Offset C2 -->|否| C4重试或投递到 DLQ end M4 --> Consume Pull --> P5 P5 --> P1 style Pull fill:#e3f2fd style Consume fill:#c8e6c9
关键线程说明:
| 线程/组件 | 职责 |
|---|---|
| PullMessageService | 单线程,负责从 Broker 拉取消息,将消息提交到消费线程池 |
| RebalanceService | 单线程,定时检查并触发 Rebalance |
| Consumer 线程池 | 多线程,负责执行业务监听器,处理消息 |
| Offset 提交线程 | 定时或定频提交消费进度到 Broker |
消费线程池的配置:
java
// 设置消费线程池大小(默认 20)
consumer.setConsumeThreadMin(10);
consumer.setConsumeThreadMax(40);
- 线程池大小决定了消息消费的并行度
- 如果业务处理耗时较长(如调用第三方接口),需要适当增大线程池
- 但线程池过大会增加上下文切换开销,需要根据实际压测确定
并发消费与顺序消费的实现与区别
这是业务开发中最常遇到的选择:我的消息需要按顺序处理吗?
并发消费(Concurrently)
- 多个消息并行处理,不保证顺序
- 同一个 Queue 的消息,在不同的线程中并行处理
- 吞吐量最高,适合大多数业务场景
顺序消费(Orderly)
- 同一个 Queue 的消息串行处理,保证顺序
- 同一个 Queue 在同一时刻只能被一个线程处理
- 吞吐量低于并发消费,适合需要保证顺序的业务(如订单状态流转)
flowchart LR subgraph Concurrent并发消费 Q1Queue: 消息1,2,3,4,5 Q1 --> T1线程1: 消息1 Q1 --> T2线程2: 消息2 Q1 --> T3线程3: 消息3 Q1 --> T4线程4: 消息4 NoteCon"消息顺序不保证\
处理完成顺序: 2→1→4→3→5" end subgraph Orderly顺序消费 Q2Queue: 消息1,2,3,4,5 Q2 --> T5同一个线程\
串行处理 T5 --> R1消息1 T5 --> R2消息2 T5 --> R3消息3 NoteOrd"消息顺序严格保证\
处理顺序: 1→2→3→4→5" end style Concurrent fill:#e3f2fd style Orderly fill:#fff3e0
实现对比:
| 对比维度 | 并发消费 | 顺序消费 |
|---|---|---|
| 实现类 | MessageListenerConcurrently |
MessageListenerOrderly |
| 线程模型 | 多线程并行 | 单线程串行(每个 Queue) |
| 吞吐量 | 高 | 中 |
| 消息顺序 | 不保证 | 保证同 Queue 有序 |
| 适用场景 | 大部分业务 | 订单状态机、库存扣减等 |
顺序消息的实现原理
RocketMQ 的顺序消息依赖于 MessageQueue 级别的顺序保证。
flowchart TB subgraph Producer生产者端 P1消息: order_123: 创建 -->|哈希取模| Q1Queue 0 P2消息: order_123: 支付 -->|哈希取模| Q1 P3消息: order_123: 发货 -->|哈希取模| Q1 P4消息: order_456: 创建 -->|哈希取模| Q2Queue 1 P5消息: order_456: 支付 -->|哈希取模| Q2 end subgraph BrokerBroker 存储 Q1 --> Store1CommitLog\
顺序存储 Q2 --> Store1 end subgraph Consumer消费者端 Store1 --> Q1_ConsumeQueue 0\
单线程串行消费 Store1 --> Q2_ConsumeQueue 1\
单线程串行消费 end NoteOrder"同一个 Key(订单号)的消息\
始终落在同一个 Queue\
该 Queue 内消息严格有序" style Q1 fill:#c8e6c9 style Q2 fill:#e3f2fd
顺序消息的三个关键点:
- 生产者端 :相同业务 Key 的消息,通过一致性哈希或取模,保证落到同一个 Queue
- Broker 端 :同一个 Queue 内的消息,在 CommitLog 中是顺序写入的(所有消息都是顺序写入,天然有序)
- 消费者端 :使用
MessageListenerOrderly,同一个 Queue 串行消费,保证处理顺序
⚠️ 注意:顺序消息的吞吐量受限,因为每个 Queue 只能被一个线程串行处理。如果需要更高的吞吐量,可以增加 Queue 的数量来提升并行度(Queue 数越多,并行消费的线程越多)。
消息过滤的两种方式:Tag 与 SQL92
Consumer 在订阅 Topic 时,可以指定过滤条件,只消费感兴趣的消息。
Tag 过滤:
java
// 订阅时指定 Tag:只消费 order_create 和 order_pay
consumer.subscribe("order_topic", "order_create || order_pay");
Tag 过滤在 Broker 侧基于 哈希码 快速过滤,性能极高。
SQL92 过滤:
java
// 基于消息属性进行复杂过滤
consumer.subscribe("order_topic",
MessageSelector.bySql("amount > 100 AND region = 'SH'"));
flowchart TB subgraph TagTag 过滤 T1消息到达 Broker --> T2ConsumeQueue 中存储了 Tag 哈希码 T2 --> T3根据哈希码快速比对 T3 -->|匹配| T4发送给 Consumer T3 -->|不匹配| T5丢弃 NoteTag"性能:极高\
适用:简单分类" end subgraph SQLSQL92 过滤 S1消息到达 Broker --> S2需要解析消息的所有属性 S2 --> S3执行 SQL 表达式计算 S3 -->|TRUE| S4发送给 Consumer S3 -->|FALSE| S5丢弃 NoteSQL"性能:消耗 CPU\
适用:复杂业务规则" end style Tag fill:#c8e6c9 style SQL fill:#fff3e0
最佳实践:能用 Tag 就不用 SQL92。
消费进度(Consumer Offset)的提交时机
Offset 提交的时机直接影响了消费的可靠性 和性能。
RocketMQ 的 Offset 提交策略:
flowchart TB subgraph Concurrent并发消费 - 批量提交 C1拉取一批消息\
(默认 32 条) --> C2投递给线程池\
并行处理 C2 --> C3所有消息处理完成后\
批量提交 Offset C3 --> C4提交位置 = 这批消息\
的最大 Offset + 1 end subgraph Orderly顺序消费 - 逐个提交 O1拉取消息 --> O2逐个处理 O2 --> O3每条消息处理完成后\
立即提交 Offset O3 --> O4提交位置 = 当前处理\
消息的 Offset + 1 end style Concurrent fill:#e3f2fd style Orderly fill:#fff3e0
并发消费的 Offset 提交逻辑:
- Consumer 拉取一批消息(比如 32 条),提交到线程池并行处理
- 只有当这一批消息全部处理完成后,才提交 Offset
- 提交的位置是这批消息中最大的 Offset + 1
这意味着:如果 32 条消息中有 1 条处理失败,整批消息的 Offset 都不会提交。失败的那条消息会被重试,但已经成功处理的 31 条消息可能会被重复消费(因为下次拉取会从这 32 条的起始位置开始)。
这就是 RocketMQ "至少一次"语义的体现------宁可重复,不可丢失。
提交方式配置:
| 方式 | 说明 | 配置 |
|---|---|---|
| 同步提交 | 提交 Offset 后等待 Broker 确认 | autoCommit = false + 手动提交 |
| 异步提交 | 不等待确认,性能更好 | 默认方式 |
消息重试机制与重试队列
消息消费失败怎么办?RocketMQ 有完善的重试机制。
flowchart TB Start消息消费 --> Check{消费成功?} Check -->|是| Done✅ 提交 Offset Check -->|否| Retry进入重试 Retry --> Level1{重试次数<br>≤ 最大重试次数?} Level1 -->|是| SendRetry发送到\
%RETRY%ConsumerGroup SendRetry --> Delay延迟投递\
延迟时间逐次递增 Delay --> Consumer再次消费 Consumer --> Check Level1 -->|否| DLQ投递到\
%DLQ%ConsumerGroup\
死信队列 DLQ --> Manual需要人工介入处理 style Done fill:#c8e6c9 style DLQ fill:#ffcdd2 style Manual fill:#ffcdd2
重试机制的核心设计:
- 重试队列 :消息消费失败后,不直接丢弃,而是发送到
%RETRY%{ConsumerGroup}这个特殊的 Topic - 延迟重试 :重试不是立即的,而是逐次增加延迟时间(1 秒 → 5 秒 → 10 秒 → 30 秒 → ...)
- 最大重试次数 :默认 16 次(并发消费)或 10 次(顺序消费),可以通过
consumer.setMaxReconsumeTimes()配置
延迟递进的规律:
1s → 5s → 10s → 30s → 1m → 2m → 3m → 4m → 5m → 6m → 7m → 8m → 9m → 10m → 20m → 30m → 1h → 2h
这个延迟策略给系统足够的恢复时间(比如数据库重启、网络恢复等),避免无效的频繁重试。
如何判断失败需要重试?
- 如果
MessageListener返回ConsumeConcurrentlyStatus.RECONSUME_LATER,表示消费失败,需要重试 - 如果抛出异常,也视为消费失败
- 只有返回
CONSUME_SUCCESS才表示成功
死信队列(DLQ)的产生与处理
什么是死信队列(DLQ)?
当消息重试次数超过最大重试次数 后,仍然消费失败,这条消息就会被投递到 死信队列(Dead Letter Queue)。
死信队列的命名规范:%DLQ%{ConsumerGroup}
flowchart LR subgraph Process消费流程 M消息 -->|重试 16 次都失败| DLQ死信队列\
%DLQ%order_consumer_group DLQ --> Alert告警通知 DLQ --> Manual人工介入 Manual --> Fix修复问题 Fix --> Resend重新发送到业务 Topic Resend --> Consume重新消费 end style DLQ fill:#ffcdd2 style Manual fill:#ffcdd2
死信队列的特点:
- 不再被自动消费:死信队列中的消息不会被 Consumer 自动拉取
- 需要人工处理:需要通过运维工具或 API 手动处理
- 包含完整信息:死信消息保留了原始消息的所有信息,包括原始 Topic、Tag 和消息体
- 有生命周期:死信消息也有过期时间,默认 72 小时后会被清理
死信处理的最佳实践:
- 监控告警:对 DLQ 中的消息数量设置监控,一旦有消息进入 DLQ,立即告警
- 人工补偿:从 DLQ 中拉取消息,分析失败原因,修复业务逻辑后重新发送
- 定期清理:设置合理的 DLQ 消息保留时间,避免堆积占用存储
java
// 从死信队列中重新消费
public void consumeFromDLQ() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dlq_consumer");
consumer.setNamesrvAddr("localhost:9876");
// 订阅死信队列
consumer.subscribe("%DLQ%order_consumer_group", "*");
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
// 获取原始消息信息
String originalTopic = msgs.get(0).getProperty("RETRY_TOPIC");
// 人工处理后重新发送
resendMessage(msgs.get(0));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
}
消息积压的原因与处理方案
消息积压 是 RocketMQ 生产中常见的问题,指的是 Consumer 处理消息的速度赶不上 Producer 生产消息的速度。
flowchart TB subgraph Normal正常情况 P1生产 1000条/s --> Q1Queue Q1 -->|消费 1000条/s| C1Consumer NoteNormal"生产速度 = 消费速度" end subgraph Backlog积压情况 P2生产 5000条/s --> Q2Queue Q2 -->|消费 1000条/s| C2Consumer NoteBacklog"⚠️ 生产速度 \> 消费速度\
消息堆积越来越多" end style Normal fill:#c8e6c9 style Backlog fill:#ffcdd2
积压的常见原因:
| 原因 | 说明 | 解决方案 |
|---|---|---|
| 消费速度慢 | 业务处理逻辑复杂,单条消息处理时间长 | 优化业务逻辑,增加 Consumer 实例 |
| Consumer 数量不足 | Queue 数量远大于 Consumer 数量 | 增加 Consumer 实例(不超过 Queue 数量) |
| 下游依赖故障 | 消费时依赖的数据库/API 响应慢或不可用 | 快速恢复下游服务,或用异步处理 |
| 消息体过大 | 大消息导致网络传输和解析耗时 | 压缩消息体,或将大内容存 OSS |
| 顺序消费 | 顺序消费串行处理,吞吐量受限 | 考虑是否可以改为并发消费 |
积压处理方案:
flowchart TD Start发现消息积压 --> Step11. 确认积压程度\
有多少条消息未消费? Step1 --> Step22. 定位原因\
Consumer 挂了?下游慢了?生产突发? Step2 --> Step3{原因判断} Step3 -->|Consumer 挂了| Fix1重启 Consumer\
或扩容实例 Step3 -->|消费速度慢| Fix2临时增加 Consumer 实例\
注意不超过 Queue 数量 Step3 -->|生产突增| Fix3扩容 Consumer 实例\
或临时开启更多 Queue Step3 -->|顺序消息积压| Fix4增加 Topic 的 Queue 数量\
重新分配 Key 让消息更均匀 Fix1 --> Monitor持续监控消费进度 Fix2 --> Monitor Fix3 --> Monitor Fix4 --> Monitor Monitor --> Done积压消除后\
逐步恢复原配置 style Start fill:#ffcdd2 style Done fill:#c8e6c9
积压处理的黄金原则:
- 先止损:快速扩容 Consumer,让消费速度先追上来
- 再定位:分析根本原因,防止再次发生
- 临时 vs 长期:扩容是临时手段,优化业务逻辑和架构才是根本
💡 小贴士 :RocketMQ 提供了
getConsumerRunningInfo()和viewMessage()等 API,可以帮助快速定位积压原因。另外,RocketMQ Dashboard 提供了可视化的积压监控界面,建议在生产环境部署。
小结
这篇我们完整走通了消息消费的全链路,通过 9 张流程图,搞清楚了:
- Consumer 启动时如何订阅 Topic 并触发首次 Rebalance
- Pull 与 Push 的本质区别------底层都是 Pull,Push 是自动化的 Pull
- 长轮询机制如何实现"准实时"的消息推送
- 消费者组 的概念和集群消费 vs 广播消费的区别
- Rebalance 的触发时机、分配算法以及在过程中的风险(消息重复与位点重置)
- Offset 的管理与存储演变(本地 → 远端)
- 拉取流程与线程模型------PullMessageService + 消费线程池的协作
- 并发消费 vs 顺序消费的选择和实现原理
- 顺序消息如何依赖 MessageQueue 级别的顺序保证
- 消息过滤的 Tag 和 SQL92 两种方式
- Offset 提交时机如何影响"至少一次"语义
- 消息重试机制 和死信队列的产生与处理
- 消息积压的原因分析与处理方案
现在,你对 RocketMQ 消息消费的每个环节应该都了如指掌了。再加上前两篇的存储和发送机制,你已经形成了一个完整的 RocketMQ 知识闭环。下一篇,我们将进入事务消息篇,看看 RocketMQ 在本地事务+消息一致性上的表现。我们下期见!
系列文章:
- 入门认知篇 ✅
- 核心概念与架构篇 ✅
- 存储与原理篇(上)✅
- 存储与原理篇(中)✅
- 存储与原理篇(下)✅(本文)
- 事务消息(待续)
- ......