本文首发于Ressmix个人站点:https://www.tpvlog.com
通过前面几章的讲解,大家应该已经了解了NameServer和Broker的核心原理。我们来回顾一下:
首先NameServer启动后,内部会有一个核心的NamesrvController组件,控制NameServer的所有行为,包括启动一个Netty服务器去监听一个9876端口号,处理Broker和客户端发送过来的请求:
接着,Broker启动之后,由内部的一个BrokerController组件管控Broker的整体行为,包括初始化Netty服务器用于接收客户端的网络请求、启动处理请求的线程池、执行定时任务的线程池、初始化核心功能组件等等,同时还会启动之后发送注册请求到NameServer去注册自己:
最后,Broker启动之后会通过心跳与NameSever保持连接,NameServer有一个后台进程定时检查每个Broker的最近一次心跳时间,如果长时间没心跳就认为Broker已经故障,会将其剔除:
到上述流程为止,RocketMQ集群就已经可以使用,我们可以通过Producer发送消息到MQ,本章我就来讲讲Producer的底层原理。
一、Producer创建
Producer的创建和使用非常简单,我们可以在example
包下找到使用示例:
1// 创建生产者,并设置分组2DefaultMQProducer producer = new DefaultMQProducer("myGroup");3// 启动生产者4producer.start();
Producer启动后会进行初始化,但是并不会立即从NameServer拉取路由数据,只有当第一次发送消息时,才会根据消息的Topic拉取对应的路由数据。另外,其它很多核心逻辑,包括MessageQueue选择、跟Broker建立网络连接等,这些逻辑都是在Producer发送消息的时候才去执行。
Producer启动后的初始化方法没什么太多可以说的,核心逻辑都在首次发送消息时执行,所以本节不对初始化方法赘述。
二、发送消息
接着,我们来看下Producer发送消息的过程,消息必须指定Topic:
1Message msg = new Message("TopicTest","TagA", ("Hello RocketMQ").getBytes("UTF-8"));2SendResult sendResult = producer.send(msg);
关键看下send方法:
1@Override2public SendResult send(Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {3 Validators.checkMessage(msg, this);4 msg.setTopic(withNamespace(msg.getTopic()));5 return this.defaultMQProducerImpl.send(msg);6}
层层往下,我们发现最终会调用DefaultMQProducerImpl.send()
:
1private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {2 // ...3 TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());45 // ...6 MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);7}
2.1 拉取路由信息
上述代码,我省略了很多无关紧要的逻辑,关键是tryToFindTopicPublishInfo
这个方法,它会先从生产者本地查找是否有路由信息,没有再从NameServer拉取:
1private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) { 2 // 从本地查询路由表 3 TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic); 4 5 // 本地不存在,发送请求到NameServer查询 6 if (null == topicPublishInfo || !topicPublishInfo.ok()) { 7 this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo()); 8 // 从NameServer拉取 9 this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);10 topicPublishInfo = this.topicPublishInfoTable.get(topic);11 }1213 if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {14 return topicPublishInfo;15 } else {16 this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);17 topicPublishInfo = this.topicPublishInfoTable.get(topic);18 return topicPublishInfo;19 }20}
上述的updateTopicRouteInfoFromNameServer
方法是关键,它的核心流程是封装一个Request请求对象,然后通过底层的Netty客户端发送请求到NameServer,最后会接收到一个Response响应对象, 然后它会从Response响应对象里提取Topic路由数据,更新到自己的本地缓存里:
这里我就不再贴源码了,读者完全可以按照上述思路自己去源码里看,搞清了主干逻辑,源码其实很好理解。
2.2 选择MessageQueue
基础篇中我已经对RocketMQ的架构和基本原理作了详细讲解,那么大家应该知道Topic只是逻辑上的概念,消息最终是分布在Broker的各个MessageQueue中的。所以,Producer拉取完路由信息后,需要选择将消息发送到哪个MessageQueue中,知道了MessageQueue后,只要与MessageQueue所在的Broker建立连接就可以发送消息了。
DefaultMQProducerImpl.sendDefaultImpl()
方法中,有一个selectOneMessageQueue方法,这行代码其实就是选择Topic中的一个MessageQueue,然后发送消息到这个MessageQueue去:
1private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {2 // ...3 TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());45 // ...6 MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);7}
我们来看下selectOneMessageQueue
方法的细节,我省略了一些Broker的故障回避机制代码,下面代码的核心逻辑就是:用一个自增的index对Topic的MessageQueue列表进行取模,获取到一个MessageQueue列表的位置,然后返回这个位置的MessageQueue:
1int index = tpInfo.getSendWhichQueue().getAndIncrement(); 2for (int i = 0; i 3 int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size(); 4 if (pos 0)
5 pos = 0;
6 MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
7 if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
8 if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
9 return mq;
10 }
11}
对于MessageQueue的选择本质是一种负载均衡策略,上述代码其实就是一种最简单的负载均衡策略。
2.3 消息发送
选择完MessageQueue之后,就可以发送数据了,下面是DefaultMQProducerImpl.sendDefaultImpl()
中的一行代码:
1sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
在这个sendKernelImpl
方法里,会通过brokerName从本地缓存查找Broker的实际地址,如果找不到,就从NameServer拉取Topic的路由数据,然后再次缓存到本地,有这个Broker地址后,就可以跟Broker进行网络通信了:
1String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());2if (null == brokerAddr) {3 tryToFindTopicPublishInfo(mq.getTopic());4 brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());5}
后面的逻辑,就是封装一个Request请求,并给消息分配全局唯一ID、对超过4KB的消息体进行压缩等等。Request中会包含生产者组、Topic名称、Topic的MessageQueue数量、MessageQueue的ID、消息发送时间、消息的flag、消息重试次数、prepared标记(事务消息)等等信息。
最后,Producer在底层是基于Netty与Broker建立长连接, 然后基于长连接进行持续通信,这样就可以把请求发送到指定的Broker上去了:
三、总结
本章,我对Producer发送消息的底层源码进行了分析,我们要知道Producer发送消息时的主体流程是先拉取Topic路由信息,然后选择MessageQueue,最后进行基于底层的Netty组件进行消息发送。很多源码的细节我们可以忽略掉,如果大家想要研究更加细致的源码细节,可以按照我上述讲解的流程,通过IDE去阅读对应的源码即可。