在 RocketMQ 中,“顺序消息”是指消息的发送顺序与消费顺序一致。根据顺序的范围和粒度不同,分为两种:
1. 全局有序消息(Global Order)
概念:
所有消息按发送顺序依次进入同一个队列(partition),消费者也严格按这个顺序消费。
通俗理解:
就像一条生产线,所有商品只能排队进来,消费者一个一个拿,绝不乱序。
特征:
- 所有消息进入同一个 queue;
- 保证整个 topic 级别的顺序;
- 但吞吐量低(单线程消费);
适用场景:
- 银行转账记录、订单状态流转等严格顺序场景;
- 数量少、顺序要求极高的业务。
在创建Topic时指定Queue的数量。有三种指定方式:
- 在代码中创建Producer时,可以指定其自动创建的Topic的Queue数量
- 在RocketMQ可视化控制台中手动创建Topic时指定Queue数量
- 使用mqadmin命令手动创建Topic时指定Queue数量
2. 分区有序消息(Partition Order / 分区顺序)
概念:
按照某个业务维度(如订单 ID)将消息发送到指定队列,每个队列内部顺序保证,但不同队列之间不保证顺序。
通俗理解:
就像多条生产线,每条线内有序,每条线管一个订单编号,不同编号可能并行处理。
特征:
- 将消息根据某个规则(如
hash(orderId)
)分配到不同 queue; - 同一个 key 的消息总是进入同一个 queue,因此消费时顺序一致;
- 适度牺牲全局顺序,换来并发性能。
适用场景:
- 订单系统、物流跟踪:只需要每个订单内有序;
- 高频写入,顺序性重要但不要求全局。
如何选择队列:
如何实现Queue的选择?在定义Producer时我们可以指定消息队列选择器,而这个选择器是我们自己实现了MessageQueueSelector接口定义的。
在定义选择器的选择算法时,一般需要使用选择key。这个选择key可以是消息key也可以是其它数据。但无论谁做选择key,都不能重复,都是唯一的。
一般性的选择算法是,让选择key(或其hash值)与该Topic所包含的Queue的数量取模,其结果即为选择出的Queue的QueueId。
取模算法存在一个问题:不同选择key与Queue数量取模结果可能会是相同的,即不同选择key的
消息可能会出现在相同的Queue,即同一个Consuemr可能会消费到不同选择key的消息。这个问题如何解决?一般性的作法是,从消息中获取到选择key,对其进行判断。若是当前Consumer需
要消费的消息,则直接消费,否则,什么也不做。这种做法要求选择key要能够随着消息一起被
Consumer获取到。此时使用消息key作为选择key是比较好的做法。
以上做法会不会出现如下新的问题呢?不属于那个Consumer的消息被拉取走了,那么应该消费
该消息的Consumer是否还能再消费该消息呢?同一个Queue中的消息不可能被同一个Group中的
不同Consumer同时消费。所以,消费一个Queue的不同选择key的消息的Consumer一定属于不
同的Group。而不同的Group中的Consumer间的消费是相互隔离的,互不影响的。
存疑:继续理解
public class OrderedProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketmqOS:9876");
producer.start();
for (int i = 0; i < 100; i++) {
Integer orderId = i;
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("TopicA", "TagA", body);
SendResult sendResult = producer.send(msg, new
MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs,
Message msg, Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.println(sendResult);
}
producer.shutdown();
}
}
3. 总结对比
类型 | 顺序粒度 | 队列数 | 并发性 | 吞吐量 | 使用场景 |
---|---|---|---|---|---|
全局有序 | 所有消息 | 1 | 差 | 低 | 极端顺序要求(交易流水) |
分区有序 | 每个分区内有序 | 多 | 好 | 高 | 订单系统、用户行为日志等 |
4. 示例:订单系统
假设有用户订单状态变化:下单 -> 支付 -> 发货 -> 收货
- 全局有序:所有订单的这些状态都排队来,严格一个顺序处理。
- 分区有序:每个订单根据
orderId
落到一个队列,这个订单的状态有序,但不同订单并发处理。
5.延迟消息定义
当消息写入到Broker后,在指定的时长后才可被消费处理的消息,称为延时消息。
从 RocketMQ 的存储核心结构 —— CommitLog、ConsumeQueue、IndexFile 的角度,来通俗解释 延时消息的整个生命周期,会让你更深入理解它是怎么“绕一圈”实现的。
6.从 RocketMQ 三大核心存储来理解延迟消息实现原理
结构 | 作用 | 类比 |
---|---|---|
CommitLog | 所有消息的物理写入日志(顺序写,最底层) | 日志本 |
ConsumeQueue | 按 topic + queue 划分的消息逻辑消费索引 | 目录页 |
IndexFile | 支持 key 查询消息的 hash 索引文件 | 查字典 |
7. 延时消息过程全链路分析(结合这三个结构)
步骤 1:Producer 发出延时消息(如延时 10 秒)
Message msg = new Message("myTopic", "tag", "Hello");
msg.setDelayTimeLevel(3); // 10 秒
producer.send(msg);
此时,RocketMQ 不会把消息写到 myTopic 的 CommitLog 区域中,而是:
写入到特殊的 topic:SCHEDULE_TOPIC_XXXX
的 CommitLog 中。
步骤 2:写入 CommitLog(物理存储)
延时消息被写入 CommitLog,如下:
[SCHEDULE_TOPIC_XXXX, queueId = delayLevel - 1]
这个 topic 就是延迟队列的物理存储区,每个 delayLevel 对应一个 queue。
此时:
- CommitLog 有数据;
- ConsumeQueue 还没有生成(因为这还不是真正要消费的消息);
- Index 也没有生成。
步骤 3:ScheduleMessageService 定时轮询处理
RocketMQ broker 后台的调度线程 ScheduleMessageService
每秒执行:
- 从
SCHEDULE_TOPIC_XXXX
对应的 queue(比如 queueId = 2)拉取消息; - 检查是否“到点”;
- 如果“延迟时间已到”,就会把该消息“克隆出来”,**重新作为一个“普通消息”写入真正的 topic(比如
myTopic
)的 CommitLog 中。
步骤 4:转发后再次写入 CommitLog(真正 topic)
这时候,RocketMQ 会做一件关键的事:
- 将消息重新写入 真正的 topic(如
myTopic
) 的 CommitLog; - 根据 topic + queueId 更新 ConsumeQueue 文件;
- 根据 key 或 msgId 写入 IndexFile(可选)。
所以你可以理解为:
延时消息在 CommitLog 中写了两次,第一次是中转(延时队列),第二次才是投递。
步骤 5:Consumer 读取消息(从 ConsumeQueue 读取)
- 消费者订阅
myTopic
; - 根据
myTopic
+ queueId 找到对应的 ConsumeQueue; - ConsumeQueue 中记录了 offset + size + tagHash;
- Consumer 根据这些索引,从 CommitLog 取出真正的消息。
8.延时消息在存储结构中的完整生命周期图解
Producer
|
| 发送延时消息(setDelayTimeLevel)
↓
CommitLog(写入 SCHEDULE_TOPIC_XXXX) <—— 第一次写入
↓
ScheduleMessageService 每秒轮询检查
↓
时间到:
↓
CommitLog(写入实际 topic) <—— 第二次写入
↓ ↓
ConsumeQueue IndexFile <—— 建立消费与检索索引
↓
Consumer 订阅消费
9.每个结构在延时消息中的作用
结构 | 延时消息阶段中作用 |
---|---|
CommitLog | 存储延时消息(第一次),再存真实消息(第二次) |
ConsumeQueue | 只有真正写入真实 topic 后才建立索引(可被消费) |
IndexFile | 可选,存储 key/msgId 用于快速查找(同样延后建立) |
10.事务消息
RocketMQ提供了类似X/Open XA的分布式事务功能,通过事务消息能达到分布式事务的最终一致。XA是一种分布式事务解决方案,一种分布式事务处理模式。
事务消息不支持延迟消息!!!
消息回查:
11.理解XA模式2PC
分布式事务:
对于分布式事务,通俗地说就是,一次操作由若干分支操作组成,这些分支操作分属不同应用,分布在不同服务器上。分布式事务需要保证这些分支操作要么全部成功,要么全部失败。
XA 模式是一个分布式事务协议,使用“两阶段提交(2PC)”来确保多个数据库/资源之间的一致性。
由一个“事务管理器”(Transaction Manager,TM)统一调度;
每个参与者是“资源管理器”(Resource Manager,RM),如 MySQL、Oracle、MQ等;
MQ中 半消息机制 暂存消息,不立刻投递。 也就是半事务消息
下面是一个基于 RocketMQ 使用 XA-2PC 模式(两阶段提交) 的例子:
场景设定:电商系统中的“下单 + 扣库存”操作
假设你开发了一个电商系统,用户每次下单时需要:
- 创建订单(写入订单数据库);
- 扣减库存(写入库存数据库);
- 同时,发送一个消息给其他系统(比如发货系统、用户通知等)。
为了保证这三个操作要么全部成功,要么全部失败,你需要使用 分布式事务。RocketMQ 提供了一种 事务消息(基于2PC)机制 来保证一致性。
RocketMQ XA / 2PC 分布式事务的流程图:
🚶 用户点击下单
|
Producer.sendMessageInTransaction() // 发起事务消息
|
|———[阶段 1]———
| RocketMQ 暂时“挂起”这条消息(不投递)
|
| 执行本地事务(下单 + 扣库存)
| ↳ 如果成功,告诉 MQ “可以提交”
| ↳ 如果失败,告诉 MQ “请回滚”
|
|———[阶段 2]———
| RocketMQ 根据返回状态:
| ↳ commit:将消息发送给消费者
| ↳ rollback:消息丢弃
代码逻辑层面:
步骤 1:发送事务消息(暂不投递)
SendResult sendResult = producer.sendMessageInTransaction(msg, orderData);
- 这时候 RocketMQ 会把消息写入 broker,但不会立即投递给消费者。
- 进入“半消息”状态。
步骤 2:执行本地事务(例如订单写库、库存减一)
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 写订单数据库
// 扣库存数据库
return LocalTransactionState.COMMIT_MESSAGE; // 事务成功
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE; // 事务失败
}
}
步骤 3:RocketMQ 根据返回的状态决定消息是否正式发送
- 如果你返回了
COMMIT_MESSAGE
,RocketMQ 会把这条消息 正式发送给消费者; - 如果你返回
ROLLBACK_MESSAGE
,消息将被丢弃; - 如果你返回
UNKNOWN
,RocketMQ 过一段时间后会回查你的事务状态(checkLocalTransaction()
方法)。
RocketMQ 实现分布式事务的核心特性
特性 | 描述 |
---|---|
半消息机制 | 暂存消息,不立刻投递 |
本地事务钩子 | 自定义执行本地事务 |
事务状态回查 | 处理网络抖动或状态未知场景 |
最终一致性保障 | 提高可用性、牺牲部分实时性 |
小结
步骤 | RocketMQ 做的事 | 应用做的事 |
---|---|---|
1 | 发送“半消息” | 暂存消息 |
2 | 等你本地事务执行完 | 下单 + 扣库存(或其他操作) |
3 | 你告知是成功还是失败 | MQ 根据你返回状态决定提交或回滚 |
4 | 可选:MQ 回查事务状态 | 应用实现 check 回调确认状态 |