Skip to content

Apache RocketMQ:从路由发现到消息落盘的源码地图

RocketMQ 是一套分布式消息与流处理平台。生产者和消费者不直接维护完整集群拓扑,而是从 NameServer 获取 Topic 路由,再与 Broker 建立数据通道;Broker 同时承担协议处理、消息存储、消费进度、事务检查和高可用协作。RocketMQ 5.x 还加入 Proxy、Controller、认证授权与分层存储等模块,让多协议接入、自动选主和冷热数据分层有了独立边界。

这套框架最关键的结构不是某个单独线程,而是三组分离关系:路由发现与消息传输分离,消息实体与消费索引分离,业务事务状态与消息可见性分离。前两组关系让客户端可以绕过 NameServer 直连 Broker,也让 CommitLog 保持顺序追加;后一组关系通过半消息、Op 消息和事务回查,把一次不确定的二阶段确认变成可补偿的状态机。

下文以固定提交 e348efa 为坐标,覆盖 NameServer、Broker、Java 客户端、Remoting、CommitLog/ConsumeQueue、长轮询、重平衡、事务消息、定时消息与 Controller。Proxy、TieredStore、OpenMessaging、认证授权和容器模块只建立职责地图,不把尚未逐条核对的并行实现并入主链。外部依赖包括 JDK、Maven、操作系统页缓存与磁盘,以及可选的 Controller/DLedger、Proxy 和监控体系。

GitHub 仓库信息

信息内容
项目标题Apache RocketMQ
项目描述Apache RocketMQ 是云原生消息与流处理平台,用于简化事件驱动应用的构建。
GitHub 仓库apache/rocketmq
官网地址rocketmq.apache.org
主要开发语言Java 98.9%,其他语言 1.1%(GitHub 页面核对日期:2026-09-01)
开源许可证Apache-2.0(已与固定提交中的 LICENSE 核对)
最近代码更新2026-08-26(默认分支最新可见提交;GitHub pushed_at API 因匿名限流未取得,核对日期:2026-09-01)

仓库结构与模块职责

RocketMQ 是 Maven 多模块仓库。下面的目录树只保留理解运行时所需的语义骨架,完整的裁剪快照见 structure.txt

text
rocketmq/
├── client/          Java Producer、Consumer、路由缓存与重平衡
├── namesrv/         Topic 路由注册、查询与失活 Broker 清理
├── broker/          Broker 生命周期、请求处理、事务与长轮询
├── store/           CommitLog、ConsumeQueue、IndexFile、HA、TimerStore
├── remoting/        RemotingCommand、Netty 客户端/服务端与请求分派
├── common/          配置、消息模型、协议公共类型与工具
├── controller/      基于一致性日志的 Broker 选主与 SyncStateSet 管理
├── proxy/           gRPC 等接入协议到内部消息能力的代理层
├── tieredstore/     分层存储接口与实现
├── auth/            认证、鉴权与权限模型
├── tools/           mqadmin 等运维命令
├── distribution/    发布包脚本、配置与组装
├── example/         生产、消费、事务等用例
├── test/            端到端与集成测试
├── docs/            仓库内架构、存储、事务和 Controller 设计
├── BUILDING         构建要求与 Maven 命令
└── pom.xml          5.5.1 版本与模块聚合
语义分区代表模块或文件负责什么上下游关系
客户端入口clientDefaultMQProducerImplRebalanceImpl维护路由缓存、选队列、发送、拉取与重平衡从 NameServer 取路由,直接调用 Broker
控制面namesrvRouteInfoManager接收 Broker 注册,维护 Topic、Broker、集群映射Broker 上报;Producer/Consumer 查询
Broker 编排BrokerStartupBrokerController加载配置与元数据,装配处理器、存储和后台服务向下启动 store/remoting,向上注册 NameServer
网络协议remotingNettyRemotingServer编解码 RemotingCommand,按请求码分派业务线程池被 client、namesrv、broker、controller 共同依赖
数据面DefaultMessageStoreCommitLogConsumeQueueStore顺序追加消息,构建消费索引,处理刷盘和副本确认Broker 写入;Pull/Query 读取
可靠性扩展controller、HA、tieredstore选主、复制、冷热数据迁移与 Broker 存储和运维配置协作
接入与治理proxyauthtools多协议入口、权限校验与管理命令位于客户端/运维面与核心 Broker 之间

一个重要的阅读转折在这里:broker 并不等于“存储模块”。Broker 是运行时编排者,真正的数据文件和恢复逻辑集中在 store;反过来,store 不处理 Producer 的网络请求,业务请求先经过 remoting 和 Broker Processor。排查写入失败时,必须沿这三个边界逐层定位。

核心能力是怎样拼出来的

路由发现:用可重建内存换取简单控制面

NameServer 保存 Topic 到队列、Broker 名称到地址、集群到 Broker 的映射。Broker 启动后向各个 NameServer 注册,客户端按 Topic 查询路由;NameServer 实例之间不复制状态,因此每个 Broker 要向所有 NameServer 上报。固定源码中,RouteInfoManager.registerBroker更新这些表,pickupTopicRouteData组装查询结果,scanNotActiveBroker清理失活节点。

收益是 NameServer 很轻,故障后可由 Broker 重新注册恢复;代价是路由存在传播窗口,客户端还要处理本地缓存、定时刷新与发送失败后的重选。

顺序日志与二级索引:写一次,按多种方式读

Broker 把不同 Topic 的消息主体顺序追加到 CommitLog。ConsumeQueue 不复制消息体,只保存逻辑队列条目到 CommitLog 物理位置的映射;IndexFile 再提供按 Key 和时间范围查询的索引。官方存储设计说明了这种“共享日志 + 逻辑队列 + 哈希索引”的组织方式。

写入路径因此可以集中利用顺序 I/O 与页缓存,消费侧则不必扫描整个 CommitLog。相应代价也很具体:消息写入成功和消费索引可见之间存在异步分发边界;磁盘损坏、索引落后、刷盘策略和副本确认不能混成一个“写成功”概念。

消费:Pull 内核、长轮询外观、客户端分配

Push Consumer 的外观容易让人误以为 Broker 主动推送。源码中的主干仍是 Pull:客户端发起拉取;Broker 没有新消息时把请求挂到 PullRequestHoldService;新消息到达或超时后唤醒,再执行一次 Pull。这样避免了空转轮询,又保留了客户端控制消费位点的模型。

同一消费组的队列分配也在客户端完成。RebalanceImpl取得队列集合和 Consumer ID,排序后调用分配策略,再为新增队列创建 ProcessQueue 与 PullRequest。优点是 Broker 不维护一套集中分配状态;代价是成员变化时各客户端需要收敛到同一视图,重平衡期间还要正确处理旧队列的丢弃和位点交接。

事务与定时:借内部 Topic 把复杂状态变成消息

事务消息先保存原 Topic/Queue,再写入 RMQ_SYS_TRANS_HALF_TOPIC,所以业务 Consumer 暂时不可见。Producer 执行本地事务后发送 Commit 或 Rollback;二阶段丢失时,Broker 扫描半消息与 Op 消息并回查 Producer。Commit 并不是原地修改 CommitLog,而是恢复真实 Topic/Queue 后重新走一次普通消息写入。

定时消息使用类似的“先进入内部时间结构,后恢复投递”思路,但实现已不只是旧版延迟级别:TimerMessageStore 组合时间轮、TimerLog 与入队/出队服务。TimerMessageStore 从定时 Topic 的 ConsumeQueue 取消息放入时间轮,到期后再恢复待投递消息。内部 Topic 让主存储模型保持一致,却会增加状态追踪、磁盘占用和问题定位成本。

高可用:ACK 语义由刷盘与副本策略共同决定

CommitLog.handleDiskFlushAndHA把刷盘 Future 和副本确认 Future 合并。同步刷盘、异步刷盘、需要多少个可用副本、是否启用 Controller 模式,会共同影响 Producer 最终收到的状态。

Controller 模式把选主与 SyncStateSet 元数据交给独立一致性组件。官方 Controller 设计明确:Controller 可以独立部署或嵌入 NameServer,Broker 的 ReplicasManager 感知新 Master 并切换角色。这个方案改善自动选主和日志截断,但引入 Controller/DLedger 的部署、监控与故障诊断成本。

源码环境准备与本地运行

工具链与边界

固定提交的 BUILDING要求 JDK 1.8+、Maven 3.0.3+;根 pom.xml声明版本 5.5.1。JDK 8 是最低门槛,不代表所有本地插件、IDE 或部署组合都应继续选用旧 JDK。

本次只进行了固定提交的静态阅读,没有执行 Maven、仓库脚本、NameServer、Broker、Producer、Consumer 或容器。以下命令来自仓库文档和构建文件,是可复核路径,不是亲测记录。

获取同一份源码

前置条件:本机已经安装 Git,当前目录是准备存放源码的父目录。

bash
git clone https://github.com/apache/rocketmq.git
cd rocketmq
git checkout e348efa66b08eb645ee123706ea6492fa9a3ad35
git rev-parse HEAD

最后一条命令输出的提交应以 e348efa 开头,并与文章 frontmatter 的 source_commit 一致。若 checkout 提示找不到对象,先确认克隆不是截断历史的浅克隆,或显式获取该提交。

构建与测试入口

在仓库根目录执行。测试命令来自 BUILDING

bash
mvn test

完整发布包构建路径为:

bash
mvn -Prelease-all -DskipTests clean install -U

成功信号应是 Maven 返回 BUILD SUCCESS,并在各发行模块下产生构建产物。-DskipTests 会跳过测试,不能同时把这次构建描述为“测试通过”。依赖下载失败时,先检查 Maven 仓库、代理和证书;编译错误时,保留第一条失败模块与首个根因,而不是从末尾的 reactor 汇总猜测。

最小运行顺序

仓库 README给出的单机顺序是先 NameServer,再 Broker。下面假设已经得到 5.5.1 发布目录,并进入其中的 bin

bash
nohup sh mqnamesrv &
tail -f ~/logs/rocketmqlogs/namesrv.log

看到 The Name Server boot success 后,再执行:

bash
nohup sh mqbroker -n localhost:9876 &
tail -f ~/logs/rocketmqlogs/broker.log

Broker 日志应出现 boot success,并带 Broker 名称和监听地址。这里的 localhost:9876 只适合客户端与 Broker 都能解析到同一主机的单机环境;容器、虚拟机或跨主机部署要显式检查 brokerIP1、端口映射与 NameServer 可达性。

整体架构地图与功能分布

这张图把控制面和数据面分开:NameServer 返回“去哪一台 Broker”,不转发消息;Controller 决定“哪一个副本是 Master”,也不进入普通消息读写链。Producer/Consumer 获取路由后直接与 Broker 通信。

想理解或排查的功能建议入口继续向下读
Topic 路由不更新namesrv/.../RouteInfoManagerregisterBrokerpickupTopicRouteData、失活清理
Producer 发送失败或重试DefaultMQProducerImplsendDefaultImplsendKernelImplMQFaultStrategy
Broker 收到消息但未成功写入SendMessageProcessorDefaultMessageStoreCommitLog、刷盘/HA 状态
Consumer 拉不到消息PullMessageProcessorDefaultPullMessageResultHandlerPullRequestHoldService
队列分配变化RebalanceImpl分配策略、ProcessQueue、消费位点
事务消息长时间未确认TransactionalMessageServiceImplHalf/Op Queue、回查、EndTransactionProcessor
定时消息未到期或未投递TimerMessageStoreTimerWheel、TimerLog、enqueue/dequeue 服务
主从切换异常controllerReplicasManagerSyncStateSet、MasterEpoch、AutoSwitchHAService

程序入口、初始化与启动链路

RocketMQ 有多个真实运行入口,不能用一个 main 代表全部部署形态。

角色入口初始化重点对外结果
NameServerNamesrvStartup.main配置、Netty server/client、路由处理器、扫描任务监听 9876,接受注册与路由查询
BrokerBrokerStartup.main元数据、MessageStore、Processor、后台服务监听客户端请求并注册到 NameServer
ControllerControllerStartup.main一致性实现、心跳与选主处理器管理 Broker 主节点与 SyncStateSet
ProxyProxy 启动类gRPC/协议服务、路由与消息服务适配提供 5.x 接入协议入口

固定源码里,NamesrvStartup把创建、初始化、关闭钩子和启动串起来;NamesrvController.initialize装配网络与处理器。Broker 入口在 BrokerStartup,而 BrokerController把元数据、存储恢复和基础服务拆成阶段。

这种阶段化初始化便于失败时停止后续组件,也让关闭钩子可以按相反方向释放资源。代价是 BrokerController 体量很大,新增能力容易继续挤入中央编排类;读源码时应按阶段和服务族拆解,不宜从构造函数第一行一路顺读。

核心功能链路矩阵

链路入口关键跨模块交接外部依赖或副作用代表性分支
NameServer/Broker 启动两个 Startup配置 → Controller → Remoting/Store端口、日志、磁盘、路由注册配置无效、恢复失败、端口占用
Producer 发送与落盘DefaultMQProducerImpl.sendclient → remoting → broker → store网络、PageCache、磁盘、副本重试、刷盘超时、ISR 不足
Consumer Pull 长轮询Broker Pull Processorbroker → store → hold service消费位点、挂起请求立即返回、挂起、过滤不匹配
Consumer 重平衡RebalanceImpl.doRebalance路由/成员 → 分配策略 → PullRequestBroker 成员列表、OffsetStore广播/集群、有序锁失败
事务消息sendMessageInTransactionProducer → Half Queue → 回查/EndTransaction本地事务状态、内部 TopicCommit、Rollback、Unknown、超限
定时消息TimerMessageStore定时 Topic → TimerWheel/TimerLog → 普通投递时钟、磁盘与后台线程未到期、到期、恢复扫描
Controller 选主Controller request/heartbeatController → ReplicasManager → HADLedger/Raft、Broker 副本Master 失活、SyncStateSet 收缩

发送:从客户端选队列到 CommitLog

Producer 启动时,DefaultMQProducerImpl.start取得共享 MQClientInstance,注册 Producer,启动客户端工厂并发送心跳。发送入口先查本地 Topic 路由;没有可用路由时触发更新,然后选择 MessageQueue。

sendDefaultImpl负责同步发送的尝试次数、队列选择和故障项更新;sendKernelImpl补齐消息 ID、压缩、Hook 与请求头,再交给 MQClientAPIImpl

Broker 的 SendMessageProcessor区分普通、批量、事务预备和回退消息。普通消息进入 MessageStore.asyncPutMessage,事务预备消息进入事务服务。存储层的 DefaultMessageStore.asyncPutMessage再委托 CommitLog;CommitLog.asyncPutMessage在 Topic/Queue 锁与 PutMessageLock 约束下编码、追加,并等待所需的刷盘与 HA 结果。

第二个认知转折出现在 ACK:Producer 收到成功时,ConsumeQueue 可能仍由 Reput 异步构建。写入可靠性主要看 CommitLog、刷盘和副本条件;“立刻能按 Topic 拉到”还依赖索引分发进度。两者要分别观测。

Pull:没消息时,请求去哪了

PullMessageProcessor先检查 Broker 读权限、订阅组、Topic、队列和过滤条件,再调用 getMessageAsync。找到消息就直接返回;未找到且请求允许 suspend 时,结果处理器把 PullRequest 放入 Hold Service。

挂起动作位于 DefaultPullMessageResultHandler,请求容器与唤醒逻辑位于 PullRequestHoldService。唤醒后不是复用旧结果,而是通过 executeRequestWhenWakeup重新跑一次 Pull,避免返回挂起期间已经失效的判断。

重平衡:队列分配是 Pull 的起点

doRebalance遍历订阅 Topic。集群消费路径的 rebalanceByTopic取得 Topic 队列和消费组成员,排序后调用 AllocateMessageQueueStrategy.allocate。每个客户端使用相同输入和策略,独立算出自己的队列集合。

updateProcessQueueTableInRebalance处理差集:失去的队列标记 dropped 并尝试移除;新增队列计算消费位点,创建 ProcessQueue 和 PullRequest,最后派发到 Pull 服务。因此“Consumer 已启动”还不等于“已经开始拉某个队列”,真正的起点是重平衡产出的 PullRequest。

广播模式绕过消费组分配,把 Topic 队列交给每个实例;集群模式才按成员分摊。顺序消费还会增加 Broker 侧队列锁,拿锁失败的新增队列不会立即进入 Pull。

事务:半消息怎样变成可见消息

事务预备消息进入 TransactionalMessageService.asyncPrepareMessage,真实 Topic/Queue 被保存到属性,当前 Topic 改为内部 Half Topic。Broker 周期性执行 TransactionalMessageServiceImpl.check,对照 Half Queue 与 Op Queue,判断是否需要回查 Producer。

Producer 的二阶段请求由 EndTransactionProcessor处理。Commit 读取预备消息,恢复真实 Topic/Queue,重新写入 MessageStore,再写 Op 标记;Rollback 不删除 CommitLog 中的原记录,只写已处理标记。Unknown 或二阶段丢失会留给后续回查。

这是一种最终一致性工具,不是跨数据库和 Broker 的全局 ACID 事务。业务侧仍要保存可重复查询的本地事务结果,回查逻辑也必须幂等;否则 Broker 能重试询问,却无法替业务系统判断事实。

定时消息与 Controller:两条补充链路

TimerMessageStore.start恢复状态并启动 enqueue/dequeue 等服务。enqueue 从定时 Topic 的 ConsumeQueue 读取消息,写入 TimerLog/TimerWheel;dequeue 在到期槽位恢复消息并重新投递。排查“定时消息不见了”时,要同时看源 CommitLog、定时队列消费位点、时间轮槽位和最终重写结果。

Controller 路径与普通消息链没有共享请求入口。Controller 通过心跳感知 Broker,Master 不可用时从 SyncStateSet 中选择候选,用一致性日志提交元数据;Broker 的 ReplicasManager 拉取变化,再切换 BrokerRole 与 HA 状态。只部署 Controller 而没有让 Broker 进入相应模式,不会自动获得这套选主语义。

分支、失败、降级与扩展链路

分流条件进入的路径返回或恢复方式首要观测点
本地无 Topic 路由向 NameServer 更新路由仍无路由则发送失败NameServer 路由表、Broker 注册、客户端缓存
同步发送失败重新选队列并按配置重试达到次数后抛异常每次尝试的 Broker、响应码、延迟故障项
Broker 拒绝写入handlePutMessageResult 映射状态PageCache busy、刷盘/副本超时等响应Broker 写线程池、磁盘、HA/ISR
Pull 无新消息立即返回或挂入 Hold Service新消息/超时唤醒后重拉挂起表、到达通知、消费位点
过滤不匹配ConsumeQueue 预过滤后可能继续校验正文返回无匹配消息或推进位点Tag Hash、SQL92 表达式、订阅版本
消费组成员变化客户端重平衡移除旧队列、创建新 PullRequestConsumer ID 列表、分配策略、队列锁
事务状态 Unknown保持 Half 消息待确认周期回查;超限后按配置处理Half/Op 位点、Producer channel、本地事务表
定时消息未到期保留在时间轮/TimerLog到期后重新投递Broker 时钟、Timer checkpoint、enqueue/dequeue lag
Master 失活Controller 选主提升 SyncStateSet 内候选并通知副本组Controller leader、SyncStateSet、MasterEpoch

SendMessageProcessor.handlePutMessageResult把存储状态转换为协议响应。出现 FLUSH_DISK_TIMEOUT 或副本相关超时时,消息可能已经进入 PageCache 或部分副本,不能简单按“失败即不存在”处理;Producer 重试需要业务键与幂等消费配合。

核心思想、可学习设计与不足

共享日志与派生索引。 CommitLog 承担消息事实,ConsumeQueue 和 IndexFile 为不同读法服务。这个边界适合高写入量和消息回溯,也要求恢复流程能从日志重建派生状态。学习重点在 DefaultMessageStore.load/start、Reput 分发和索引 checkpoint,而不是只看 MappedByteBuffer

请求码驱动的协议分派。 Remoting 层把连接、编解码、请求关联和业务线程池分开,Broker 再按请求类型配置处理器。扩展管理命令或协议能力时,可以复用传输骨架;但请求码、Header、响应码和版本兼容会形成横跨 remoting、client、broker、tools 的修改面。

内部 Topic 复用主存储。 事务、重试、死信和定时功能尽量复用消息存储与消费机制,减少完全独立的状态系统。好处是恢复和运维工具可以共享一部分能力;不足是系统 Topic 数量、重写消息、内部位点与后台扫描增加了排障复杂度。

客户端承担智能。 路由缓存、队列选择、延迟故障规避和重平衡都在客户端。这让 Broker 的集中协调压力更小,也使多语言客户端必须正确实现相同协议语义。升级客户端时,不能只做 API 兼容测试,还要覆盖路由刷新、重平衡、事务回查与错误码处理。

中央编排类的复杂度。 BrokerController 汇集大量组件和生命周期阶段,源码入口清楚,但改动影响面容易扩大。新增 Broker 级能力时,优先寻找已有 Manager/Service/Processor 边界,并把启动、停止、恢复、配置和指标一起设计;只把一个新字段塞进 Controller,通常会留下生命周期缺口。

应用场景与同类产品对比

RocketMQ 更适合需要 Java 生态、队列级顺序、事务消息、定时消息、消息回溯和大堆积处理,并愿意直接运维 Broker/NameServer 的团队。若主要目标是日志流平台、跨地域多租户,或复杂 AMQP 路由,应把同类方案放在相同维度比较。

维度RocketMQ 5.xApache Kafka 4.xApache Pulsar 4.1RabbitMQ 4.3
核心定位消息与流处理平台,业务消息语义较丰富分区提交日志与事件流平台计算/接入 Broker 与 BookKeeper 存储分离的多租户消息平台AMQP 路由与队列消息中间件
元数据/架构NameServer 路由;可选 Controller 选主KRaft controller 管理元数据,数据按 partition logBroker、BookKeeper、metadata store 分层Exchange/Queue;Quorum Queue 用 Raft 复制
顺序边界单 MessageQueue 内顺序单 Partition 内顺序单 Topic/Partition 与订阅语义相关单 Queue 有序受重投、优先级和多消费者影响
事务与延迟Half/Op 回查事务;TimerStore 定时消息幂等 Producer 与跨分区事务原生事务与延迟投递能力Publisher Confirm、事务 Channel;延迟常结合 TTL/DLX 或插件
扩展方式Broker/队列扩容,客户端感知路由增加 Broker 与 Partition,生态处理链成熟Broker 和 Bookie 可分别扩展,支持跨集群复制集群与 Queue 类型选择,路由拓扑灵活
运维代价NameServer + Broker;Controller/Proxy 为可选增量Broker + KRaft controller,分区治理是重点Broker + BookKeeper + metadata store,组件最多入门拓扑较直观;大量 Queue 与 Quorum 成员仍需治理
优先考虑订单、交易事件、定时任务、削峰与回溯日志、CDC、事件流处理与数据管道多租户、跨地域、存算独立扩展复杂路由、传统任务队列、AMQP 生态

Kafka 官方设计文档强调分区日志、页缓存、批处理、复制与事务;Pulsar 官方架构说明明确 Broker、BookKeeper 和元数据存储的分层;RabbitMQ 官方Quorum Queue 文档说明其基于 Raft 的持久复制队列及适用限制。表格比较的是开源项目当前公开架构,不包含云厂商托管版的专有能力与 SLA。

采用判断可以更直接:事件流和数据平台先比较 Kafka;需要存算分离、多租户和跨地域复制时重点评估 Pulsar;复杂 AMQP 路由和短任务队列优先看 RabbitMQ;业务消息同时依赖事务回查、定时投递和 Java 客户端语义时,RocketMQ 的模型通常更贴近问题。

构建、安装、部署与调试问题

以下问题来自固定提交文档、源码错误分支与历史社区文章的交叉核对,本次没有实际启动环境。

ROCKETMQ_HOME 未设置

现象: NameServer 或 Broker 启动阶段直接退出。

原因: 启动脚本、日志配置或运行时配置找不到发行目录。源码 checkout 与可运行发布目录也可能被混用。

处理: 确认当前进入的是构建后的发行目录;设置 ROCKETMQ_HOME 指向该目录;检查 confbinlib 是否齐全。重新启动后,以 NameServer/Broker 的 boot success 日志为验证信号。

NameServer 成功,Broker 却注册不上

现象: NameServer 监听 9876,但客户端没有 Topic 路由,Broker 日志持续出现连接或注册失败。

原因: -n 地址只在 Broker 所在网络命名空间内解析;容器里的 localhost 指向容器自身。防火墙、端口映射或错误的 brokerIP1 也会让注册地址不可达。

处理: 从 Broker 运行环境验证 NameServer 地址;从客户端验证 Broker 注册出去的 10911 地址;核对 RouteInfoManager 是否出现该 Broker。验证目标不是“端口能连”,而是 Topic 路由中出现客户端可达的 Broker 地址。

Maven 构建在某个模块失败

现象: reactor 末尾显示多个 SKIPPED,容易误以为所有模块都坏了。

原因: 多模块构建会在首个失败点停止后续依赖模块。

处理: 回到日志中的第一个 FAILURE 模块和第一条根因;确认 JDK/Maven 版本与依赖仓库;修复后用 Maven 的 resume 提示或重新执行原命令。只有实际运行 mvn test 且成功,才能记录测试通过。

Producer 超时后出现重复消息

现象: Producer 收到刷盘或副本超时,重试后 Consumer 看到相同业务事件多次。

原因: 超时代表 ACK 条件没有在期限内完成,不保证原写入完全不存在。重试可能再次追加消息。

处理: 使用稳定业务键,在消费侧做幂等;同时核对刷盘模式、磁盘延迟、可用副本和 Broker 返回码。验证时既看 Producer 结果,也按业务键查询 Broker 中实际消息数。

Consumer 在线但没有消费

现象: 心跳正常,Topic 也存在,消费速率仍为零。

原因: 当前实例可能没有分到队列;消费位点位于队尾;订阅表达式或版本不一致;有序消费的队列锁未取得;PullRequest 没有被创建或已 dropped。

处理: 依次检查消费组成员列表、队列数与分配策略、processQueueTable、下一拉取位点、订阅数据和 Broker 侧队列锁。看到成员在线只证明注册成功,不证明当前实例拥有队列。

事务消息持续回查

现象: Producer 不断收到 checkLocalTransaction,业务消息迟迟不可见。

原因: 二阶段状态未送达、Producer Group 没有可用 Channel,或本地事务状态一直返回 Unknown。

处理: 本地事务表使用事务 ID 或业务键保存最终状态;确保同一 Producer Group 的实例可以回答回查;检查 Half/Op Queue 位点和最大回查配置。验证信号是回查后产生 Commit/终止状态,并且真实 Topic 出现最终消息,而不是只看回调执行日志。

定时消息延迟明显

现象: 到期时间已过,消息仍未重新投递。

原因: Broker 时钟偏差、Timer enqueue/dequeue 积压、磁盘压力或恢复 checkpoint 落后。

处理: 先同步主机时钟,再检查 Timer 服务线程、队列位点、TimerLog/TimerWheel checkpoint 与普通写入链路。最终验证应同时满足“定时内部位点推进”和“目标 Topic 可见”。

总结与后续阅读路径

RocketMQ 的主线可以压缩为一句话:NameServer 告诉客户端去哪,Broker 用 Remoting 接住请求,CommitLog 保存消息事实,ConsumeQueue/IndexFile提供读取视图,客户端重平衡决定谁来拉,内部 Topic 和后台服务再承载事务、定时与重试等高级语义。

按目标继续读源码会比顺着目录翻更有效:扩展发送链从 DefaultMQProducerImpl、请求头和 SendMessageProcessor 进入;排查消息不可见从 CommitLog 写入结果、Reput 位点和 ConsumeQueue 进入;排查消费停滞从 RebalanceImpl、PullRequest 和消费位点进入;处理高可用则从 Controller 元数据、ReplicasManager 和 HA ACK 条件进入。

参考资料

文章结论绑定 source commit;动态信息以核对日期为准。