rocketmq从零单排

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

架构核心组件说明

组件 作用 特点
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

为什么不用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

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

为什么需要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
刷盘方式 数据安全 性能 适用场景
同步刷盘 最高(不丢消息) 最低(每条等待磁盘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

默认负载均衡策略:轮询(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
特性 集群消费(默认) 广播消费
消费组内 每条消息只被一个消费者消费 每条消息被所有消费者消费
适用场景 普通业务处理 通知所有实例更新本地缓存/配置
消费进度 服务端统一管理(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

内存映射 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的关键。

相关推荐
一点一一2 小时前
nestjs+langchain:Output Parsers+调用本地大模型
人工智能·后端
小谢小哥2 小时前
49-缓存一致性详解
java·后端·架构
Leo8992 小时前
mysql从零单排之快照读与当前读
后端
Leo8992 小时前
mysql从零单排之B+与AHI
后端
hresh2 小时前
两个 Chrome 窗口各 20 多个 tab 后,我把 tab-out 改成了更顺手的 TabNest
前端·chrome·后端
彭于晏Yan2 小时前
Spring Boot 整合 WebSocket 实现单聊+广播
spring boot·后端·websocket
武子康2 小时前
大数据-275 Spark MLib-集成学习:从Bagging到Boosting的群体智慧
大数据·后端·spark
SimonKing2 小时前
国产开源富文本编辑器 wangEditor,本姓编辑器
java·后端·程序员
Moment2 小时前
面试官:LangChain中 TS 和 Python 版本有什么差别,什么时候选TS ❓❓❓
前端·javascript·后端