系列第四阶段:进阶应用篇
你好,我们又见面了。
经过前面三阶段的学习,你已经掌握了 RocketMQ 的核心架构、存储原理、发送消费机制以及事务消息。可以说,你现在已经是一名合格的 RocketMQ 开发者了。
但"合格"和"精通"之间,还差一个进阶的距离。在实际业务中,你会遇到各种各样的特殊需求:
- 订单状态流转必须严格按顺序执行,不能乱序
- 需要定时或延迟发送消息,比如 30 分钟后自动取消未支付订单
- 消息出了问题,需要快速定位,知道它经历了什么
- 不同的消费者只需要消费特定类型的消息,减少无效传输
今天这篇文章,我们就来集中攻克这四大进阶技能:顺序消息、延迟消息、消息轨迹、消息过滤。每一个都是生产环境中的高频需求。老规矩,配合流程图,一步一图。
九、顺序消息
顺序消息的概念与业务场景
什么是顺序消息?
顺序消息是指消息的发送顺序和消费顺序保持一致。比如你发了消息 A、B、C,那么 Consumer 收到和处理消息的顺序也一定是 A、B、C。
典型业务场景:
| 场景 | 为什么需要顺序 |
|---|---|
| 订单状态流转 | 创建 → 支付 → 发货 → 完成,必须按顺序执行,绝不能先完成再创建 |
| 库存扣减日志 | 同一商品的入库和出库操作,必须按时间顺序处理,否则库存数据会错乱 |
| 数据库 binlog 同步 | 数据库变更日志必须按顺序回放,否则数据不一致 |
| IoT 设备指令 | 对同一设备的控制指令必须有序执行(如开 → 关 → 开) |
flowchart LR subgraph Wrong❌ 乱序消费 W1创建订单 --> W3发货 W2支付 --> W3 W1 --> W2 NoteWrong"支付可能在创建之前被消费\
→ 数据错乱" end subgraph Right✅ 顺序消费 R1创建订单 --> R2支付 --> R3发货 --> R4完成 NoteRight"严格按照业务顺序执行\
→ 数据正确" end style Wrong fill:#ffcdd2 style Right fill:#c8e6c9
全局顺序消息与分区顺序消息
RocketMQ 的顺序消息分为两个级别:
全局顺序消息
- 一个 Topic 下只有一个 MessageQueue
- 所有消息严格按顺序发送和消费
- 代价:吞吐量极低(单队列串行,无法并行)
flowchart LR P1消息1 --> Q1唯一的 Queue P2消息2 --> Q1 P3消息3 --> Q1 Q1 --> C1Consumer\
串行消费 NoteGlobal"全局有序 = 1 个 Topic 只有 1 个 Queue\
性能瓶颈明显,一般不建议"
分区顺序消息
- Topic 下有多个 Queue,但同一个业务 Key 的消息始终落到同一个 Queue
- 不同 Key 的消息可以在不同 Queue 中并行,互不影响
- 这是 RocketMQ 的默认推荐方式,既保证了顺序,又保持了并行度
flowchart LR subgraph Keys业务 Key 分组 K1订单 A: 创建→支付→发货 K2订单 B: 创建→支付→发货 K3订单 C: 创建→支付→发货 end subgraph Queues多队列并行 K1 -->|哈希取模| Q1Queue 0\
串行消费订单 A K2 -->|哈希取模| Q2Queue 1\
串行消费订单 B K3 -->|哈希取模| Q3Queue 2\
串行消费订单 C end NotePartition"同一个订单的消息在同一个 Queue 中严格有序\
不同订单在不同的 Queue 中并行消费"
顺序消息的发送原理(队列选择器 MessageQueueSelector)
顺序消息的发送关键在生产者端------必须保证相同业务 Key 的消息被分配到同一个 Queue。
RocketMQ 提供了 MessageQueueSelector 接口,让你自定义选择逻辑:
java
// 顺序消息发送示例
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// arg 是业务 Key,比如订单 ID
String orderId = (String) arg;
// 对订单 ID 取模,确保同一个订单始终落在同一个 Queue
int index = Math.abs(orderId.hashCode()) % mqs.size();
return mqs.get(index);
}
}, orderId);
flowchart TB subgraph Selector队列选择器工作流程 S1消息携带业务 Key\
如 orderId = 12345 --> S2计算 Key 的哈希值 S2 --> S3对可用 Queue 数量取模 S3 --> S4返回固定的 Queue end subgraph Result结果 R1order_123: Queue 0 R2order_678: Queue 1 R3order_123 再次发送: Queue 0 end Selector --> Result NoteSelector"相同 Key → 相同 Queue\
不同 Key → 可能不同 Queue"
关键点:
- 如果某个 Broker 宕机,该 Broker 上的 Queue 会从可用列表中移除,此时哈希取模的结果会变化,可能导致顺序被打破。RocketMQ 的故障规避机制在这种情况下会特殊处理,尽量保持顺序,但极端情况下仍可能乱序。所以顺序消息的 Broker 高可用很重要。
顺序消息的消费原理(顺序消费线程模型)
顺序消息在消费端通过 MessageListenerOrderly 实现,其核心是每个 Queue 由单线程串行消费:
flowchart TB subgraph Pull拉取层 PMPullMessageService -->|拉取| Q1Queue 0 PM -->|拉取| Q2Queue 1 PM -->|拉取| Q3Queue 2 end subgraph QueueLock队列锁 Q1 --> Lock1锁定 Queue 0 Q2 --> Lock2锁定 Queue 1 Q3 --> Lock3锁定 Queue 2 end subgraph ThreadPool消费线程池 Lock1 --> T1线程 1\
串行处理 Queue 0 的消息 Lock2 --> T2线程 2\
串行处理 Queue 1 的消息 Lock3 --> T3线程 3\
串行处理 Queue 2 的消息 end NotePool"每个 Queue 在同一时刻只被一个线程处理\
不同 Queue 之间并行,互不影响"
核心机制:
- 队列锁 :Consumer 在拉取消息时,会对 Queue 加锁(
rebalanceLock),确保同一个 Queue 在同一时刻只被一个线程处理 - 串行消费 :一个 Queue 中的消息按拉取顺序,逐个调用
MessageListenerOrderly处理 - 全局锁:如果某个 Consumer 宕机,Rebalance 会将该 Queue 分配给其他实例,新实例会尝试获取队列锁,确保不会有两个实例同时消费同一个 Queue
java
// 顺序消费监听器示例
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
// 消息会按顺序被调用,每次一批(通常是一条)
for (MessageExt msg : msgs) {
processOrder(msg); // 业务处理
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
顺序消息的 Rebalance 处理
顺序消息的 Rebalance 比并发消息更复杂,因为涉及到队列锁的交接:
sequenceDiagram participant C1 as Consumer A participant C2 as Consumer B participant B as Broker Note over C1,B: 正常状态:C1 消费 Queue 0,1;C2 消费 Queue 2,3 C2--xB: Consumer B 宕机 B->>C1: 触发 Rebalance(检测到 C2 下线) C1->>C1: 重新分配 Queue C1->>B: 尝试锁定 Queue 2,3 B-->>C1: 锁定成功(Queue 2,3 无锁) Note over C1: C1 开始消费 Queue 2,3<br>但在此之前,C1 已消费完 Queue 0,1 的当前批次 Note over C1: ✅ 整个过程保证了<br>每个 Queue 同一时刻只被一个 Consumer 消费
Rebalance 中的顺序保障:
- 先停止后分配:在 Rebalance 过程中,Consumer 会先释放自己持有的队列锁,再重新分配
- 锁定机制:新分配的 Queue 需要成功锁定后才能开始消费,避免两个实例同时消费同一个 Queue
- 消息重复风险:由于 Offset 提交是异步的,Rebalance 时仍可能重复消费,业务层依然需要幂等
顺序消息的异常与重试机制
顺序消息的重试和并发消息有很大不同:
异常处理:
| 情况 | 处理方式 |
|---|---|
| 消费失败(业务异常) | 返回 SUSPEND_CURRENT_QUEUE_A_MOMENT,暂停当前 Queue 的消费,等待重试 |
| 重试次数 ≤ 最大重试次数 | 消息被重新投递,再次消费 |
| 重试次数 > 最大重试次数 | 投递到死信队列(DLQ),不再消费 |
flowchart TB Start消费消息 --> Check{消费成功?} Check -->|是| Done✅ 返回 SUCCESS\
继续下一条 Check -->|否| Pause返回 SUSPEND_CURRENT_QUEUE_A_MOMENT Pause --> Sleep暂停该 Queue 消费\
进入等待(默认 1 秒) Sleep --> Retry重新投递该消息 Retry --> Count{重试次数<br>≤ 最大重试次数?} Count -->|是| Start Count -->|否| DLQ投递到死信队列 DLQ --> Skip跳过该消息,继续后续消息 style Done fill:#c8e6c9 style DLQ fill:#ffcdd2
顺序消息重试的关键特点:
- 阻塞后续消息 :一条消息消费失败,该 Queue 后面的消息都会被阻塞,直到这条消息被成功消费或进入 DLQ
- 影响吞吐量:因为阻塞机制,顺序消息的吞吐量对单条消息的处理时间非常敏感
- 最大重试次数 :默认 10 次(并发消费默认 16 次),可通过
consumer.setMaxReconsumeTimes()调整
顺序消息与并发消息的性能对比
| 对比维度 | 并发消息 | 顺序消息 |
|---|---|---|
| 线程模型 | 多线程并行 | 每个 Queue 单线程 |
| 吞吐量 | 高(充分利用 CPU) | 中(受限于单线程串行) |
| 延迟 | 低 | 中高(单条失败会阻塞后续) |
| 适用场景 | 大多数业务 | 需要严格顺序的场景 |
| 推荐 Queue 数 | Consumer 数量的 2-3 倍 | 尽量多(提高并行度) |
flowchart LR subgraph Concurrent并发消息吞吐量 C1Queue 0 --> T1线程池 --> High"🚀 高吞吐" C2Queue 1 --> T1 C3Queue 2 --> T1 C4Queue 3 --> T1 end subgraph Orderly顺序消息吞吐量 O1Queue 0 --> S1单线程 --> Mid"🐢 中吞吐" O2Queue 1 --> S2单线程 O3Queue 2 --> S3单线程 Note"单条消息处理慢了,整个 Queue 被拖慢" end style Concurrent fill:#c8e6c9 style Orderly fill:#fff3e0
性能差距量化:
- 在相同硬件条件下,并发消息的 TPS 通常比顺序消息高 3-5 倍
- 顺序消息中,单条消息的处理时间直接影响整个 Queue 的吞吐量
顺序消息的常见踩坑与规避
| 踩坑 | 后果 | 规避措施 |
|---|---|---|
| 使用默认的轮询队列选择器 | 相同业务 Key 的消息落到不同 Queue,顺序被打乱 | 必须使用 MessageQueueSelector,按 Key 哈希选 Queue |
| Consumer 数量超过 Queue 数量 | 部分 Consumer 空闲,浪费资源 | Consumer 数 ≤ Queue 数,建议 Consumer 数 = Queue 数 |
| 单条消息处理时间过长 | 整个 Queue 被阻塞,吞吐量骤降 | 优化业务逻辑,尽量缩短单条消息的处理时间;如果必须长耗时,考虑拆分或异步化 |
| 消费异常返回 RECONSUME_LATER | 阻塞后续消息,影响效率 | 谨慎使用重试,尽可能在业务层解决异常,避免依赖重试 |
| Rebalance 频繁触发 | 顺序可能被短暂打断,消息重复 | 稳定 Consumer 实例 ,避免频繁上下线;使用 AllocateMessageQueueAveragely 策略 |
| 将顺序消息和并发消息混在同一 Group | Rebalance 混乱,顺序无法保证 | 分离 Group,顺序消息使用独立的 Group |
flowchart TB subgraph Pitfalls常见踩坑 P1"❌ 队列选择器用错\
→ 顺序乱" P2"❌ Consumer \> Queue\
→ 资源浪费" P3"❌ 单条消息耗时过长\
→ 整体阻塞" P4"❌ 频繁 Rebalance\
→ 顺序中断" end subgraph Fixes解决方案 F1"✅ 用 MessageQueueSelector\
按 Key 哈希" F2"✅ Consumer 数 ≤ Queue 数" F3"✅ 优化业务逻辑\
异步化长任务" F4"✅ 稳定实例\
合理配置 Rebalance 参数" end P1 --> F1 P2 --> F2 P3 --> F3 P4 --> F4
十、延迟消息与定时消息
延迟消息的概念与业务场景
延迟消息 是生产者发送消息时指定一个延迟时间,消息在到达该时间后才被 Consumer 消费。它和普通消息的区别在于消息不是立即投递,而是"预约"投递。
典型业务场景:
| 场景 | 延迟时间 | 说明 |
|---|---|---|
| 订单超时自动取消 | 30 分钟 | 下单后 30 分钟未支付,自动取消订单 |
| 定时提醒 | 1 天 | 预约明天上午 10 点的提醒 |
| 重试调度 | 1s, 5s, 10s... | 消费失败后延迟重试(RocketMQ 内部已实现) |
| 定时任务调度 | 固定时间 | 每天凌晨 2 点执行数据统计 |
RocketMQ 延迟消息的等级机制(18 个等级)
在 RocketMQ 4.x 及 5.x 早期版本中,延迟消息基于预定义等级实现,不支持任意时间延迟:
| 等级 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| 延迟 | 1s | 5s | 10s | 30s | 1m | 2m | 3m | 4m | 5m |
| 等级 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
|---|---|---|---|---|---|---|---|---|---|
| 延迟 | 6m | 7m | 8m | 9m | 10m | 20m | 30m | 1h | 2h |
发送延迟消息:
java
Message msg = new Message("order_topic", "订单内容".getBytes());
// 设置延迟等级为 3,表示延迟 10 秒
msg.setDelayTimeLevel(3);
producer.send(msg);
为什么使用等级机制?
- 内部通过
SCHEDULE_TOPIC_XXXX的 18 个 Queue 来分别存储不同延迟等级的消息 - 后台定时任务
ScheduleMessageService扫描这些 Queue,到时间后重新投递 - 等级机制实现简单,性能好,但不够灵活
延迟消息的存储与调度原理
flowchart TB subgraph Send发送阶段 PProducer -->|设置 delayTimeLevel=3| BBroker B -->|写入| CLCommitLog end subgraph Index索引构建 CL -->|ReputMessageService| CQConsumeQueue CQ -->|根据延迟等级| SQueueSCHEDULE_TOPIC_XXXX\
Queue 3 end subgraph Schedule调度阶段 SQueue -->|定时扫描| TimerScheduleMessageService\
每个等级一个定时线程 Timer -->|到达投递时间| RePush重新写入目标 Topic end subgraph Consume消费阶段 RePush --> TargetCQ目标 Topic 的 ConsumeQueue TargetCQ --> ConsumerConsumer 消费 end style SQueue fill:#fff3e0 style Timer fill:#ffcdd2 style TargetCQ fill:#c8e6c9
核心调度逻辑:
ScheduleMessageService为每个延迟等级启动一个定时线程- 定时线程每隔固定时间(默认 1 秒)扫描对应的 Queue
- 检查 ConsumeQueue 中的消息,如果当前时间 ≥ 投递时间,则:
- 从 CommitLog 中读取完整消息
- 清除延迟属性,重新写入目标 Topic 的 CommitLog 和 ConsumeQueue
- 这样 Consumer 就能正常消费到这条消息
延迟消息的消息重投与消费
延迟消息的消费重试和普通消息完全一样(基于 %RETRY% 重试队列)。但有一个特殊点:重试不会重置延迟。即消息第一次被消费失败后,进入重试队列的重试延迟是独立的(由重试队列的延迟等级决定),和消息本身的延迟等级无关。
RocketMQ 5.x 的定时消息新特性(基于时间轮的精准定时)
RocketMQ 5.x 引入了精准定时消息,突破了 18 个等级的限制,支持任意时间点的定时投递:
java
// 5.x 新特性:精准定时消息
Message msg = new Message("order_topic", "内容".getBytes());
// 指定精确的投递时间戳(毫秒)
msg.setDeliverTimeMs(System.currentTimeMillis() + 30 * 60 * 1000); // 30 分钟后
producer.send(msg);
底层实现变化:
| 4.x | 5.x |
|---|---|
| 18 个固定等级 | 支持任意时间点 |
| 每个等级一个 Queue | 基于 Timing Wheel(时间轮) 算法 |
| 定时线程扫描 | 时间轮 + 多级时间轮,高效调度 |
| 适合固定延迟场景 | 适合任意定时场景 |
flowchart LR subgraph TimerWheel时间轮定时消息 T1消息投递时间\
任意时间戳 --> T2插入时间轮槽位 T2 --> T3时间轮每秒推进 T3 -->|槽位到期| T4触发投递 NoteWheel"高效调度 O(1) 复杂度\
支持海量定时消息" end style TimerWheel fill:#e3f2fd
时间轮的优势:
- 可以处理任意时间点的定时消息,不再受 18 个等级限制
- 时间轮算法在调度大量定时任务时性能远优于传统的扫描方式
- 支持取消定时消息(通过消息 ID)
延迟消息的使用限制与最佳实践
使用限制:
| 限制 | 说明 |
|---|---|
| ❌ 不支持事务消息 | 延迟消息不能和事务消息组合使用 |
| ❌ 不支持批量消息 | 批量消息不能设置延迟等级 |
| ❌ 4.x 只支持 18 个等级 | 不能自定义延迟时间(5.x 解决了) |
| ❌ 消息投递后不可取消 | 一旦 Broker 调度投递,无法撤回(5.x 支持取消部分) |
最佳实践:
- 评估延迟粒度:如果只需要秒/分钟级别的延迟,4.x 的 18 个等级通常够用
- 5.x 新项目推荐使用定时消息:更灵活,支持取消
- 避免过长的延迟(> 2 小时):长时间延迟消息占用 Broker 存储,建议采用外部调度方案
- 监控延迟消息堆积 :延迟消息在
SCHEDULE_TOPIC_XXXX中堆积,需要监控其数量,避免积压过多导致 Broker 内存压力
十一、消息轨迹与消息查询
消息轨迹的作用与配置开启
消息轨迹(Message Trace) 记录了消息从生产到消费的完整链路,是问题排查的利器。
核心作用:
flowchart LR subgraph Trace消息轨迹能回答的问题 Q1"❓ 这条消息是谁发的?" Q2"❓ 什么时候发的?" Q3"❓ 发到了哪个 Broker?" Q4"❓ 被消费了吗?" Q5"❓ 谁消费的?" Q6"❓ 消费成功还是失败?" end style Trace fill:#e3f2fd
配置开启:
Broker 端配置 (broker.conf):
properties
# 开启消息轨迹
traceTopicEnable=true
# 轨迹数据存储的 Topic 名称(默认)
msgTraceTopicName=RMQ_SYS_TRACE_TOPIC
Producer 端开启:
java
DefaultMQProducer producer = new DefaultMQProducer("producer_group");
producer.setEnableMsgTrace(true); // 开启轨迹
producer.setMsgTraceTopic("RMQ_SYS_TRACE_TOPIC");
Consumer 端开启:
java
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
consumer.setEnableMsgTrace(true);
consumer.setMsgTraceTopic("RMQ_SYS_TRACE_TOPIC");
消息轨迹的数据采集与存储(traceTopic)
消息轨迹通过 Hook 机制 无侵入地采集数据:
sequenceDiagram participant P as Producer participant H as Trace Hook participant B as Broker participant Trace as RMQ_SYS_TRACE_TOPIC P->>H: 1. 发送消息前触发 before H->>H: 2. 记录发送开始时间、IP 等 P->>B: 3. 发送消息 B-->>P: 4. 返回响应 P->>H: 5. 发送后触发 after H->>H: 6. 记录耗时、结果 H->>Trace: 7. 将轨迹数据作为普通消息<br>异步发送到 traceTopic
轨迹数据内容:
| 字段 | 说明 |
|---|---|
traceId |
全局唯一追踪 ID |
messageId |
消息 ID |
key |
业务 Key |
topic |
Topic 名称 |
producerGroup |
生产者组 |
consumerGroup |
消费者组 |
sendTime |
发送时间 |
consumeTime |
消费时间 |
sendCost |
发送耗时 |
consumeCost |
消费耗时 |
clientIp |
客户端 IP |
status |
状态(成功/失败) |
errorMsg |
错误信息 |
消息轨迹的查询与可视化
RocketMQ 提供了 Dashboard 可视化工具来查询消息轨迹:
flowchart LR subgraph Query查询方式 Q1"按 MessageId 查询" Q2"按 Key 查询" Q3"按 Topic + 时间范围查询" end subgraph Display展示内容 D1生产阶段\
发送时间、IP、耗时、状态 D2存储阶段\
Broker、CommitLog 位置 D3消费阶段\
消费组、消费时间、结果 end Query --> DashboardRocketMQ Dashboard Dashboard --> Display style Dashboard fill:#e3f2fd
在 Dashboard 中查询轨迹:
- 登录 RocketMQ Dashboard
- 进入"消息轨迹"页面
- 输入 MessageId 或 Key,点击查询
- 展示完整的生产 → 存储 → 消费链路
API 查询示例:
java
// 通过 MessageId 查询轨迹
QueryResult queryResult = admin.queryMessageByMessageId("topic", "messageId");
消息查询的三种方式:按 MessageId、按 Key、按时间
| 查询方式 | 原理 | 适用场景 |
|---|---|---|
| 按 MessageId | 直接通过 IndexFile 定位 | 已知消息 ID,查询单条消息详情 |
| 按 Key | 通过 IndexFile 哈希索引查询 | 根据业务 Key(如订单 ID)查询一批消息 |
| 按时间 | 遍历 ConsumeQueue 中的时间戳 | 查询某段时间内的所有消息,适合批量分析 |
消息轨迹的日志解读与问题定位
常见问题通过轨迹定位:
flowchart TB Start问题现象 --> Check{通过轨迹查看} Check -->|消息未收到| A1查看生产轨迹\
消息是否成功发送到 Broker? A1 --> A2{发送成功?} A2 -->|否| A3Producer 发送失败,检查网络/重试 A2 -->|是| A4查看消费轨迹\
是否有 Consumer 订阅? A4 --> A5{有消费记录?} A5 -->|否| A6检查消费组是否在线/订阅是否正确 A5 -->|是| A7查看消费状态\
是否消费失败? Check -->|消费失败| B1查看消费轨迹中的\
错误信息和耗时 B1 --> B2定位业务逻辑或下游依赖问题 style A3 fill:#ffcdd2 style A6 fill:#ffcdd2 style A7 fill:#ffcdd2 style B2 fill:#ffcdd2
十二、消息过滤
Tag 标签过滤的原理与使用
Tag 是 Topic 下的二级分类,用于在 Broker 侧对消息进行轻量级过滤。
发送时打 Tag:
java
Message msg1 = new Message("order_topic", "order_create", "内容".getBytes());
Message msg2 = new Message("order_topic", "order_pay", "内容".getBytes());
消费时按 Tag 过滤:
java
// 只消费 order_create 和 order_pay 两种 Tag
consumer.subscribe("order_topic", "order_create || order_pay");
Tag 过滤的原理:
flowchart TB subgraph TagFilterTag 过滤流程 A消息携带 Tag --> BBroker 构建 ConsumeQueue 时\
将 Tag 的哈希值存入索引 B --> CConsumer 订阅时\
指定 Tag 表达式 C --> DBroker 拉取消息时\
根据哈希码快速匹配 D -->|匹配| E✅ 投递给 Consumer D -->|不匹配| F❌ 丢弃,不网络传输 end style E fill:#c8e6c9 style F fill:#ffcdd2
Tag 过滤的极致性能:
- ConsumeQueue 中存储了 Tag 的 哈希码(8 字节),而非完整 Tag
- 过滤时只需对比哈希码,无需读取消息体
- 完全在 Broker 的内存中完成,几乎无性能损耗
SQL92 表达式过滤的原理与使用
当过滤逻辑复杂时(如基于消息属性做条件判断),需要使用 SQL92 过滤。
发送时携带属性:
java
Message msg = new Message("order_topic", "内容".getBytes());
msg.putUserProperty("amount", "1500");
msg.putUserProperty("region", "SH");
msg.putUserProperty("vip", "true");
消费时使用 SQL92 表达式:
java
// 只消费上海地区、金额大于 1000、VIP 用户的消息
consumer.subscribe("order_topic",
MessageSelector.bySql("region = 'SH' AND amount > 1000 AND vip = true"));
SQL92 过滤的原理:
flowchart TB subgraph SQLFilterSQL92 过滤流程 A消息携带用户属性 --> BBroker 收到拉取请求\
解析 SQL 表达式 B --> C读取消息的所有用户属性 C --> D在 Broker 中执行 SQL 计算 D -->|TRUE| E✅ 投递给 Consumer D -->|FALSE| F❌ 丢弃 end NoteSQL"⚠️ 需要读取完整消息属性\
消耗更多 CPU 和内存\
不适合超高吞吐场景" style E fill:#c8e6c9 style F fill:#ffcdd2
两种过滤方式的性能对比
| 对比维度 | Tag 过滤 | SQL92 过滤 |
|---|---|---|
| 过滤依据 | Tag 字符串 | 用户自定义属性 |
| Broker 开销 | 极低(仅哈希比对) | 较高(解析 SQL + 读取属性) |
| 网络传输 | 不匹配的消息不传输 | 不匹配的消息不传输 |
| 灵活性 | 低(只能精确匹配 Tag) | 高(复杂条件组合) |
| 支持操作符 | ` | |
| 性能损耗 | ~1%-2% | ~10%-20% |
| 适用场景 | 简单的消息分类 | 复杂的业务规则筛选 |
flowchart LR subgraph Performance性能对比示意 TagTag 过滤\
⚡ 极快 -->|哈希比对| Mem内存操作 SQLSQL92 过滤\
🐢 较慢 -->|解析+计算| CPUCPU 密集型 end style Tag fill:#c8e6c9 style SQL fill:#fff3e0
消息过滤 Broker 端与 Consumer 端的职责分工
RocketMQ 的过滤是 Broker 端过滤,即过滤在 Broker 完成,不匹配的消息不会传输给 Consumer,减少了网络流量。
flowchart LR subgraph BrokerBroker 端职责 B1接收 Consumer 订阅请求\
存储过滤表达式 B2从 ConsumeQueue 读取消息\
根据 Tag 或 SQL 过滤 B3仅将匹配的消息\
返回给 Consumer end subgraph ConsumerConsumer 端职责 C1订阅时指定\
Tag 或 SQL 表达式 C2接收 Broker 返回的\
已过滤后的消息 C3业务处理\
不再二次过滤 end B1 --> B2 --> B3 C1 --> Broker Broker --> C2 --> C3
消息过滤的实践场景与误区
实践场景:
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 同一业务下不同事件(订单创建/支付/退款) | Tag 过滤 | 分类明确,性能最优 |
| 同一 Topic 下不同租户的数据 | Tag 过滤(租户 ID 作为 Tag) | 简单高效 |
| 基于金额/地区/用户等级的条件筛选 | SQL92 过滤 | 需要复杂条件 |
| 动态变化的过滤条件 | SQL92 过滤 | 灵活调整表达式 |
常见误区:
flowchart TB subgraph Misconceptions常见误区 M1"❌ 为每个事件单独创建 Topic\
(Topic 泛滥)" M2"❌ 所有业务都用 SQL92 过滤\
(浪费性能)" M3"❌ Consumer 端再做一次过滤\
(重复劳动,浪费网络)" M4"❌ Tag 中使用复杂字符串\
(实际上只存哈希码)" end subgraph Correct正确做法 C1"✅ 同一业务共用 Topic\
用 Tag 区分事件" C2"✅ 简单分类用 Tag\
复杂条件才用 SQL92" C3"✅ 过滤在 Broker 完成\
Consumer 直接处理" C4"✅ Tag 简洁有意义" end M1 --> C1 M2 --> C2 M3 --> C3 M4 --> C4
小结
这篇文章涵盖了 RocketMQ 进阶应用的四大模块,通过 10+ 张流程图,我们搞清楚了:
- 顺序消息:分区顺序的实现原理、队列选择器、顺序消费的线程模型、Rebalance 处理、异常重试以及和并发消息的性能对比
- 延迟消息与定时消息:4.x 的 18 个延迟等级机制、存储调度原理、5.x 基于时间轮的精准定时消息
- 消息轨迹:作用、配置开启、轨迹数据的采集存储、查询方式以及通过轨迹定位问题的思路
- 消息过滤:Tag 过滤和 SQL92 过滤的原理、性能对比、Broker 与 Consumer 的职责分工以及实践场景
到此,你已经掌握了 RocketMQ 从基础到进阶的全部核心知识。从入门认知到架构原理,从存储机制到发送消费,从事务消息到今天的进阶特性,你已经成为了一名能够独立应对各种复杂场景的 RocketMQ 开发者。
恭喜你完成这个系列的学习!希望你能在实际工作中,把今天学到的知识灵活运用,真正让 RocketMQ 成为你手中的利器。
系列文章:
- 入门认知篇 ✅
- 核心概念与架构篇 ✅
- 存储与原理篇(上)✅
- 存储与原理篇(中)✅
- 存储与原理篇(下)✅
- 事务消息 ✅
- 进阶应用篇 ✅(本文)
- 部署与运维篇 (待续...)