消息中间件进阶:RocketMQ 消息的消费(三)

系列第三篇:存储与原理篇(下)

你好,我们又见面了。

前两篇我们聊了消息怎么存(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 启动的关键区别

  1. 订阅 Topic:Consumer 启动时需要明确告诉 Broker"我要消费哪个 Topic"
  2. 注册监听器 :Consumer 需要注册 MessageListener,处理消息的业务逻辑就写在这里面
  3. 触发 Rebalance:这是消费特有的环节------Consumer 启动后会立即进行一次 Rebalance,确定自己负责消费哪些 Queue
  4. 消费位点: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 线程

  1. Consumer 启动后,内部有一个 PullMessageService 线程
  2. 这个线程不断从 Broker 拉取消息(每次拉取一批)
  3. 拉到的消息提交到线程池处理
  4. 处理完成后,继续拉取下一批

所以本质上,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

长轮询的好处

  1. 实时性:消息一到达,挂起的请求立即被唤醒,Consumer 几乎实时收到消息
  2. 节省资源:相比短轮询(每秒一次),长轮询大大减少了空请求的数量
  3. 保护 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 只分配给一个实例"

消费者组的核心作用

  1. 水平扩展:增加 Consumer 实例,就能线性提升消费吞吐量
  2. 消费进度管理:同一个 Group 共享一份消费进度(Offset),任意实例宕机后,其他实例可以接替继续消费
  3. 负载均衡: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)

具体的触发场景包括:

  1. Consumer 启动或关闭:Group 内成员变化,需要重新分配
  2. Topic 的 Queue 数量变化:新增或减少 Queue,需要重新分配
  3. Consumer 订阅信息变化:订阅的 Topic 或 Tag 变化
  4. 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 开始消费,就会重复消费那些已经拉取但还没提交进度的消息。

如何规避风险?

  1. 幂等消费:这是最基本的防御手段------业务层保证同一消息被多次消费时,效果和消费一次相同
  2. 合理设置超时:避免消费超时导致 Offset 提交延迟
  3. 使用同步提交 :在关键场景下,可以配置 commitOffset 为同步模式
  4. 监控 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

顺序消息的三个关键点

  1. 生产者端 :相同业务 Key 的消息,通过一致性哈希或取模,保证落到同一个 Queue
  2. Broker 端 :同一个 Queue 内的消息,在 CommitLog 中是顺序写入的(所有消息都是顺序写入,天然有序)
  3. 消费者端 :使用 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

重试机制的核心设计

  1. 重试队列 :消息消费失败后,不直接丢弃,而是发送到 %RETRY%{ConsumerGroup} 这个特殊的 Topic
  2. 延迟重试 :重试不是立即的,而是逐次增加延迟时间(1 秒 → 5 秒 → 10 秒 → 30 秒 → ...)
  3. 最大重试次数 :默认 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

死信队列的特点

  1. 不再被自动消费:死信队列中的消息不会被 Consumer 自动拉取
  2. 需要人工处理:需要通过运维工具或 API 手动处理
  3. 包含完整信息:死信消息保留了原始消息的所有信息,包括原始 Topic、Tag 和消息体
  4. 有生命周期:死信消息也有过期时间,默认 72 小时后会被清理

死信处理的最佳实践

  1. 监控告警:对 DLQ 中的消息数量设置监控,一旦有消息进入 DLQ,立即告警
  2. 人工补偿:从 DLQ 中拉取消息,分析失败原因,修复业务逻辑后重新发送
  3. 定期清理:设置合理的 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

积压处理的黄金原则

  1. 先止损:快速扩容 Consumer,让消费速度先追上来
  2. 再定位:分析根本原因,防止再次发生
  3. 临时 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 在本地事务+消息一致性上的表现。我们下期见!


系列文章:

  1. 入门认知篇 ✅
  2. 核心概念与架构篇 ✅
  3. 存储与原理篇(上)✅
  4. 存储与原理篇(中)✅
  5. 存储与原理篇(下)✅(本文)
  6. 事务消息(待续)
  7. ......
相关推荐
程序员老邢1 个月前
《技术底稿 46》AI 解构成果→知识库自动化同步管道 设计与落地总结
架构设计·异步任务·数据同步·后端开发·幂等性·技术底稿
shangjian0074 个月前
AI-大语言模型LLM-智能体应用如何保证幂等性
agent·幂等性
only-qi4 个月前
分布式系统四问:幂等、时钟、隔离、权衡
架构·分布式事务·幂等性·时钟回拨·性能与一致性权衡
切糕师学AI4 个月前
什么是幂等(Idempotence)?
数学·设计原则·幂等·幂等性
是码龙不是码农5 个月前
支付防重复下单|5 种幂等性设计方案(从初级到架构级)
java·架构·幂等性
没有bug.的程序员5 个月前
Spring Boot 与 Kafka:消息可靠性传输与幂等性设计的终极实战
java·spring boot·后端·kafka·幂等性·消息可靠
小楼v6 个月前
深入全面理解幂等性设计原理及实现幂等的主流方案
后端·雪花算法·幂等性·幂等设计
萧曵 丶6 个月前
接口幂等性保证方案详解
幂等性
better_liang7 个月前
每日Java面试场景题知识点之-RabbitMQ消息重复消费问题
java·分布式·消息队列·rabbitmq·幂等性