进阶应用篇:顺序消息、延迟消息、轨迹与过滤
系列第四阶段:进阶应用篇
你好,我们又见面了。
经过前面三阶段的学习,你已经掌握了 RocketMQ 的核心架构、存储原理、发送消费机制以及事务消息。可以说,你现在已经是一名合格的 RocketMQ 开发者了。
但“合格”和“精通”之间,还差一个进阶的距离。在实际业务中,你会遇到各种各样的特殊需求:
- 订单状态流转必须严格按顺序执行,不能乱序
- 需要定时或延迟发送消息,比如 30 分钟后自动取消未支付订单
- 消息出了问题,需要快速定位,知道它经历了什么
- 不同的消费者只需要消费特定类型的消息,减少无效传输
今天这篇文章,我们就来集中攻克这四大进阶技能:顺序消息、延迟消息、消息轨迹、消息过滤。每一个都是生产环境中的高频需求。老规矩,配合流程图,一步一图。
九、顺序消息
顺序消息的概念与业务场景
什么是顺序消息?
顺序消息是指消息的发送顺序和消费顺序保持一致。比如你发了消息 A、B、C,那么 Consumer 收到和处理消息的顺序也一定是 A、B、C。
典型业务场景:
| 场景 | 为什么需要顺序 |
|---|---|
| 订单状态流转 | 创建 → 支付 → 发货 → 完成,必须按顺序执行,绝不能先完成再创建 |
| 库存扣减日志 | 同一商品的入库和出库操作,必须按时间顺序处理,否则库存数据会错乱 |
| 数据库 binlog 同步 | 数据库变更日志必须按顺序回放,否则数据不一致 |
| IoT 设备指令 | 对同一设备的控制指令必须有序执行(如开 → 关 → 开) |
全局顺序消息与分区顺序消息
RocketMQ 的顺序消息分为两个级别:
全局顺序消息
- 一个 Topic 下只有一个 MessageQueue
- 所有消息严格按顺序发送和消费
- 代价:吞吐量极低(单队列串行,无法并行)
分区顺序消息
- Topic 下有多个 Queue,但同一个业务 Key 的消息始终落到同一个 Queue
- 不同 Key 的消息可以在不同 Queue 中并行,互不影响
- 这是 RocketMQ 的默认推荐方式,既保证了顺序,又保持了并行度
顺序消息的发送原理(队列选择器 MessageQueueSelector)
顺序消息的发送关键在生产者端——必须保证相同业务 Key 的消息被分配到同一个 Queue。
RocketMQ 提供了 MessageQueueSelector 接口,让你自定义选择逻辑:
// 顺序消息发送示例
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);
关键点:
- 如果某个 Broker 宕机,该 Broker 上的 Queue 会从可用列表中移除,此时哈希取模的结果会变化,可能导致顺序被打破。RocketMQ 的故障规避机制在这种情况下会特殊处理,尽量保持顺序,但极端情况下仍可能乱序。所以顺序消息的 Broker 高可用很重要。
顺序消息的消费原理(顺序消费线程模型)
顺序消息在消费端通过 MessageListenerOrderly 实现,其核心是每个 Queue 由单线程串行消费:
核心机制:
- 队列锁:Consumer 在拉取消息时,会对 Queue 加锁(
rebalanceLock),确保同一个 Queue 在同一时刻只被一个线程处理 - 串行消费:一个 Queue 中的消息按拉取顺序,逐个调用
MessageListenerOrderly处理 - 全局锁:如果某个 Consumer 宕机,Rebalance 会将该 Queue 分配给其他实例,新实例会尝试获取队列锁,确保不会有两个实例同时消费同一个 Queue
// 顺序消费监听器示例
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
// 消息会按顺序被调用,每次一批(通常是一条)
for (MessageExt msg : msgs) {
processOrder(msg); // 业务处理
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
顺序消息的 Rebalance 处理
顺序消息的 Rebalance 比并发消息更复杂,因为涉及到队列锁的交接:
Rebalance 中的顺序保障:
- 先停止后分配:在 Rebalance 过程中,Consumer 会先释放自己持有的队列锁,再重新分配
- 锁定机制:新分配的 Queue 需要成功锁定后才能开始消费,避免两个实例同时消费同一个 Queue
- 消息重复风险:由于 Offset 提交是异步的,Rebalance 时仍可能重复消费,业务层依然需要幂等
顺序消息的异常与重试机制
顺序消息的重试和并发消息有很大不同:
异常处理:
| 情况 | 处理方式 |
|---|---|
| 消费失败(业务异常) | 返回 SUSPEND_CURRENT_QUEUE_A_MOMENT,暂停当前 Queue 的消费,等待重试 |
| 重试次数 ≤ 最大重试次数 | 消息被重新投递,再次消费 |
| 重试次数 > 最大重试次数 | 投递到死信队列(DLQ),不再消费 |
顺序消息重试的关键特点:
- 阻塞后续消息:一条消息消费失败,该 Queue 后面的消息都会被阻塞,直到这条消息被成功消费或进入 DLQ
- 影响吞吐量:因为阻塞机制,顺序消息的吞吐量对单条消息的处理时间非常敏感
- 最大重试次数:默认 10 次(并发消费默认 16 次),可通过
consumer.setMaxReconsumeTimes()调整
顺序消息与并发消息的性能对比
| 对比维度 | 并发消息 | 顺序消息 |
|---|---|---|
| 线程模型 | 多线程并行 | 每个 Queue 单线程 |
| 吞吐量 | 高(充分利用 CPU) | 中(受限于单线程串行) |
| 延迟 | 低 | 中高(单条失败会阻塞后续) |
| 适用场景 | 大多数业务 | 需要严格顺序的场景 |
| 推荐 Queue 数 | Consumer 数量的 2-3 倍 | 尽量多(提高并行度) |
性能差距量化:
- 在相同硬件条件下,并发消息的 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 |
十、延迟消息与定时消息
延迟消息的概念与业务场景
延迟消息是生产者发送消息时指定一个延迟时间,消息在到达该时间后才被 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 |
发送延迟消息:
Message msg = new Message("order_topic", "订单内容".getBytes());
// 设置延迟等级为 3,表示延迟 10 秒
msg.setDelayTimeLevel(3);
producer.send(msg);
为什么使用等级机制?
- 内部通过
SCHEDULE_TOPIC_XXXX的 18 个 Queue 来分别存储不同延迟等级的消息 - 后台定时任务
ScheduleMessageService扫描这些 Queue,到时间后重新投递 - 等级机制实现简单,性能好,但不够灵活
延迟消息的存储与调度原理
核心调度逻辑:
ScheduleMessageService为每个延迟等级启动一个定时线程- 定时线程每隔固定时间(默认 1 秒)扫描对应的 Queue
- 检查 ConsumeQueue 中的消息,如果当前时间 ≥ 投递时间,则:
- 从 CommitLog 中读取完整消息
- 清除延迟属性,重新写入目标 Topic 的 CommitLog 和 ConsumeQueue
- 这样 Consumer 就能正常消费到这条消息
延迟消息的消息重投与消费
延迟消息的消费重试和普通消息完全一样(基于 %RETRY% 重试队列)。但有一个特殊点:重试不会重置延迟。即消息第一次被消费失败后,进入重试队列的重试延迟是独立的(由重试队列的延迟等级决定),和消息本身的延迟等级无关。
RocketMQ 5.x 的定时消息新特性(基于时间轮的精准定时)
RocketMQ 5.x 引入了精准定时消息,突破了 18 个等级的限制,支持任意时间点的定时投递:
// 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(时间轮) 算法 |
| 定时线程扫描 | 时间轮 + 多级时间轮,高效调度 |
| 适合固定延迟场景 | 适合任意定时场景 |
时间轮的优势:
- 可以处理任意时间点的定时消息,不再受 18 个等级限制
- 时间轮算法在调度大量定时任务时性能远优于传统的扫描方式
- 支持取消定时消息(通过消息 ID)
延迟消息的使用限制与最佳实践
使用限制:
| 限制 | 说明 |
|---|---|
| ❌ 不支持事务消息 | 延迟消息不能和事务消息组合使用 |
| ❌ 不支持批量消息 | 批量消息不能设置延迟等级 |
| ❌ 4.x 只支持 18 个等级 | 不能自定义延迟时间(5.x 解决了) |
| ❌ 消息投递后不可取消 | 一旦 Broker 调度投递,无法撤回(5.x 支持取消部分) |
最佳实践:
- 评估延迟粒度:如果只需要秒/分钟级别的延迟,4.x 的 18 个等级通常够用
- 5.x 新项目推荐使用定时消息:更灵活,支持取消
- 避免过长的延迟(> 2 小时):长时间延迟消息占用 Broker 存储,建议采用外部调度方案
- 监控延迟消息堆积:延迟消息在
SCHEDULE_TOPIC_XXXX中堆积,需要监控其数量,避免积压过多导致 Broker 内存压力
十一、消息轨迹与消息查询
消息轨迹的作用与配置开启
消息轨迹(Message Trace) 记录了消息从生产到消费的完整链路,是问题排查的利器。
核心作用:
配置开启:
Broker 端配置(broker.conf):
# 开启消息轨迹
traceTopicEnable=true
# 轨迹数据存储的 Topic 名称(默认)
msgTraceTopicName=RMQ_SYS_TRACE_TOPIC
Producer 端开启:
DefaultMQProducer producer = new DefaultMQProducer("producer_group");
producer.setEnableMsgTrace(true); // 开启轨迹
producer.setMsgTraceTopic("RMQ_SYS_TRACE_TOPIC");
Consumer 端开启:
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
consumer.setEnableMsgTrace(true);
consumer.setMsgTraceTopic("RMQ_SYS_TRACE_TOPIC");
消息轨迹的数据采集与存储(traceTopic)
消息轨迹通过 Hook 机制 无侵入地采集数据:
轨迹数据内容:
| 字段 | 说明 |
|---|---|
traceId |
全局唯一追踪 ID |
messageId |
消息 ID |
key |
业务 Key |
topic |
Topic 名称 |
producerGroup |
生产者组 |
consumerGroup |
消费者组 |
sendTime |
发送时间 |
consumeTime |
消费时间 |
sendCost |
发送耗时 |
consumeCost |
消费耗时 |
clientIp |
客户端 IP |
status |
状态(成功/失败) |
errorMsg |
错误信息 |
消息轨迹的查询与可视化
RocketMQ 提供了 Dashboard 可视化工具来查询消息轨迹:
在 Dashboard 中查询轨迹:
- 登录 RocketMQ Dashboard
- 进入“消息轨迹”页面
- 输入 MessageId 或 Key,点击查询
- 展示完整的生产 → 存储 → 消费链路
API 查询示例:
// 通过 MessageId 查询轨迹
QueryResult queryResult = admin.queryMessageByMessageId("topic", "messageId");
消息查询的三种方式:按 MessageId、按 Key、按时间
| 查询方式 | 原理 | 适用场景 |
|---|---|---|
| 按 MessageId | 直接通过 IndexFile 定位 | 已知消息 ID,查询单条消息详情 |
| 按 Key | 通过 IndexFile 哈希索引查询 | 根据业务 Key(如订单 ID)查询一批消息 |
| 按时间 | 遍历 ConsumeQueue 中的时间戳 | 查询某段时间内的所有消息,适合批量分析 |
消息轨迹的日志解读与问题定位
常见问题通过轨迹定位:
十二、消息过滤
Tag 标签过滤的原理与使用
Tag 是 Topic 下的二级分类,用于在 Broker 侧对消息进行轻量级过滤。
发送时打 Tag:
Message msg1 = new Message("order_topic", "order_create", "内容".getBytes());
Message msg2 = new Message("order_topic", "order_pay", "内容".getBytes());
消费时按 Tag 过滤:
// 只消费 order_create 和 order_pay 两种 Tag
consumer.subscribe("order_topic", "order_create || order_pay");
Tag 过滤的原理:
Tag 过滤的极致性能:
- ConsumeQueue 中存储了 Tag 的 哈希码(8 字节),而非完整 Tag
- 过滤时只需对比哈希码,无需读取消息体
- 完全在 Broker 的内存中完成,几乎无性能损耗
SQL92 表达式过滤的原理与使用
当过滤逻辑复杂时(如基于消息属性做条件判断),需要使用 SQL92 过滤。
发送时携带属性:
Message msg = new Message("order_topic", "内容".getBytes());
msg.putUserProperty("amount", "1500");
msg.putUserProperty("region", "SH");
msg.putUserProperty("vip", "true");
消费时使用 SQL92 表达式:
// 只消费上海地区、金额大于 1000、VIP 用户的消息
consumer.subscribe("order_topic",
MessageSelector.bySql("region = 'SH' AND amount > 1000 AND vip = true"));
SQL92 过滤的原理:
两种过滤方式的性能对比
| 对比维度 | Tag 过滤 | SQL92 过滤 |
|---|---|---|
| 过滤依据 | Tag 字符串 | 用户自定义属性 |
| Broker 开销 | 极低(仅哈希比对) | 较高(解析 SQL + 读取属性) |
| 网络传输 | 不匹配的消息不传输 | 不匹配的消息不传输 |
| 灵活性 | 低(只能精确匹配 Tag) | 高(复杂条件组合) |
| 支持操作符 | ||(或) |
> < = LIKE IN AND OR 等 |
| 性能损耗 | ~1%-2% | ~10%-20% |
| 适用场景 | 简单的消息分类 | 复杂的业务规则筛选 |
消息过滤 Broker 端与 Consumer 端的职责分工
RocketMQ 的过滤是 Broker 端过滤,即过滤在 Broker 完成,不匹配的消息不会传输给 Consumer,减少了网络流量。
消息过滤的实践场景与误区
实践场景:
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 同一业务下不同事件(订单创建/支付/退款) | Tag 过滤 | 分类明确,性能最优 |
| 同一 Topic 下不同租户的数据 | Tag 过滤(租户 ID 作为 Tag) | 简单高效 |
| 基于金额/地区/用户等级的条件筛选 | SQL92 过滤 | 需要复杂条件 |
| 动态变化的过滤条件 | SQL92 过滤 | 灵活调整表达式 |
常见误区:
小结
这篇文章涵盖了 RocketMQ 进阶应用的四大模块,通过 10+ 张流程图,我们搞清楚了:
- 顺序消息:分区顺序的实现原理、队列选择器、顺序消费的线程模型、Rebalance 处理、异常重试以及和并发消息的性能对比
- 延迟消息与定时消息:4.x 的 18 个延迟等级机制、存储调度原理、5.x 基于时间轮的精准定时消息
- 消息轨迹:作用、配置开启、轨迹数据的采集存储、查询方式以及通过轨迹定位问题的思路
- 消息过滤:Tag 过滤和 SQL92 过滤的原理、性能对比、Broker 与 Consumer 的职责分工以及实践场景
到此,你已经掌握了 RocketMQ 从基础到进阶的全部核心知识。从入门认知到架构原理,从存储机制到发送消费,从事务消息到今天的进阶特性,你已经成为了一名能够独立应对各种复杂场景的 RocketMQ 开发者。
恭喜你完成这个系列的学习!希望你能在实际工作中,把今天学到的知识灵活运用,真正让 RocketMQ 成为你手中的利器。
系列文章:
- 入门认知篇 ✅
- 核心概念与架构篇 ✅
- 存储与原理篇(上)✅
- 存储与原理篇(中)✅
- 存储与原理篇(下)✅
- 事务消息 ✅
- 进阶应用篇 ✅(本文)
- 部署与运维篇 (待续...)
❤️ 如果你喜欢这篇文章,请点赞支持! 👍 同时欢迎关注我的博客,获取更多精彩内容!
本文来自博客园,作者:佛祖让我来巡山,转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/21303448

浙公网安备 33010602011771号