文章篇幅较长,阅读体验不太友好,建议阅读前先按照文章的前面讲demo运行起来,以更好的跟进代码
启动rocketmq
我们这里使用4.8.0的code包,rocketmq项目地址
我们下载源码zip
然后,导入idea如下:
配上namesrv的环境变量,该目录下存放logback_broker.xml以及lockback_namesrv.xml这两个文件,启动的时候需要
然后把broker的配置也加上就可以启动namesrv和broker了。
然后启动消费者,org.apache.rocketmq.example.quickstart.Consumer消费者快速启动类,
在代码里加上如下:
consumer.setNamesrvAddr("localhost:9876");
然后启动:
接着在同一个包下面的org.apache.rocketmq.example.quickstart.Producer生产者类也加上上面代码注册中心地址然后启动:
消息发送成功,并且在消费者这里也打印了消息接受到了的情况:
到此,环境就搭建成功了,就可以继续我们的主题从代码看rocketmq的消息发送流程了。
RockeqMq消息发送流程debug
要debug发送流程,首先要启动我们的生产者
启动
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("localhost:9876");
/*
* 生产者启动
*/
producer.start();
for (int i = 0; i < 10; i++) {
try {
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
producer.shutdown();
}
}
我们随着代码一路来到
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#start(boolean)
这个方法
public void start(final boolean startFactory) throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST://第一次创建,走这个分支
this.serviceState = ServiceState.START_FAILED;//表示已经启动过
this.checkConfig();//检查producerGroup是否为空
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
this.defaultMQProducer.changeInstanceNameToPID();
}
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
//创建出mQClientFactory对象实例,启动一些服务都封装在这个对象里面
boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
if (!registerOK) {
this.serviceState = ServiceState.CREATE_JUST;
throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
+ "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
null);
}
this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());
if (startFactory) {
mQClientFactory.start();//启动
}
log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
this.defaultMQProducer.isSendMessageWithVIPChannel());
this.serviceState = ServiceState.RUNNING;
break;
case RUNNING:
case START_FAILED:
case SHUTDOWN_ALREADY:
throw new MQClientException("The producer service state not OK, maybe started once, "
+ this.serviceState
+ FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
null);
default:
break;
}
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
this.timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
RequestFutureTable.scanExpiredRequest();
} catch (Throwable e) {
log.error("scan RequestFutureTable exception", e);
}
}
}, 1000 * 3, 1000);
}
mQClientFactory.start()重点在这个start方法里面,我们跟进去:
来到这个方法org.apache.rocketmq.client.impl.factory.MQClientInstance#start
我们发现,这里启动了client也就是netty的客户端启动,然后启动了一些startScheduledTask,这个我们之前分析过顺藤摸瓜RocketMQ之整体架构以及执行流程,然后启动了pullMessageService,这个地方是生产者为什么要启动拉取消息的服务呢?
留个悬念,接下来,启动了rebalanceService服务,这个rebalanceService是消费者的负载均衡服务,也就是消费者消费的queue是需要负载均衡在消费者上的,那么问题来了,生产者这里为什么要启动这个服务呢?
这最重要的就是这个启动netty客户端服务this.mQClientAPIImpl.start();然后我们就可以调用发送消息了。
public void start() throws MQClientException {
synchronized (this) {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
// If not specified,looking address from name server
if (null == this.clientConfig.getNamesrvAddr()) {
this.mQClientAPIImpl.fetchNameServerAddr();
}
// Start request-response channel
this.mQClientAPIImpl.start();
// Start various schedule tasks
this.startScheduledTask();
// Start pull service
this.pullMessageService.start();
// Start rebalance service
this.rebalanceService.start();
// Start push service
this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
log.info("the client factory [{}] start OK", this.clientId);
this.serviceState = ServiceState.RUNNING;
break;
case START_FAILED:
throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
default:
break;
}
}
}
发送消息
在讲解此节之前我们先梳理一下他的整个发送流程,希望读者在看下面代码的时候能根据发送流程带着自己的想象去看他的整个发送流程:
1.send发送
2.从namesrv找到需要发送的路由信息
3.根据路由信息以及负载算法选择其中的一个队列
4.调用remoting模块将消息发送到该队列上
5.broker接受到消息后刷盘
我们回到这个生产者类org.apache.rocketmq.example.quickstart.Producer
SendResult sendResult = producer.send(msg);调用的这个send方法来发送消息的我们跟进去,
这里校验了msg信息,主要是检查里面一些必要值是否为空,然后看这个topic是否是重试的,是否发送到了死信队列里面,来完善一下topic信息接着调用send方法,跟进去
@Override
public SendResult send(
Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
Validators.checkMessage(msg, this);
msg.setTopic(withNamespace(msg.getTopic()));
return this.defaultMQProducerImpl.send(msg);
}
发送超时时间是3s
public SendResult send(
Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
return send(msg, this.defaultMQProducer.getSendMsgTimeout());
}
默认同步发送
public SendResult send(Message msg,
long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
}
接着我们来到这个方法org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl
我们来看看这个时候msg成什么样了,如下图,给了topic,tags,以及body信息,和我们所猜测的基本差不多,接下来应该是从namesrv根据topic信息找到路由信息了。我们接着往下跟。
这个方法我就不把代码贴出来了,我贴一下debug信息,具体读者在源码里面跟着debug一下。
到这里定义了一些时间戳开始时间
接着来到TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());这个方法,看方法名可以看到是尝试获取topic信息,按照我们的想象这里应该是从namesrv根据topic信息找到路由信息,我们跟进去
可以看到是从一个集合里面拿的,ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable
这个集合,而且发现集合里面有两个topic,而第一个正是我们发送的topic,那么第二个是什么呢 TBW102,因为我们开启了autoCreateTopicEnable=true,而rocketmq是利用这个tbw102帮我们自动创建topic的,所以这个必不可少,到这里有一个问题:
topicPublishInfoTable这个集合里面的信息是哪里来的呢,一开始启动的时候这个集合是空的,我们猜测是通过remoting发送请求去从namesrv获取topic信息,我们接着往下看:
还是这段代码,我们接着往下分析,假设第一次进来的时候集合里面的数据是空的,所以会走第一个if判断,this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);这个方法名称印证了我们的判断,接着跟进去来到org.apache.rocketmq.client.impl.factory.MQClientInstance#updateTopicRouteInfoFromNameServer(java.lang.String, boolean, org.apache.rocketmq.client.producer.DefaultMQProducer)这个方法,
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
if (null == topicPublishInfo || !topicPublishInfo.ok()) {
this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
}
if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
return topicPublishInfo;
} else {
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
return topicPublishInfo;
}
}
如下代码:
继续跟进:
创建了一个RemotingCommand,request,而这个请求type是equestCode.GET_ROUTEINFO_BY_TOPIC,也就是获取该topic的路由信息,接着调用this.remotingClient.invokeSync来调用remoting模块进行调用,具体调用流程我们在之前的文章里面讲过了顺藤摸瓜RocketMQ之整体架构以及执行流程,拿到response之后获取到body,解码之后返回
public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis,
boolean allowTopicNotExist) throws MQClientException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException {
GetRouteInfoRequestHeader requestHeader = new GetRouteInfoRequestHeader();
requestHeader.setTopic(topic);
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINFO_BY_TOPIC, requestHeader);
RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis);
assert response != null;
switch (response.getCode()) {
case ResponseCode.TOPIC_NOT_EXIST: {
if (allowTopicNotExist) {
log.warn("get Topic [{}] RouteInfoFromNameServer is not exist value", topic);
}
break;
}
case ResponseCode.SUCCESS: {
byte[] body = response.getBody();
if (body != null) {
return TopicRouteData.decode(body, TopicRouteData.class);
}
}
default:
break;
}
throw new MQClientException(response.getCode(), response.getRemark());
}
回到我们的org.apache.rocketmq.client.impl.factory.MQClientInstance#updateTopicRouteInfoFromNameServer(java.lang.String, boolean, org.apache.rocketmq.client.producer.DefaultMQProducer)方法
走到这里topicPublishInfoTable这个集合里面就已经有从namesrv里面拉过来的topic信息了,然后回到下面代码,接着判断topicPublishInfo.isHaveTopicRouterInfo()这里返回的什么呢?注意看上面那个图,中间有一行publishInfo.setHaveTopicRouterInfo(true);
将这个参数设置为true,也就是从namesrv获取了之后这个表示是否有topic信息的标识符就会被设置成true表示集合里面已经有信息了,然后就直接return topicPublishInfo;这个topicPublishInfo就包含上面截图里面那些数据。
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
if (null == topicPublishInfo || !topicPublishInfo.ok()) {
this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
}
if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
return topicPublishInfo;
} else {
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
return topicPublishInfo;
}
}
思绪拉回我们的主线,org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl这个方法中,继续debug,可以看到timesTotal是3,也就是如果发送失败最多会发送3次,
接下来按照我们的主逻辑,拿到了topic信息之后是不是要从该topic信息中选择一个queue,也就是接下来的方法里面MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);我们跟进去:
代码跟进到org.apache.rocketmq.client.impl.producer.TopicPublishInfo#selectOneMessageQueue(),读者可以根据我的调用栈来看一下代码是怎么走的,我们这边直接分析选择queue逻辑,是根据sendWhichQueue这个参数来去余去选择哪个队列,也就是轮训
,所以如果要保证每个消息发送到同一个queue上面来保证消息消费顺序?
这个问题也就引刃而解了,(当然不是在这里改代码,而是有个MessageQueueSelector接口实现了之后,放到send方法里面,这里不展开了)。
回到主逻辑,这里轮询到了一个queue接下来,是不是应该把这个queue信息方法发送request里面准备发送了呢?我们接着跟进:
接下来最重要的一步,调用sendKernelImpl方法来进行发送,参数笔者标注了一下,可以清晰的看到各个参数,msg信息和上面一样,mq是我们刚刚选择到的queue,重点关注该queue的queueid
,communicationMode是同步发送,sendCallback为空(异步发送回调),topicPublishInfo不多说了,上面分析了好久。
接着跟进去:
对消息进行序列话,设置消息id,判断是是否是批量消息
接着跟进:
请求头里面放了topic信息,queue信息,还有一些标记,这边大家可以思考下:如果是你,你会讲哪些信息放到请求头里面?
接着来到:
this.mQClientFactory.getMQClientAPIImpl().sendMessage,调用这个方法发送,
最后来到:
迷失方向的小伙伴可以通过看下面调用栈去跟进代码,可以看到request信息,code
是310,opaque
是895,然后invokeSync这个方法就是调用我们的remoting模块的netty发送请求给broker了,这里broker刷盘的逻辑我们放到后面的文章分析。
我们这边先不去看broker那边怎么处理,我们这边先假设broker处理没问题,这边拿到了reponse,接着调用this.processSendResponse(brokerName, msg, response,addr);方法,我们接着跟进:
返回的code码是0,就是success,并且我们在这里看到了熟悉的SEND_OK,还记得我们上面发送执行完成之后控制台打印的那些东西吗?返回码正式SEND_OK,所以发送的reponse返回来之后,在这个方法里面进行处理结果,
接着往下:
将msg的一下信息放到sendResult里面,最后返回到send的结果。
拿到发送结果!至此发送流程结束!
初次看rocketmq代码一定会是一头雾水,笔者也是最近才开始研究rocketmq源码,希望读者耐心把这个拦路虎读完,一定会有很多收获,以此共勉!