一、RocketMQ: 发送一条消息经历了什么
环境
NameServer: address 0.0.0.0:9876
Broker启动前配置NameSrv
java
public static void main(String[] args) {
System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, "127.0.0.1:9876");
start(createBrokerController(args));
}
源码环境搭建
Github 下载源码(rocektmq、rocketmq-dashboard)
- github.com/apache/rock... (RocketMQ 核心源码 版本:5.3.4)
- github.com/apache/rock... 控制台 版本:2.1.1-SNAPSHOT)
RocketMQ源码编译JDK版本:JDK24
启动项单独添加额外参数:--add-exports java.base/sun.nio.ch=ALL-UNNAMED

启动顺序:
-
启动namesvr,控制台看IP:PORT打印,最终输出结果为当前namesvr实际地址
The Name Server boot success. serializeType=JSON, address 0.0.0.0:9876 -
启动Broker,启动前需要将namesver配置信息配置给到Broker,Debug阶段可简单做如下配置,以便Broker可识别到当前namesvr
javapublic static void main(String[] args) { System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, "127.0.0.1:9876"); start(createBrokerController(args)); }
RocketMQ Dashboard编译JDK版本: JDK17(过高过低都会导致启动失败),先启动Java服务端应用后,在启动前端应用。
一、Producer 初始化
Producer简单Demo
java
/**
* @author 塔菈
* @date 2025/11/29
* @slogan 少年应有鸿鹄志,当骑骏马踏平川。
*/
public class ProducerDemo {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("DefaultProducerGroupDemo");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
for (int i = 0; i < 5; i++) {
String ctx = "test" + String.valueOf(i);
Message message = new Message("DEFAULT_TOPIC_DEMO_DEBUG",
"DEBUG", String.valueOf(i), JSON.toJSONBytes(ctx));
producer.send(message);
System.out.println("send success" + i);
}
System.out.println("success");
}
}

最终我们可以看到消息如实发送出去,OK,我们一步一步看看Producer是如何进行初始化的!
Producer(DefaultMQProducer)实例构造定义:
java
public DefaultMQProducer(final String producerGroup, RPCHook rpcHook, final List<String> topics,
boolean enableMsgTrace, final String customizedTraceTopic) {
// DefaultProducerGroupDemo(我们构造的ProducerGroup)
// 同一个ProducerGroup在消息发送以及Producer管理上具有逻辑一致性
this.producerGroup = producerGroup;
// null
this.rpcHook = rpcHook;
// null(还未发送消息,此时当前Producer的Topic集合为空)
this.topics = topics;
// false,是否开启采样追踪
this.enableTrace = enableMsgTrace;
// null,自定义消息追踪Topic
this.traceTopic = customizedTraceTopic;
// DefaultMQProducer可以理解为Config配置类,具体的Producer实现均在DefaultMQProducerImpl内部
defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
}
其中比较有意思的点为 DefaultMQProducer 可以简单理解为门面(Prdocuer基础配置),其Producer的具体逻辑功能实现为DefaultMQProducerImpl,并且该类也实现了接口MQProducerInner,在后续的Producer管理内都是通过接口MQProducerInner来统一抽象管理。
其中DefaultMQProducerImpl初始化如下:
java
public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) {
// 将当前DefaultMQProducer门面配置赋值给具体实现Producer功能的Impl
this.defaultMQProducer = defaultMQProducer;
this.rpcHook = rpcHook;
// 构建线程池,用于后续的Producer异步操作
this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue<>(50000);
this.defaultAsyncSenderExecutor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors(),
1000 * 60,
TimeUnit.MILLISECONDS,
this.asyncSenderThreadPoolQueue,
new ThreadFactoryImpl("AsyncSenderExecutor_"));
// 省略掉其他源码,并通过注释简单描述
// semaphore 初始化相关
// 初始化ServiceDetector,用于健康检查
// 初始化故障策略: 生产者用来实现故障自动规避和负载均衡。当Broker出现故障或性能下降时,引导消息发送到更加健康的Broker上
}
通过DefaultMQProducerImpl初始化源代码我们可以了解到,Producer的实际操作类DefaultMQPrdocuerImpl在初始化时,除去字段默认值之外,将Producer的门面DefaultMQProducer配置类赋值给当前Impl具体实现。同时DefaultMQProducerImpl有几个核心字段需要我们了解:
java
// 当前Topic对应Broker内已经生效(已发布)的详细信息,包含当前MessageQueue队列大小,以及每个队列分布在那个Broker上,其主要在两个地方进行Set/update
// 1. 后台任务每30S从Broker获取当前Producer已经注册过的Topic,进行刷新同步到当前Table内
// 2. 对于Topic,首次进行send消息时,会在tryToFindTopicPublishInfo内手动执行调用
private final ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable =
new ConcurrentHashMap<>();
java
// 当前Impl引用了Producer的门面类,用于后续操作时获取除代Producer配置
private final DefaultMQProducer defaultMQProducer;
除去这两个字段外,DefaultMQProducerImpl可先暂时不进行研究,同时对于topicPublishInfoTable字段,我在这里先抛出其存在更新的两个地方(后面梳理源码过程中,也会和这里前后呼应):
- 后台任务每30S从Broker获取当前Producer已经注册过的Topic,进行刷新同步到当前Table内
- 对于Topic,首次进行send消息时,会在tryToFindTopicPublishInfo内手动执行调用
只有这两个行为,才能够触发DefaultMQProducerImpl#topicPublishInfoTable容器更新有效的Topic信息,后续在发送消息时也会和该字段关联。
而这仅仅是构造了DefaultMQProducer以及DefaultMQProducerImpl两个实例,还未进行初始化,而具体Netty初始化以及定时任务/心跳检测都是在初始化模块内进行执行的。
上面对于Producer核心初始化相关的类已经通过源码以及类图解释通读下来,其中Producer的配置nameserver已经配置好了,等Producer启动后就可以从nameserver内获取Topic以及Broker的信息,然后进行消息发送了。
二、Producer 启动
2.1 Producer 初始化
我们直接看DefaultMQProducer#start()函数启动类,这里也是RocketMQ的Producer启动核心类,Producer和Nameserver信息获取以及心跳报活等都是在当前流程内通过设置定时任务开启的。
java
public void start() throws MQClientException {
this.setProducerGroup(withNamespace(this.producerGroup));
// 核心启动类为impl.start9)
this.defaultMQProducerImpl.start();
if (this.produceAccumulator != null) {
this.produceAccumulator.start();
}
// trace相关,不影响当前分析核心流程,暂时忽略掉
// (...)
}
通过看DefaultMQProducer#start()我们可以确定Producer的启动核心为DefaultMQProducerImpl#start(),而当前函数只是作为一个门面Start函数,我们接着看实际的Start函数, 无效代码均已经省略。
java
public void start(final boolean startFactory) throws MQClientException {
// service状态机
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
// 如果是用户主动设置的DEFAULT,则手动更新为PID+毫秒时间戳
// MQClient API实例,后续TCP消息发送都是基于此进行发送,而具体的TCP链接也是在该类模块下实现
// Producer想要消息发送,必须依赖MQClientFactory进行消息发送
// MQClientInstance存储了GroupName和生产者之间的映射关系以及Topic和消息队列的映射关系以及Broker之间的映射关系
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
// 将当前ProducerInner注册到MQClientFactory
// 只有注册了之后,在MQClientFactory内才会对已经注册过的Producer进行初始化
boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
// registerOk判定
// start后,producer则可以开始发送消息
if (startFactory) {
// 连接建立
mQClientFactory.start();
}
// (其他初始化)
// 初始化Topic路由信息
// 策略、负载均衡
this.serviceState = ServiceState.RUNNING;
break;
default:
break;
}
// Broker心跳、Request超时检查
}
通过上面源码分析,我们简单在梳理一下DefaultMQProducerImpl#start启动逻辑:
DefaultMQProducerImpl状态机初始化- 初始化核心Client
MQClientInstance,该类包含了所以Broker、NameServer上运行的实时信息包含Topic、Broker信息、MessageQueue消息队列信息等。算是对TCP和RocketMQ远程服务的抽象类。- 创建
MQClientInstance实例 - 创建
NettyConfig配置类(NameServer TCP Client配置) - 创建TCP事件回调以及TCP消息读取Processor,包装了底层TCP的实现,只需要关注TCP有哪些事情在当前节点可做,并通过指定API实现即可。
- 创建
MQClientAPIImpl,具体Netty客户端的实现类,比如Producer.send的最底层则会由MQClientAPIImpl兜底实现。这里的NettyClient并不会立马启动,而是懒加载机制,等需要用的时候在从缓存表内获取,获取不到在创建一个新的Channel。
- 创建
- 将当前
DefaultMQProducerImpl注册到MQClientInstance.produceTable内,用以定时任务获取Producer内实时信息从远程服务(NameServer)上。 - 核心重点:
MQClientInstance#start()启动函数,该函数启动了Producer比较重要的几个定时任务,以及初始化了NettyClient的启动引导器。- 启动
NettyRemotingClient#start(),初始化NettyClient引导器,并赋值给NettyRemotingClient.bootstrap,用于后续懒加载时使用。 - 启动定时器:
- 启动间隔2分钟获取NameServer地址(NameServer高可用)定时器
- 启动间隔30S获取Topic信息从NameServer的定时器
- 启动间隔30S和AllBroker之间的心跳的定时器
- 启动间隔5S同步Broker当前消费者Offset的定时器
- 启动间隔1分钟消费者线程池动态调整的定时器
- 启动
通过上面的分析,我们已经知道了Producer在初始化过程中引用了几个关键class,以及每个class的定位是什么,这个能够让我们快速方便的理解Producer启动过程。
2.2 RocketMQ状态机
java
public enum ServiceState {
/**
* Service just created,not start
*/
CREATE_JUST,
/**
* Service Running
*/
RUNNING,
/**
* Service shutdown
*/
SHUTDOWN_ALREADY,
/**
* Service Start failure
*/
START_FAILED;
}
上面的为RocketMQ内唯一状态机枚举类型,在Producer内状态机的表现形式如下:
RocketMQ Service初始化状态默认都为Create_JUST,执行start时,会先赋值START_FAILED,如果初始化成功,则赋值RUNNING,否则意味初始化失败。如果监听到SHUTDOWN命令,则状态置换为SHUTDOWN_ALREAY。
三、Producer 消息发送
基于上面几个步骤,我们已经明确知道了RocketMQ是如何初始化相关实例的,但是上面还有一个隐藏的问题,没有写出来,那就是Producer的实际TCP连接是具体在什么时候和NameServer以及Broker建立连接的?对吧。我们只是分析了NettyClient引导器的基础配置信息以及定时器的创建等。但是呢,TCP的实际创建是在哪里创建的呢?这部分我们就是来分析Producer下的TCP Client是在什么时候创建的。
嗯,结论就是发送消息时懒加载创建的,只有当有实际需要用到TCP连接发送消息的时候,才会去NettyRemotingClient.channelTables内根据地址(Key)获取对应的Channel,如果获取不到,则创建一个新的Channel. 我们简单看下具体的实现逻辑。
java
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
// 初始化Producer(此时Topic、Namesrv配置都为空)
DefaultMQProducer producer = new DefaultMQProducer("DefaultProducerGroupDemo");
// 这里不设置,会从环境变量内获取对应配置信息
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
String ctx = "test1";
Message message = new Message("DEFAULT_TOPIC_DEMO_DEBUG",
"DEBUG", "1", JSON.toJSONBytes(ctx));
// 消息发送
producer.send(message);
}
通过上面的Demo,我这里直接通过DefaultMQProducer.send(Message)进行消息发送,并且我当前的Producer没有其他特殊设置(背压、批量发送等),所以其会直接走同步发送消息流程。
我们先看下具体流程图,按照下面的流程图去理解源码将会更顺利一点
消息发送流程图
java
private SendResult sendDefaultImpl(Message msg,final CommunicationMode communicationMode,final SendCallback sendCallback,final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
//(...不必要的省略)
// 获取本次要发送的Topic信息,如果获取不到,则从NameServer上获取
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
//(...省略)
// 最大可重试次数(同步消息发送) 默认发送3次(主动一次+失败重试2次)
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
for (; times < timesTotal; times++) {
// null
String lastBrokerName = null == mq ? null : mq.getBrokerName();
if (times > 0) {
// 非第一次发送消息之外都进行reset,避免消息发送到已经坏了的broker上
resetIndex = true;
}
// 队列选择
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName, resetIndex);
if (mqSelected != null) {
mq = mqSelected;
brokersSent[times] = mq.getBrokerName();
try {
// 当前发送消息时间
beginTimestampPrev = System.currentTimeMillis();
if (times > 0) {
//Reset topic with namespace during resend.
msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()));
}
long costTime = beginTimestampPrev - beginTimestampFirst;
if (timeout < costTime) {
// 超时
callTimeout = true;
break;
}
// costTime已经消耗时间
// curTimeout当前剩余超时时间
long curTimeout = timeout - costTime;
//(...)
// 消息发送
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, curTimeout);
// 根据同步/异步发送return,省略
}
} else {
break;
}
}
// (省略异常判定代码(Broker校活))
throw new MQClientException("No route info of this topic: " + msg.getTopic() + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO),
null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION);
}
上面则是核心的发送消息流程。一个完整的Message经过DefaultMQProducerImpl#send出发最终由NettyRemotingClient实际构造TCP消息发送到Broker上进行消息持久化。具体SendMessageRequest的消息格式,我们在分析完NettyRemotingClient后在来详细分析Metaq的TCP协议。
在第二节我们已经明白在初始化DefaultMQProducerImpl流程内,也初始化了MQClientInstance以及MQClientAPI等相关客户端的类,在构造MQClientAPI的时候其构建了一个Netty的Bootstrap用于客户端请求相关操作,我们看下具体的配置
java
public void start() {
if (this.defaultEventExecutorGroup == null) {
this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
nettyClientConfig.getClientWorkerThreads(),
new ThreadFactoryImpl("NettyClientWorkerThread_"));
}
// 当前Netty Client Bootstrap启动引导器
Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE, false)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis())
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (nettyClientConfig.isUseTLS()) {
if (null != sslContext) {
pipeline.addFirst(defaultEventExecutorGroup, "sslHandler", sslContext.newHandler(ch.alloc()));
LOGGER.info("Prepend SSL handler");
} else {
LOGGER.warn("Connections are insecure as SslContext is null!");
}
}
ch.pipeline().addLast(
nettyClientConfig.isDisableNettyWorkerGroup() ? null : defaultEventExecutorGroup,
new NettyEncoder(), // 出站编码(由其集成的Interface决定)
new NettyDecoder(), // 入站解码(由其集成的Interface决定)
new IdleStateHandler(0, 0, nettyClientConfig.getClientChannelMaxIdleTimeSeconds()),
new NettyConnectManageHandler(),
new NettyClientHandler()); // 入站业务处理
}
});
// (...tcp配置)
nettyEventExecutor.start();
// 启动定时任务扫描REQUEST-RESPONSE中间存在超时的RESPONSE
// 启动定时任务扫描存活的NameServer
}
简单描述下Netty.Bootstrap的基本配置:
- 基于Java NIO的非阻塞I/O模型作为TCP通信模型
- 禁用Nagle算法,减少延迟
- 禁用TCP keep-alive
- 配置超时时间
- Pipline处理器配置: Netty定义TCP层框架,业务逻辑处理由Pipline定制的Handler进行处理(Pipline维护了一个双向链表,入栈从头到尾,出站从尾到头)
- 出站编码(出站头部最后执行,最后一步则是Encode)
- 入站解码(入站头部首先执行,第一步则是Decode)
- TCP连接状态检查
- TCP连接管理器
- NetttyClientHandler业务处理
基于上面配置,我们在MQClientAPI内已经完成了对NettyClient的初始化流程,但是对那个NettyServer建立连接需要再实际使用的时候通过Bootstrap指定目标addr才可确定,具体接口如下,通过预先配置好的bootstrap,具体使用时通过host以及port确定具体目标Server。
java
/**
* Connect a {@link Channel} to the remote peer.
*/
public ChannelFuture connect(String inetHost, int inetPort) {
return connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
}
TCP链接复用
RocketMQ为了提升Producer吞吐量,并非一次请求就创建一个TCP链接,而是通过一个Map<String/*addr*/,Channel/*TCP链接*/>哈希容器来维护每一个Addr对应的TCP链接的。
java
/**
* 创建Netty Channel
**/
private ChannelFuture createChannelAsync(final String addr) throws InterruptedException {
ChannelWrapper cw = this.channelTables.get(addr);
if (cw != null && cw.isOK()) {
return cw.getChannelFuture();
}
if (this.lockChannelTables.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
try {
cw = this.channelTables.get(addr);
if (cw != null) {
if (cw.isOK() || !cw.getChannelFuture().isDone()) {
return cw.getChannelFuture();
} else {
this.channelTables.remove(addr);
}
}
return createChannel(addr).getChannelFuture();
} catch (Exception e) {
LOGGER.error("createChannel: create channel exception", e);
} finally {
this.lockChannelTables.unlock();
}
} else {
LOGGER.warn("createChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS);
}
return null;
}
我们通过上面的代码可以明确了解到,在RocketMQ.Producer内TCP链接的管理方式为通过一个哈希表来维护的,而仅通过一个哈希表来维护也是因为这个场景需要维护的TCP链接并不是很多(Producer本身并不会存在很多)。
这样我们在具体使用的时候,就能够针对某个Producer获取目标对应的Channel(TCP链接)了,并进行对应的TCP写操作到BrokerServer进行持久化。
四、Producer稳定性考虑
4.1 Prdoucer背压机制
RocketMQ在消息发送时会通过检查当前正在发送的消息个数判断当前生产速率是否达到要进行背压。而具体背压则是通过JUC包内的信号量来具体实现。
- 具体配置如下:
com.rocketmq.remoting.clientOnewaySemaphoreValue: 65535
java
try {
// 异步并发限流,rocketmq的背压
// 当前信号量通过配置
acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
} catch (Throwable t) {
// 在指定超时时间内未获取到信号量则针对当前Feature抛出异常
future.completeExceptionally(t);
return future;
}
4.2 Producer三级容错
- 第一级 (
availableFilter) :选择完全健康(不在故障规避列表内)的Broker。这是最优选择。 - 第二级 (
reachableFilter) :如果上一级没有,则选择可到达(Broker进程存活,但可能因网络延迟等被暂时规避)的Broker。 - 第三级 (无筛选) :如果前两级都失败(理论上所有Broker都"不可用"),则强制选择一个。这是保证无论如何都能发出消息的兜底策略,体现了RocketMQ的健壮性设计。