RocketMQ 从零到一
一、RocketMQ 整体架构
graph TB
subgraph 生产者端
P[Producer
生产者] end subgraph NameServer集群 NS1[NameServer 1] NS2[NameServer 2] NS3[NameServer 3] end subgraph Broker集群 subgraph Master-Slave架构 M1[Broker Master A] S1[Broker Slave A] M2[Broker Master B] S2[Broker Slave B] end end subgraph 消费者端 C1[Consumer Group A] C2[Consumer Group B] end P -->|注册/发现路由| NS1 P -->|发送消息| M1 P -->|发送消息| M2 M1 -->|同步复制| S1 M2 -->|同步复制| S2 NS1 -->|维护路由信息| M1 NS1 -->|维护路由信息| M2 C1 -->|订阅/拉取消息| M1 C1 -->|订阅/拉取消息| S1 C2 -->|订阅/拉取消息| M2 style P fill:#e1f5fe style NS1 fill:#fff3e0 style M1 fill:#e8f5e9 style C1 fill:#fce4ec
生产者] end subgraph NameServer集群 NS1[NameServer 1] NS2[NameServer 2] NS3[NameServer 3] end subgraph Broker集群 subgraph Master-Slave架构 M1[Broker Master A] S1[Broker Slave A] M2[Broker Master B] S2[Broker Slave B] end end subgraph 消费者端 C1[Consumer Group A] C2[Consumer Group B] end P -->|注册/发现路由| NS1 P -->|发送消息| M1 P -->|发送消息| M2 M1 -->|同步复制| S1 M2 -->|同步复制| S2 NS1 -->|维护路由信息| M1 NS1 -->|维护路由信息| M2 C1 -->|订阅/拉取消息| M1 C1 -->|订阅/拉取消息| S1 C2 -->|订阅/拉取消息| M2 style P fill:#e1f5fe style NS1 fill:#fff3e0 style M1 fill:#e8f5e9 style C1 fill:#fce4ec
架构核心组件说明
| 组件 | 作用 | 特点 |
|---|---|---|
| NameServer | 轻量级路由注册中心 | 无状态、可集群部署、Broker向所有NameServer注册 |
| Broker | 消息存储与转发核心 | 分Master/Slave,支持同步/异步复制 |
| Producer | 消息生产者 | 支持同步/异步/单向发送,负载均衡 |
| Consumer | 消息消费者 | 支持集群消费和广播消费 |
二、NameServer 详解
2.1 设计哲学:轻量级 vs ZooKeeper
graph LR
subgraph ZooKeeper方案
ZK[ZooKeeper集群
强一致性CP] B1[Broker] C1[Client] B1 -->|注册/监听| ZK C1 -->|获取数据| ZK end subgraph NameServer方案 NS[NameServer集群
最终一致性AP] B2[Broker] C2[Client] B2 -->|心跳注册| NS C2 -->|定时拉取路由| NS end style ZK fill:#ffccbc style NS fill:#c8e6c9
强一致性CP] B1[Broker] C1[Client] B1 -->|注册/监听| ZK C1 -->|获取数据| ZK end subgraph NameServer方案 NS[NameServer集群
最终一致性AP] B2[Broker] C2[Client] B2 -->|心跳注册| NS C2 -->|定时拉取路由| NS end style ZK fill:#ffccbc style NS fill:#c8e6c9
为什么不用ZooKeeper?
| 对比维度 | ZooKeeper | NameServer |
|---|---|---|
| 一致性 | 强一致性(CP) | 最终一致性(AP) |
| 复杂度 | 有Leader选举,复杂 | 完全无状态,简单 |
| 性能 | 写性能受限于Leader | 各节点独立,性能高 |
| 可用性 | Leader宕机需选举,短暂不可用 | 单节点宕机不影响其他节点 |
| 数据同步 | ZAB协议同步 | Broker向所有NameServer独立注册 |
核心原因 :RocketMQ认为消息队列场景下,路由信息的最终一致性足够,不需要强一致性的协调服务,NameServer的设计大大简化了架构复杂度。
2.2 NameServer 路由管理流程
sequenceDiagram
participant B as Broker
participant NS as NameServer
participant P as Producer/Consumer
Note over B,NS: Broker启动注册流程
B->>NS: 1. 发送心跳包(包含Broker信息)
NS-->>B: 2. 注册成功
loop 每30秒
B->>NS: 3. 发送心跳维持存活
end
Note over NS,P: 客户端获取路由
P->>NS: 4. 请求Topic路由信息
NS-->>P: 5. 返回Broker列表及队列分布
alt Broker宕机超过120秒
NS->>NS: 6. 剔除失效Broker
end
关键源码逻辑:
java
// Broker向NameServer注册的核心逻辑
public class BrokerController {
public void registerBrokerAll(boolean checkOrderConfig, boolean oneway) {
TopicConfigSerializeWrapper topicConfigWrapper = this.getTopicConfigManager().buildTopicConfigSerializeWrapper();
RegisterBrokerResult registerBrokerResult = this.brokerOuterAPI.registerBrokerAll(
this.brokerConfig.getBrokerClusterName(),
this.getBrokerAddr(),
this.brokerConfig.getBrokerName(),
this.brokerConfig.getBrokerId(), // 0=Master, >0=Slave
topicConfigWrapper,
this.filterServerManager.buildNewFilterServerList(),
oneway,
this.brokerConfig.getRegisterBrokerTimeoutMills()
);
}
}
心跳机制参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
brokerHeartbeatInterval |
30秒 | Broker发送心跳间隔 |
brokerTimeoutMillis |
120秒 | NameServer判定Broker失效时间 |
scanNotActiveBrokerInterval |
5秒 | NameServer扫描间隔 |
三、Broker 详解
3.1 Broker 存储架构
graph TB
subgraph Broker存储架构
subgraph 内存区域
M1[MappedFileQueue
内存映射文件队列] M2[TransientStorePool
堆外内存池
仅异步刷盘开启] end subgraph 磁盘存储 D1[CommitLog
消息主体存储
1G一个文件] D2[ConsumeQueue
消费队列索引
20字节/条] D3[IndexFile
消息索引
支持Key查询] D4[ConfigStore
配置存储] end end P[Producer] -->|写入| M1 M1 -->|刷盘| D1 D1 -->|构建索引| D2 D1 -->|构建索引| D3 C[Consumer] -->|读取| D2 style M1 fill:#e3f2fd style M2 fill:#e3f2fd style D1 fill:#fff8e1 style D2 fill:#fff8e1
内存映射文件队列] M2[TransientStorePool
堆外内存池
仅异步刷盘开启] end subgraph 磁盘存储 D1[CommitLog
消息主体存储
1G一个文件] D2[ConsumeQueue
消费队列索引
20字节/条] D3[IndexFile
消息索引
支持Key查询] D4[ConfigStore
配置存储] end end P[Producer] -->|写入| M1 M1 -->|刷盘| D1 D1 -->|构建索引| D2 D1 -->|构建索引| D3 C[Consumer] -->|读取| D2 style M1 fill:#e3f2fd style M2 fill:#e3f2fd style D1 fill:#fff8e1 style D2 fill:#fff8e1
3.2 CommitLog - 消息主体存储
CommitLog 文件结构:
javascript
CommitLog 存储目录结构:
~/store/commitlog/
├── 00000000000000000000 ← 第一个文件,起始偏移量0
├── 00000000001073741824 ← 第二个文件,起始偏移量1G
├── 00000000002147483648 ← 第三个文件,起始偏移量2G
└── ...
每个文件固定1GB(可配置),文件名 = 起始物理偏移量
单条消息存储格式:
css
┌─────────────────────────────────────────────────────────────┐
│ CommitLog 消息格式 │
├──────────────┬─────────┬────────────────────────────────────┤
│ 字段 │ 大小 │ 说明 │
├──────────────┼─────────┼────────────────────────────────────┤
│ totalSize │ 4字节 │ 消息总长度 │
│ magicCode │ 4字节 │ 魔术字,用于校验 │
│ bodyCRC │ 4字节 │ 消息体CRC校验码 │
│ queueId │ 4字节 │ 消息队列ID │
│ flag │ 4字节 │ 消息标志(如事务标记) │
│ queueOffset │ 8字节 │ 消息在ConsumeQueue的偏移量 │
│ physicOffset │ 8字节 │ 消息在CommitLog的物理偏移量 │
│ sysFlag │ 4字节 │ 系统标志(压缩/事务等) │
│ bornTimestamp│ 8字节 │ 消息生成时间戳 │
│ bornHost │ 8字节 │ 生产者地址 │
│ storeTimestamp│ 8字节 │ 消息存储时间戳 │
│ storeHost │ 8字节 │ 存储地址(Broker地址) │
│ reconsumeTimes│ 4字节 │ 重试次数 │
│ preparedTransactionOffset│ 8字节 │ 事务消息偏移量 │
│ bodyLength │ 4字节 │ 消息体长度 │
│ body │ N字节 │ 消息体(JSON/二进制) │
│ topicLength │ 1字节 │ Topic长度 │
│ topic │ N字节 │ Topic名称 │
│ propertiesLength│ 2字节 │ 属性长度 │
│ properties │ N字节 │ 扩展属性(如keys、tags等) │
└──────────────┴─────────┴────────────────────────────────────┘
3.3 ConsumeQueue - 消费索引
ConsumeQueue 设计精髓:
graph LR
subgraph "ConsumeQueue 条目结构(20字节)"
A[CommitLog Offset
8字节] B[Message Size
4字节] C[Message Tag Hashcode
8字节] end D[Consumer] -->|根据offset| A A -->|定位到| E[CommitLog具体位置] B -->|读取| E C -->|过滤| F[Tag过滤] style A fill:#e8f5e9 style B fill:#e8f5e9 style C fill:#e8f5e9
8字节] B[Message Size
4字节] C[Message Tag Hashcode
8字节] end D[Consumer] -->|根据offset| A A -->|定位到| E[CommitLog具体位置] B -->|读取| E C -->|过滤| F[Tag过滤] style A fill:#e8f5e9 style B fill:#e8f5e9 style C fill:#e8f5e9
为什么需要ConsumeQueue?
scss
场景:Consumer要消费TopicA的消息
没有ConsumeQueue时:
Consumer → 扫描整个CommitLog → 过滤TopicA的消息 → 效率极低 O(n)
有ConsumeQueue时:
Consumer → 直接读取TopicA的ConsumeQueue文件 →
获取(commitLogOffset, size) →
直接定位CommitLog读取 → 效率极高 O(1)
ConsumeQueue 存储结构:
javascript
~/store/consumequeue/{topic}/{queueId}/
├── 00000000000000000000 ← 每个条目20字节
├── 00000000000006000000 ← 30万条消息一个文件(默认)
└── ...
计算:30万条 × 20字节 = 6MB,所以每个文件约存30万条索引
3.4 刷盘机制对比
graph TB
subgraph 同步刷盘
P1[Producer] -->|写入| M1[MappedFile
内存映射] M1 -->|立即刷盘| D1[CommitLog磁盘] D1 -->|返回成功| P1 end subgraph 异步刷盘 P2[Producer] -->|写入| M2[MappedFile
内存映射] M2 -->|立即返回成功| P2 M2 -.->|定时刷盘| D2[CommitLog磁盘] end subgraph 异步刷盘+堆外内存 P3[Producer] -->|写入| M3[TransientStorePool
堆外内存] M3 -->|立即返回成功| P3 M3 -.->|定时提交到| M4[MappedFile] M4 -.->|定时刷盘| D3[CommitLog磁盘] end style P1 fill:#ffcdd2 style P2 fill:#c8e6c9 style P3 fill:#bbdefb
内存映射] M1 -->|立即刷盘| D1[CommitLog磁盘] D1 -->|返回成功| P1 end subgraph 异步刷盘 P2[Producer] -->|写入| M2[MappedFile
内存映射] M2 -->|立即返回成功| P2 M2 -.->|定时刷盘| D2[CommitLog磁盘] end subgraph 异步刷盘+堆外内存 P3[Producer] -->|写入| M3[TransientStorePool
堆外内存] M3 -->|立即返回成功| P3 M3 -.->|定时提交到| M4[MappedFile] M4 -.->|定时刷盘| D3[CommitLog磁盘] end style P1 fill:#ffcdd2 style P2 fill:#c8e6c9 style P3 fill:#bbdefb
| 刷盘方式 | 数据安全 | 性能 | 适用场景 |
|---|---|---|---|
| 同步刷盘 | 最高(不丢消息) | 最低(每条等待磁盘IO) | 金融、支付等强一致场景 |
| 异步刷盘 | 中等(宕机可能丢少量) | 高 | 普通业务,追求吞吐量 |
| 异步+堆外 | 中等 | 最高(避免页缓存竞争) | 极致性能场景 |
同步刷盘源码核心逻辑:
java
// CommitLog.java 同步刷盘实现
public class CommitLog {
public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
// 1. 获取MappedFile
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
// 2. 写入消息到内存
result = mappedFile.appendMessage(msg, this.appendMessageCallback);
// 3. 同步刷盘 - 等待刷盘完成
if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
// 提交到刷盘线程,并等待唤醒
boolean flushOK = flushCommitLogService.putRequest(request);
// 阻塞等待刷盘完成
request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
}
return new PutMessageResult(PutMessageStatus.PUT_OK, result);
}
}
四、消息生产(Producer)
4.1 发送方式对比
sequenceDiagram
participant P as Producer
participant B as Broker
Note over P,B: 方式一:同步发送
P->>B: send(msg)
B-->>P: 等待Broker确认后返回
Note right of P: 可靠,但耗时
Note over P,B: 方式二:异步发送
P->>B: send(msg, callback)
P->>P: 不等待,立即返回
B-->>P: callback.onSuccess/onException
Note right of P: 高吞吐,需处理回调
Note over P,B: 方式三:单向发送
P->>B: sendOneway(msg)
Note right of P: 不等待任何返回
Note right of P: 最快,但可能丢失
代码示例:
java
public class ProducerExample {
// 1. 同步发送 - 最常用
public void syncSend() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("test_group");
producer.setNamesrvAddr("localhost:9876");
producer.start();
Message msg = new Message("TopicTest", "TagA", "OrderID001", "Hello World".getBytes());
// 同步等待发送结果
SendResult sendResult = producer.send(msg);
System.out.println("发送结果: " + sendResult.getSendStatus());
// 输出: SEND_OK, SLAVE_NOT_AVAILABLE, FLUSH_DISK_TIMEOUT等
producer.shutdown();
}
// 2. 异步发送 - 高并发场景
public void asyncSend() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("async_group");
producer.setNamesrvAddr("localhost:9876");
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0); // 异步发送失败不重试
Message msg = new Message("TopicTest", "TagA", "OrderID001", "Hello World".getBytes());
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("异步发送成功: " + sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
System.out.println("异步发送失败: " + e.getMessage());
// 需要自行处理失败消息(如记录日志、重试、入死信队列)
}
});
}
// 3. 单向发送 - 日志采集等不敏感场景
public void onewaySend() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("oneway_group");
producer.start();
Message msg = new Message("TopicTest", "TagA", "Hello World".getBytes());
producer.sendOneway(msg); // 不等待返回,立即返回
// 适用于:日志收集、监控上报等可丢失场景
}
}
4.2 消息发送负载均衡
graph TB
subgraph Topic队列分布
T["Topic: OrderTopic"]
Q0["Queue 0
Broker-A"] Q1["Queue 1
Broker-A"] Q2["Queue 2
Broker-B"] Q3["Queue 3
Broker-B"] T --> Q0 T --> Q1 T --> Q2 T --> Q3 end subgraph 轮询策略 P1["Producer 1"] -->|msg1| Q0 P1 -->|msg2| Q1 P1 -->|msg3| Q2 P1 -->|msg4| Q3 P1 -->|msg5| Q0 end subgraph 顺序消息策略 P2["Producer 2"] -->|"hash(orderId)%4=0"| Q0 P2 -->|"hash(orderId)%4=0"| Q0 P2 -->|"hash(orderId)%4=2"| Q2 end style Q0 fill:#e8f5e9 style Q1 fill:#fff3e0 style Q2 fill:#e8f5e9 style Q3 fill:#fff3e0
Broker-A"] Q1["Queue 1
Broker-A"] Q2["Queue 2
Broker-B"] Q3["Queue 3
Broker-B"] T --> Q0 T --> Q1 T --> Q2 T --> Q3 end subgraph 轮询策略 P1["Producer 1"] -->|msg1| Q0 P1 -->|msg2| Q1 P1 -->|msg3| Q2 P1 -->|msg4| Q3 P1 -->|msg5| Q0 end subgraph 顺序消息策略 P2["Producer 2"] -->|"hash(orderId)%4=0"| Q0 P2 -->|"hash(orderId)%4=0"| Q0 P2 -->|"hash(orderId)%4=2"| Q2 end style Q0 fill:#e8f5e9 style Q1 fill:#fff3e0 style Q2 fill:#e8f5e9 style Q3 fill:#fff3e0
默认负载均衡策略:轮询(RoundRobin)
java
// DefaultMQProducerImpl.java 默认选择队列
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
// 轮询选择队列
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
// TopicPublishInfo.java
public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
if (lastBrokerName == null) {
return selectOneMessageQueue(); // 简单轮询
} else {
// 上次发送失败的Broker,这次避开
for (int i = 0; i < this.messageQueueList.size(); i++) {
int index = this.sendWhichQueue.getAndIncrement();
int pos = Math.abs(index) % this.messageQueueList.size();
MessageQueue mq = this.messageQueueList.get(pos);
if (!mq.getBrokerName().equals(lastBrokerName)) {
return mq; // 选择不同Broker的队列
}
}
return selectOneMessageQueue(); // 无法避开,继续轮询
}
}
五、消息消费(Consumer)
5.1 消费模式:集群消费 vs 广播消费
graph TB
subgraph 集群消费 Clustering
T[Topic: OrderTopic
4个Queue] C1[Consumer 1
消费Queue0, Queue1] C2[Consumer 2
消费Queue2, Queue3] C3[Consumer 3
不分配队列
备用] T --> C1 T --> C2 end subgraph 广播消费 Broadcasting T2[Topic: OrderTopic
4个Queue] C4[Consumer 1
消费全部4个Queue] C5[Consumer 2
消费全部4个Queue] C6[Consumer 3
消费全部4个Queue] T2 --> C4 T2 --> C5 T2 --> C6 end style C1 fill:#c8e6c9 style C2 fill:#c8e6c9 style C3 fill:#ffcdd2 style C4 fill:#bbdefb style C5 fill:#bbdefb style C6 fill:#bbdefb
4个Queue] C1[Consumer 1
消费Queue0, Queue1] C2[Consumer 2
消费Queue2, Queue3] C3[Consumer 3
不分配队列
备用] T --> C1 T --> C2 end subgraph 广播消费 Broadcasting T2[Topic: OrderTopic
4个Queue] C4[Consumer 1
消费全部4个Queue] C5[Consumer 2
消费全部4个Queue] C6[Consumer 3
消费全部4个Queue] T2 --> C4 T2 --> C5 T2 --> C6 end style C1 fill:#c8e6c9 style C2 fill:#c8e6c9 style C3 fill:#ffcdd2 style C4 fill:#bbdefb style C5 fill:#bbdefb style C6 fill:#bbdefb
| 特性 | 集群消费(默认) | 广播消费 |
|---|---|---|
| 消费组内 | 每条消息只被一个消费者消费 | 每条消息被所有消费者消费 |
| 适用场景 | 普通业务处理 | 通知所有实例更新本地缓存/配置 |
| 消费进度 | 服务端统一管理(OffsetStore) | 消费者本地管理 |
| 故障恢复 | 自动重平衡分配队列 | 无重平衡,各实例独立 |
| 注意 | - | 消费失败不会重试,直接丢弃 |
代码示例:
java
public class ConsumerExample {
// 1. 集群消费(默认)
public void clusteringConsumer() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
// 默认就是集群模式
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.subscribe("OrderTopic", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
System.out.println("收到消息: " + new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
// 返回RECONSUME_LATER会进入重试队列
}
});
consumer.start();
}
// 2. 广播消费
public void broadcastConsumer() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("config_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
// 设置为广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.subscribe("ConfigTopic", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
// 所有实例都会收到,用于刷新本地缓存
refreshLocalCache(new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
// 广播模式下,消费失败不会重试!
}
});
consumer.start();
}
}
5.2 消费方式:Push vs Pull
sequenceDiagram
participant C as Consumer
participant B as Broker
Note over C,B: Push模式(长轮询,默认推荐)
C->>B: 1. 发送拉取请求(挂起15秒)
B->>B: 2. 检查是否有新消息
alt 有新消息
B-->>C: 3a. 立即返回消息
else 无新消息
B->>B: 3b. 等待直到有消息或超时
B-->>C: 4. 超时后返回空,客户端立即再次请求
end
Note over C,B: Pull模式(主动拉取)
C->>C: 1. 业务代码控制拉取时机
C->>B: 2. 主动请求拉取消息
B-->>C: 3. 立即返回(有或没有)
C->>C: 4. 处理消息
C->>C: 5. 决定何时再次拉取
| 对比 | PushConsumer | PullConsumer |
|---|---|---|
| 使用难度 | 简单,自动管理 | 复杂,需自行管理Offset |
| 实时性 | 高(长轮询) | 取决于拉取频率 |
| 吞吐量控制 | 由Broker控制流控 | 由消费者控制 |
| 适用场景 | 大多数业务场景 | 定时任务、批量处理、流计算 |
| 消息堆积处理 | 自动调整拉取速率 | 需自行实现 |
Push模式核心 - 长轮询机制:
java
// DefaultMQPushConsumerImpl.java 拉取消息流程
public class DefaultMQPushConsumerImpl implements MQPushConsumer {
// 拉取消息服务
private final PullMessageService pullMessageService;
// 核心拉取逻辑
public void pullMessage(final PullRequest pullRequest) {
// 1. 获取ProcessQueue(内存中的消息缓存队列)
final ProcessQueue processQueue = this.rebalanceImpl.getProcessQueueTable().get(pullRequest.getMessageQueue());
// 2. 流控检查:如果本地积压超过1000条,暂停拉取
if (processQueue.getMsgCount().get() > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
return;
}
// 3. 发送拉取请求到Broker
this.pullAPIWrapper.pullKernelImpl(
pullRequest.getMessageQueue(), // 目标队列
subExpression, // 订阅表达式
subscriptionData.getExpressionType(),
pullRequest.getNextOffset(), // 消费偏移量
this.defaultMQPushConsumer.getPullBatchSize(), // 批量大小(默认32)
sysFlag,
commitOffsetValue,
BROKER_SUSPEND_MAX_TIME_MILLIS, // Broker挂起最大时间:15秒
CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND, // 消费者超时时间:30秒
CommunicationMode.ASYNC, // 异步通信
pullCallback // 回调处理
);
}
}
5.3 消费者重平衡(Rebalance)
sequenceDiagram
participant C1 as Consumer 1
participant C2 as Consumer 2
participant C3 as Consumer 3
participant B as Broker
Note over C1,B: 初始状态:2个消费者,4个队列
C1->>B: 订阅Topic,获取队列信息
C2->>B: 订阅Topic,获取队列信息
B-->>C1: Queue0, Queue1, Queue2, Queue3
B-->>C2: Queue0, Queue1, Queue2, Queue3
C1->>C1: Rebalance: 分配Queue0, Queue1
C2->>C2: Rebalance: 分配Queue2, Queue3
Note over C1,C3: 新消费者加入
C3->>B: 订阅Topic
B-->>C3: Queue0, Queue1, Queue2, Queue3
C1->>C1: 触发Rebalance
C2->>C2: 触发Rebalance
C3->>C3: 触发Rebalance
C1->>C1: 新分配:Queue0
C2->>C2: 新分配:Queue1, Queue2
C3->>C3: 新分配:Queue3
Note over C1,C3: 队列重新均匀分配
重平衡触发时机:
| 触发条件 | 说明 |
|---|---|
| 消费者启动 | 首次加入消费组 |
| 消费者宕机 | 通过心跳超时检测(120秒) |
| 消费者主动退出 | 调用shutdown() |
| 队列数量变化 | Broker扩容/缩容导致队列增减 |
| 订阅关系变化 | Topic订阅Tag变化 |
重平衡策略:
java
// AllocateMessageQueueStrategy 分配策略接口
public interface AllocateMessageQueueStrategy {
List<MessageQueue> allocate(
String consumerGroup, // 消费组名
String currentCID, // 当前消费者ID
List<MessageQueue> mqAll, // 所有队列
List<String> cidAll // 所有消费者
);
}
// 1. 平均分配策略(默认)- 连续分配
// 4队列 + 3消费者 → C1:[0,1], C2:[2], C3:[3]
// 2. 平均轮询分配 - 轮流分配
// 4队列 + 3消费者 → C1:[0,3], C2:[1], C3:[2]
// 3. 一致性Hash - 相同消费者总是分配相同队列
// 适用于:希望减少Rebalance时的队列迁移
// 4. 按机房分配 - 就近消费
// 适用于:多机房部署,优先消费本机房Broker的队列
六、消息类型详解
6.1 普通消息
graph LR
P[Producer] -->|send| B[Broker]
B -->|存储到| C[CommitLog]
C -->|构建| D[ConsumeQueue]
D -->|Consumer拉取| E[Consumer]
style B fill:#e8f5e9
最基础的消息类型,无特殊保证,适用于日志收集、普通通知等对顺序和一致性要求不高的场景。
6.2 顺序消息

顺序消息核心保证:
| 保证维度 | 说明 |
|---|---|
| 发送顺序 | 同一队列内,Producer按发送顺序存储 |
| 存储顺序 | CommitLog和ConsumeQueue保持相同顺序 |
| 消费顺序 | 一个队列同一时间只分配给一个消费者,单线程消费 |
顺序消息代码示例:
java
public class OrderMessageExample {
public void sendOrderMessage() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("order_producer");
producer.start();
// 订单ID作为shardingKey,保证同一订单的消息有序
String orderId = "ORDER_2024001";
// 1. 创建订单
Message msg1 = new Message("OrderTopic", "Create",
("创建订单:" + orderId).getBytes());
// 使用MessageQueueSelector选择队列
SendResult result1 = producer.send(msg1, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
String orderId = (String) arg;
// hash取模,保证同一orderId总是进入同一队列
int index = Math.abs(orderId.hashCode()) % mqs.size();
return mqs.get(index);
}
}, orderId); // 传入orderId作为选择参数
// 2. 支付订单
Message msg2 = new Message("OrderTopic", "Pay",
("支付订单:" + orderId).getBytes());
SendResult result2 = producer.send(msg2, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
String orderId = (String) arg;
int index = Math.abs(orderId.hashCode()) % mqs.size();
return mqs.get(index);
}
}, orderId);
// msg1和msg2会进入同一队列,保证顺序
producer.shutdown();
}
// 顺序消费
public void consumeOrderMessage() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_consumer");
consumer.setConsumeMessageBatchMaxSize(1); // 顺序消费必须单条
consumer.subscribe("OrderTopic", "*");
// 使用顺序监听器
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
// 同一队列的消息,会串行调用此方法
// 不会并发执行,保证顺序
for (MessageExt msg : msgs) {
System.out.println("顺序消费: " + new String(msg.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
// 返回SUSPEND_CURRENT_QUEUE_A_MOMENT会暂停当前队列消费
}
});
consumer.start();
}
}
6.3 延时消息

延时级别配置(broker.conf):
properties
# 默认延时级别,单位秒
# 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
延时消息代码:
java
public class DelayMessageExample {
public void sendDelayMessage() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("delay_producer");
producer.start();
Message msg = new Message("DelayTopic", "TagA", "OrderID001",
"订单超时取消".getBytes());
// 设置延时级别:3对应10秒
msg.setDelayTimeLevel(3);
SendResult result = producer.send(msg);
System.out.println("发送时间: " + new Date());
System.out.println("预计消费时间: " + new Date(System.currentTimeMillis() + 10000));
producer.shutdown();
}
}
延时消息实现原理:
java
// ScheduleMessageService.java 核心实现
public class ScheduleMessageService extends ServiceThread {
// 18个延时级别,每个级别一个队列
private final HashMap<Integer, Long> delayLevelTable = new HashMap<>();
// 每个延时级别对应一个ConsumeQueue
// SCHEDULE_TOPIC_XXXX 下有18个队列,分别对应18个延时级别
@Override
public void run() {
while (!this.isStopped()) {
for (int i = 0; i < delayLevelTable.size(); i++) {
int delayLevel = i + 1;
// 检查该延时级别的队列是否有到期消息
long offset = this.offsetTable.get(delayLevel);
ConsumeQueue cq = this.defaultMessageStore.findConsumeQueue(
SCHEDULE_TOPIC, delayLevel - 1);
// 读取ConsumeQueue条目
SelectMappedBufferResult bufferResult = cq.getIndexBuffer(offset);
for (int j = 0; j < bufferResult.getSize(); j += CQ_STORE_UNIT_SIZE) {
long offsetPy = bufferResult.getByteBuffer().getLong(); // CommitLog偏移
int sizePy = bufferResult.getByteBuffer().getInt(); // 消息大小
long tagsCode = bufferResult.getByteBuffer().getLong(); // 存储的是投递时间
// 判断是否到期
if (tagsCode <= System.currentTimeMillis()) {
// 到期,从CommitLog读取原始消息
MessageExt msgExt = this.defaultMessageStore.lookMessageByOffset(offsetPy, sizePy);
// 恢复原始Topic,重新存入CommitLog
MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);
// 发送到原始Topic的ConsumeQueue
PutMessageResult putMessageResult = this.defaultMessageStore.putMessage(msgInner);
}
}
}
}
}
}
6.4 事务消息

事务消息状态流转:
stateDiagram-v2
[*] --> HalfMessage: 发送Half消息
HalfMessage --> Committed: 发送Commit
HalfMessage --> Rollbacked: 发送Rollback
HalfMessage --> Checking: 回查触发
Checking --> Committed: 本地事务已执行
Checking --> Rollbacked: 本地事务未执行/失败
Checking --> [*]: 回查超过15次,丢弃
Committed --> Consumed: 消费者消费
Rollbacked --> [*]: 消息删除
事务消息代码示例:
java
public class TransactionMessageExample {
public void sendTransactionMessage() throws Exception {
// 使用事务生产者
TransactionMQProducer producer = new TransactionMQProducer("trans_group");
// 设置事务监听器(核心)
producer.setTransactionListener(new TransactionListener() {
/**
* 执行本地事务
* 在发送Half消息成功后调用
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 执行本地业务逻辑
String orderId = (String) arg;
boolean success = executeBusinessLogic(orderId);
if (success) {
// 本地事务成功,提交Half消息
return LocalTransactionState.COMMIT_MESSAGE;
} else {
// 本地事务失败,回滚Half消息
return LocalTransactionState.ROLLBACK_MESSAGE;
}
} catch (Exception e) {
// 异常,进入未知状态,触发回查
return LocalTransactionState.UNKNOW;
}
}
/**
* 回查本地事务状态
* Broker定时回查时调用(默认1分钟开始,最多回查15次)
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 根据消息内容查询本地事务状态
String transactionId = msg.getTransactionId();
boolean isCommitted = checkTransactionStatus(transactionId);
if (isCommitted) {
return LocalTransactionState.COMMIT_MESSAGE;
} else {
// 如果还没完成,可以继续返回UNKNOW,等待下次回查
// 但要注意回查次数限制(默认15次)
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
});
producer.start();
// 发送事务消息
Message msg = new Message("TransTopic", "TagA", "OrderID001",
"订单创建消息".getBytes());
msg.setTransactionId("TRANS_2024001");
// 第二个参数是executeLocalTransaction的arg参数
TransactionSendResult result = producer.sendMessageInTransaction(msg, "OrderID001");
System.out.println("发送结果: " + result.getLocalTransactionState());
}
private boolean executeBusinessLogic(String orderId) {
// 执行本地事务,如:
// 1. 创建订单记录
// 2. 扣减库存
// 3. 扣减用户余额
return true; // 模拟成功
}
private boolean checkTransactionStatus(String transactionId) {
// 查询数据库,确认事务是否已提交
return true;
}
}
事务消息核心配置:
| 配置项 | 默认值 | 说明 |
|---|---|---|
transactionTimeOut |
6秒 | 事务超时时间 |
transactionCheckMax |
15次 | 最大回查次数 |
transactionCheckInterval |
60秒 | 首次回查间隔 |
endTransactionThreadPoolNums |
24 | 提交/回滚线程数 |
七、消息存储核心机制
7.1 MappedFile - 内存映射文件
graph TB
subgraph 内存映射机制
JVM[JVM堆内存]
OS[操作系统页缓存
PageCache] DISK[物理磁盘] JVM -->|mmap系统调用| OS OS <-->|脏页刷盘| DISK subgraph MappedFile MF[MappedFile
逻辑文件对象] MB[MappedByteBuffer
内存映射缓冲区] MF --> MB end MB -->|直接操作| OS end style OS fill:#e3f2fd
PageCache] DISK[物理磁盘] JVM -->|mmap系统调用| OS OS <-->|脏页刷盘| DISK subgraph MappedFile MF[MappedFile
逻辑文件对象] MB[MappedByteBuffer
内存映射缓冲区] MF --> MB end MB -->|直接操作| OS end style OS fill:#e3f2fd
内存映射 vs 普通IO:
| 特性 | 普通文件IO | MappedFile(mmap) |
|---|---|---|
| 数据拷贝 | 用户态↔内核态↔磁盘(3次拷贝) | 用户态直接操作页缓存(0次拷贝) |
| 内存占用 | 堆内存 + 页缓存(双份) | 共享页缓存(一份) |
| 大文件处理 | 需要分块读取 | 可直接映射大文件 |
| 刷盘控制 | 应用层flush | 依赖操作系统刷盘或强制flush |
| 适用场景 | 小文件、随机读写 | 大文件顺序读写 |
RocketMQ MappedFile 核心代码:
java
public class MappedFile extends ReferenceResource {
// 文件大小:默认1GB(CommitLog)或6MB(ConsumeQueue)
public static final int OS_PAGE_SIZE = 1024 * 4; // 4KB,操作系统页大小
protected int fileSize;
protected FileChannel fileChannel;
// 核心:内存映射缓冲区
protected MappedByteBuffer mappedByteBuffer;
// 写位置(当前写到哪里了)
protected final AtomicInteger wrotePosition = new AtomicInteger(0);
// 提交位置(异步刷盘时使用)
protected final AtomicInteger committedPosition = new AtomicInteger(0);
// 刷盘位置
protected final AtomicInteger flushedPosition = new AtomicInteger(0);
public MappedFile(final String fileName, final int fileSize) throws IOException {
this.fileSize = fileSize;
this.file = new File(fileName);
// 创建RandomAccessFile
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
// 核心:执行mmap内存映射
this.mappedByteBuffer = this.fileChannel.map(
MapMode.READ_WRITE, // 读写模式
0, // 起始偏移
fileSize // 映射大小
);
}
// 追加消息
public boolean appendMessage(final byte[] data) {
int currentPos = this.wrotePosition.get();
if (currentPos + data.length <= this.fileSize) {
// 直接写入内存映射缓冲区,不涉及系统调用
this.mappedByteBuffer.put(data);
this.wrotePosition.addAndGet(data.length);
return true;
}
return false; // 文件已满,需要创建新文件
}
// 刷盘
public int flush(final int flushLeastPages) {
if (this.isAbleToFlush(flushLeastPages)) {
// 强制刷盘,调用msync或fsync
this.mappedByteBuffer.force();
this.flushedPosition.set(this.committedPosition.get());
}
return this.getFlushedPosition();
}
}
7.2 消息存储流程详解

7.3 零拷贝与消息读取

RocketMQ中的零拷贝应用:
java
// MessageStore.java 消息读取(Consumer拉取时)
public GetMessageResult getMessage(String group, String topic, int queueId,
long offset, int maxMsgNums, MessageFilter messageFilter) {
// 1. 根据offset定位ConsumeQueue
ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);
// 2. 读取ConsumeQueue获取(commitLogOffset, size)
SelectMappedBufferResult bufferResult = consumeQueue.getIndexBuffer(offset);
// 3. 根据commitLogOffset直接定位CommitLog
long offsetPy = bufferResult.getByteBuffer().getLong();
int sizePy = bufferResult.getByteBuffer().getInt();
// 4. 从CommitLog读取消息 - 使用零拷贝
SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy);
// selectResult包含MappedByteBuffer的引用
// 直接传递给Netty,通过FileRegion进行零拷贝传输
return getResult;
}
// Netty传输时使用FileRegion实现零拷贝
// FileRegion底层使用sendfile系统调用
八、高可用架构
8.1 主从复制架构

复制配置:
properties
# broker.conf
# 复制方式:SYNC_MASTER / ASYNC_MASTER / SLAVE
brokerRole=SYNC_MASTER
# 同步复制下,Master等待Slave确认的超时时间
syncFlushTimeout=5000
# Slave同步Master的间隔
slaveReadEnable=true # Slave是否可读(分担读压力)
8.2 Dledger 高可用(Raft协议)

Dledger vs 传统主从:
| 特性 | 传统主从 | Dledger(Raft) |
|---|---|---|
| Leader选举 | 人工切换或依赖NameServer | 自动选举,秒级切换 |
| 一致性 | 最终一致(异步)或强一致(同步性能差) | 强一致,多数派确认 |
| 可用性 | Master宕机需切换 | 自动故障转移 |
| 部署复杂度 | 简单 | 复杂,至少3节点 |
| 性能 | 高 | 略低(需要多数派确认) |
| 数据可靠性 | 依赖复制配置 | 高(CommitIndex机制) |
九、RocketMQ 5.0 新特性
9.1 Proxy模式(计算存储分离)

9.2 Pop模式 - 解决重平衡痛点

Pop模式优势:
| 传统模式 | Pop模式 |
|---|---|
| 消费者与队列绑定 | 消费者与队列解耦 |
| 重平衡导致消息重复/延迟 | 无重平衡,消息即时消费 |
| 消费者数量受限于队列数 | 消费者数量不受队列数限制 |
| 故障恢复需等待重平衡 | 故障即时恢复,消息不堆积 |
十、核心参数调优速查
10.1 Broker关键配置
properties
# === 存储配置 ===
# 刷盘方式:SYNC_FLUSH / ASYNC_FLUSH
flushDiskType=ASYNC_FLUSH
# CommitLog文件大小,默认1G
mapedFileSizeCommitLog=1073741824
# ConsumeQueue文件大小,默认30万条*20字节=6MB
mapedFileSizeConsumeQueue=6000000
# 开启堆外内存池(异步刷盘时提升性能)
transientStorePoolEnable=true
transientStorePoolSize=5
# === 复制配置 ===
# Broker角色:SYNC_MASTER / ASYNC_MASTER / SLAVE
brokerRole=ASYNC_MASTER
# === 消息配置 ===
# 单消息最大大小,默认4MB
maxMessageSize=4194304
# 消息在ConsumeQueue存储时间,默认72小时
fileReservedTime=72
# 磁盘使用超过75%开始清理过期文件
diskMaxUsedSpaceRatio=75
10.2 Producer关键配置
java
DefaultMQProducer producer = new DefaultMQProducer("group");
// 发送超时时间,默认3秒
producer.setSendMsgTimeout(3000);
// 消息体压缩阈值,默认4KB
producer.setCompressMsgBodyOverHowmuch(1024 * 4);
// 同步发送失败重试次数,默认2次
producer.setRetryTimesWhenSendFailed(2);
// 异步发送失败重试次数,默认2次
producer.setRetryTimesWhenSendAsyncFailed(2);
// 是否发送到另一个Broker,默认false
producer.setRetryAnotherBrokerWhenNotStoreOK(false);
// 最大消息大小,默认4MB
producer.setMaxMessageSize(1024 * 1024 * 4);
10.3 Consumer关键配置
java
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group");
// 消费线程数,默认20
consumer.setConsumeThreadMin(20);
consumer.setConsumeThreadMax(64);
// 单次拉取消息数量,默认32
consumer.setPullBatchSize(32);
// 单次消费消息数量,默认1(顺序消费必须为1)
consumer.setConsumeMessageBatchMaxSize(1);
// 本地队列缓存消息数量阈值,默认1000(流控)
consumer.setPullThresholdForQueue(1000);
// 本地队列缓存消息大小阈值,默认100MB
consumer.setPullThresholdSizeForQueue(100);
// 消费超时时间,默认15分钟
consumer.setConsumeTimeout(15);
// 最大重试次数,默认16次
consumer.setMaxReconsumeTimes(16);
// 消费模式:CLUSTERING / BROADCASTING
consumer.setMessageModel(MessageModel.CLUSTERING);
总结图谱

RocketMQ的设计精髓在于顺序写磁盘 + 内存映射 + 索引构建,在保证高吞吐的同时,通过多种消息类型(顺序、延时、事务)满足不同业务场景。理解其核心存储架构和消息流转机制,是掌握RocketMQ的关键。