在分布式消息中间件领域,RocketMQ以其高吞吐量、低延迟、高可靠性的特性被广泛应用。其核心能力的支撑,依赖于消息发送、存储、消费等模块的精妙设计。本文将聚焦 RocketMQ 最核心的“消息发送”与“消息存储”模块,通过拆解核心流程、剖析关键源码,揭开其底层实现的神秘面纱。
一、RocketMQ 整体架构铺垫
在深入模块细节前,先明确 RocketMQ 的核心角色与整体流程,为后续源码解析建立认知基础。RocketMQ 采用“生产者- Broker- 消费者”的经典架构,核心角色包括:
-
Producer(生产者):负责消息的构建与发送,支持同步、异步、单向等多种发送模式。
-
Broker:消息中间件的核心节点,承担消息存储、转发、负载均衡等核心职责,是生产者与消费者的桥梁。
-
NameServer:作为注册中心,维护 Broker 节点信息与 Topic 路由元数据,为生产者提供路由发现服务。
-
Consumer(消费者):负责从 Broker 拉取或接收消息并进行业务处理。
消息的核心流转路径为:生产者通过 NameServer 获取 Topic 对应 Broker 地址 → 生产者将消息发送至目标 Broker → Broker 将消息持久化存储 → 消费者从 Broker 获取消息并消费。下文将围绕“发送”与“存储”两个关键环节展开。
二、消息发送模块:从构建到投递的全流程
消息发送是 RocketMQ 消息流转的起点,其核心目标是“高效、可靠地将消息投递至 Broker”。生产者在发送消息前需完成初始化配置,发送过程中需解决路由发现、负载均衡、故障重试等关键问题。
2.1 核心类与初始化流程
消息发送的核心入口类是 DefaultMQProducer,其初始化过程是发送能力的基础。核心流程包括:设置生产者组、指定 NameServer 地址、初始化 Netty 客户端、启动实例。
关键源码片段(DefaultMQProducerImpl#start):
public void start() throws MQClientException {
// 1. 检查生产者状态,避免重复启动
if (this.serviceState != ServiceState.CREATE_JUST) {
throw new MQClientException("The producer service state not OK, maybe started once", null);
}
// 2. 初始化 MQ 客户端实例(核心,封装了 NameServer 交互、路由管理等能力)
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
// 3. 注册生产者到客户端实例
boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
if (!registerOK) {
throw new MQClientException("Register producer to MQClientFactory failed", null);
}
// 4. 启动客户端实例(包含 Netty 客户端启动、定时任务启动等)
mQClientFactory.start();
// 5. 更新生产者状态为启动成功
this.serviceState = ServiceState.RUNNING;
}
初始化的核心是创建 MQClientInstance 实例,该类是生产者与 NameServer、Broker 交互的核心载体,内部维护了路由信息缓存、Netty 通信客户端、定时任务(如路由更新)等关键组件。
2.2 路由发现:找到消息该发往哪里
生产者发送消息前,必须明确目标 Topic 对应的 Broker 节点(即路由信息),这一过程由“路由发现”机制完成。路由信息的核心是 TopicRouteData,包含 Topic 对应的队列信息、Broker 地址等。
路由发现的核心流程:
-
生产者首次发送消息时,从本地缓存查询路由信息,若缓存为空则触发路由拉取。
-
通过 NameServer 的
getRouteInfoByTopic接口获取 Topic 路由数据。 -
将拉取到的路由信息缓存至本地,同时 MQClientInstance 会启动定时任务(默认每 30 秒)更新路由信息,保证路由时效性。
关键源码片段(MQClientInstance#updateTopicRouteInfoFromNameServer):
public boolean updateTopicRouteInfoFromNameServer(final String topic) {
// 1. 从 NameServer 拉取 Topic 路由数据
TopicRouteData topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
if (topicRouteData != null) {
// 2. 解析路由数据,更新本地缓存(包含 Broker 地址表、Topic 路由表)
TopicRouteData old = this.topicRouteTable.get(topic);
boolean changed = topicRouteDataIsChange(old, topicRouteData);
if (!changed) {
return false;
}
// 3. 更新本地路由缓存
this.topicRouteTable.put(topic, topicRouteData);
// 4. 更新 Broker 地址缓存
this.brokerAddrTable.putAll(this.buildBrokerAddrTable(topicRouteData));
return true;
}
return false;
}
2.3 负载均衡:选择最优队列
RocketMQ 的 Topic 由多个队列(MessageQueue)组成,分布在不同 Broker 上。生产者发送消息时,需通过负载均衡算法选择一个队列,以实现消息在队列间的均匀分布,提升系统吞吐量。
默认负载均衡算法为 AllocateMessageQueueAveragely(平均分配),核心逻辑是“生产者 ID 哈希取模 + 队列列表均分”。此外,还支持一致性哈希、按机房分配等算法。
关键源码片段(AllocateMessageQueueAveragely#allocate):
public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll, List<String> cidAll) {
// 1. 参数校验
if (currentCID == null || currentCID.length() <= 0) {
throw new IllegalArgumentException("currentCID is empty");
}
if (mqAll == null || mqAll.isEmpty()) {
throw new IllegalArgumentException("mqAll is empty");
}
if (cidAll == null || cidAll.isEmpty()) {
throw new IllegalArgumentException("cidAll is empty");
}
List<MessageQueue> result = new ArrayList<>();
// 2. 找到当前生产者在消费者组中的索引
int index = cidAll.indexOf(currentCID);
if (index < 0) {
return result;
}
// 3. 平均分配队列:index 为起始位置,步长为消费者数量
int mod = mqAll.size() % cidAll.size();
int averageSize = mqAll.size() / cidAll.size();
int startIndex = index * averageSize + Math.min(index, mod);
int range = averageSize + (index < mod ? 1 : 0);
for (int i = 0; i < range; i++) {
result.add(mqAll.get((startIndex + i) % mqAll.size()));
}
return result;
}
2.4 消息发送:同步/异步的核心逻辑
根据业务需求,RocketMQ 支持同步发送、异步发送、单向发送三种模式,核心差异在于是否等待 Broker 响应及是否处理响应结果。
以最常用的同步发送为例,核心流程为:
-
构建消息(设置 Topic、Tag、Body 等),并对消息进行合法性校验。
-
通过负载均衡算法选择目标 MessageQueue。
-
获取目标队列对应的 Broker 地址,构建发送请求(SendMessageRequestHeader)。
-
通过 Netty 客户端将消息发送至 Broker,并阻塞等待响应。
-
接收 Broker 响应,判断发送结果,若失败则触发重试机制。
关键源码片段(DefaultMQProducerImpl#sendDefaultImpl):
private SendResult sendDefaultImpl(Message msg, CommunicationMode communicationMode, SendCallback sendCallback, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
// 1. 消息校验(Topic、Body 等非空校验)
Validators.checkMessage(msg, this.defaultMQProducer);
// 2. 构建发送上下文
SendMessageContext context = new SendMessageContext();
// ... 上下文初始化逻辑 ...
// 3. 路由查找与负载均衡,选择目标队列
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
MessageQueue mq = null;
SendResult sendResult = null;
int timesTotal = this.defaultMQProducer.getRetryTimesWhenSendFailed() + 1;
int times = 0;
// 4. 重试机制:发送失败时重新选择队列重试
for (; times < timesTotal; times++) {
// 负载均衡选择队列
mq = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
try {
// 5. 发送消息核心逻辑
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout);
// 发送成功则跳出重试循环
break;
} catch (Exception e) {
// 6. 处理异常,准备重试
// ... 异常处理与重试判断逻辑 ...
}
}
return sendResult;
}
异步发送的核心差异在于:通过 Netty 发送消息后不阻塞等待,而是注册回调函数(SendCallback),当收到 Broker 响应时由 Netty 线程触发回调,实现非阻塞通信。
三、消息存储模块:高可靠的基石
消息存储是 RocketMQ 高可靠性的核心保障,其核心目标是“安全持久化消息、快速检索消息”。RocketMQ 采用“本地文件 + 内存映射”的存储方案,通过精巧的文件结构设计,平衡了存储性能与可靠性。
3.1 核心存储结构:三大文件体系
RocketMQ 的消息存储依赖三大核心文件体系,分别承担消息存储、索引构建、消费位点记录的职责:
-
CommitLog(消息主文件):所有消息按发送顺序统一写入 CommitLog,是消息的最终持久化载体。文件大小固定为 1G,文件名以起始偏移量命名(如 00000000000000000000.log),方便快速定位。
-
ConsumeQueue(消息消费队列):为每个 Topic 的每个队列单独构建的索引文件,存储“CommitLog 偏移量 + 消息长度 + 消息Tag 哈希值”等核心信息,用于消费者快速定位消息在 CommitLog 中的位置。
-
IndexFile(消息索引文件):基于消息 Key 或 UniqKey 构建的哈希索引文件,用于通过 Key 快速查询消息,支持“按 Key 查消息”的业务场景。
三者的关系:消息写入时,先写入 CommitLog,再异步构建 ConsumeQueue 和 IndexFile;消费者拉取消息时,先通过 ConsumeQueue 定位到 CommitLog 的偏移量,再从 CommitLog 中读取完整消息。
3.2 消息写入流程:顺序写 + 刷盘策略
CommitLog 的写入采用“顺序写”策略,因为顺序写磁盘的性能远高于随机写(机械硬盘顺序写速度可达百 MB/s 级别)。同时,为提升写入性能,RocketMQ 引入了“内存映射(MappedByteBuffer)”和“刷盘策略”机制。
消息写入 CommitLog 的核心流程:
-
消息到达 Broker 后,先写入内存中的
TransientStorePool(临时存储池,堆外内存)。 -
从 TransientStorePool 复制到 CommitLog 对应的内存映射区(MappedByteBuffer)。
-
根据配置的刷盘策略(同步刷盘/异步刷盘),将内存映射区的数据刷写到磁盘,完成持久化。
关键源码片段(CommitLog#putMessage):
public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
// 1. 消息校验与预处理(设置消息 ID、存储时间等)
msg.setStoreTimestamp(System.currentTimeMillis());
msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
// 2. 获取当前可写入的 CommitLog 文件(若当前文件写满则创建新文件)
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
// 3. 写入消息到 MappedFile(先写入内存映射区)
PutMessageResult result = mappedFile.appendMessage(msg, this.appendMessageCallback);
// 4. 处理写入结果,触发刷盘和主从同步
switch (result.getPutMessageStatus()) {
case PUT_OK:
// 5. 根据刷盘策略触发刷盘
this.handleDiskFlush(mappedFile, msg);
// 6. 触发主从同步(若为 Master 节点)
this.handleHA(mappedFile, msg);
break;
// ... 其他状态处理 ...
}
return result;
}
刷盘策略是平衡性能与可靠性的关键:
-
同步刷盘:消息写入内存后,等待刷盘完成再返回成功,可靠性最高,但性能受磁盘 IO 限制。
-
异步刷盘:消息写入内存后立即返回成功,由后台线程定时(默认 500ms)或当内存数据达到阈值(默认 4K)时批量刷盘,性能更高,但存在极端情况下(如 Broker 宕机)消息丢失的风险。
3.3 消息读取流程:索引定位 + 数据读取
消费者拉取消息时,并非直接遍历 CommitLog,而是通过 ConsumeQueue 快速定位,核心流程为:
-
消费者根据 Topic 和队列,找到对应的 ConsumeQueue 文件。
-
根据消费位点(Offset)从 ConsumeQueue 中读取对应的索引条目,获取消息在 CommitLog 中的起始偏移量(phyOffset)和消息长度(msgSize)。
-
根据 phyOffset 定位到对应的 CommitLog 文件,从该偏移量处读取长度为 msgSize 的数据,即为完整消息。
关键源码片段(DefaultMessageStore#getMessage):
public GetMessageResult getMessage(String group, String topic, int queueId, long offset, int maxMsgNums, MessageFilter messageFilter) {
// 1. 获取 Topic 对应队列的 ConsumeQueue
ConsumeQueue consumeQueue = this.findConsumeQueue(topic, queueId);
if (consumeQueue == null) {
return new GetMessageResult(GetMessageStatus.NO_MATCHED_LOGIC_QUEUE);
}
// 2. 从 ConsumeQueue 中获取指定偏移量的索引条目
SelectMappedBufferResult bufferResult = consumeQueue.getIndexBuffer(offset);
if (bufferResult == null) {
return new GetMessageResult(GetMessageStatus.OFFSET_FOUND_NULL);
}
GetMessageResult result = new GetMessageResult();
try {
// 3. 遍历索引条目,从 CommitLog 中读取消息
for (int i = 0; i < bufferResult.getSize() && i < maxMsgNums; ) {
// 解析索引条目:phyOffset(CommitLog 偏移量)、msgSize(消息长度)
long phyOffset = bufferResult.getByteBuffer().getLong();
int msgSize = bufferResult.getByteBuffer().getInt();
// 4. 从 CommitLog 中读取完整消息
SelectMappedBufferResult msgBufferResult = this.commitLog.getMessage(phyOffset, msgSize);
if (msgBufferResult != null) {
result.addMessage(msgBufferResult);
}
i += ConsumeQueue.CQ_STORE_UNIT_SIZE; // 每个索引条目占 20 字节
}
} finally {
bufferResult.release();
}
return result;
}
3.4 高可靠性保障:主从复制与数据恢复
RocketMQ 通过“主从复制 + 数据恢复”机制保障消息不丢失:
-
主从复制:Broker 支持主从架构,Master 节点接收消息并写入 CommitLog 后,会将消息同步或异步复制到 Slave 节点。当 Master 节点故障时,Slave 节点可切换为 Master,避免消息丢失。
-
数据恢复:Broker 启动时,会扫描 CommitLog 文件,通过“检查消息魔数、CRC 校验”等方式验证消息完整性,同时根据 ConsumeQueue 与 CommitLog 的一致性校验,修复异常数据,确保启动后数据可靠。
四、核心设计思想总结
通过对消息发送与存储模块的源码解析,可提炼出 RocketMQ 核心设计思想,这些思想是其高性能、高可靠的关键:
-
分层解耦:将路由发现、负载均衡、消息发送等职责拆分到不同类(如 MQClientInstance、DefaultMQProducerImpl),降低模块耦合,提升可维护性。
-
顺序写优化:CommitLog 采用顺序写磁盘策略,结合内存映射,最大化磁盘 IO 性能。
-
索引优化:通过 ConsumeQueue 构建 Topic-Queue 级别的索引,通过 IndexFile 构建 Key 级别的索引,实现“快速定位”与“快速查询”的平衡。
-
可配置的可靠性策略:支持同步/异步刷盘、同步/异步主从复制,允许业务根据自身需求在性能与可靠性之间做权衡。
-
重试与容错:消息发送失败自动重试、Broker 主从切换、启动数据恢复等机制,提升系统容错能力。
五、结语
消息发送与存储模块是 RocketMQ 的“心脏”与“基石”,其底层实现融合了网络通信、磁盘 IO、数据结构等多方面的优化技巧。深入理解这些模块的源码逻辑,不仅能帮助我们更好地使用 RocketMQ(如根据业务场景选择合适的发送模式、刷盘策略),更能为我们在分布式系统设计中提供“性能与可靠性平衡”的思路借鉴。
后续可进一步探索消息消费、主从复制、事务消息等模块的源码实现,全面掌握 RocketMQ 的核心技术体系。

1245

被折叠的 条评论
为什么被折叠?



