生产者
我们可以从测试用例 org.apache.rocketmq.example.quickstart.Producer
来看生产者发送消息的流程。
消息生产者发送消息比较简单,发送消息的组件就是 DefaultMQProducer
,它在创建时至少需要指定一个生产者组名称,然后设置 NameServer 的地址,这样才能从 NameServer 拉取到 Broker 和 Topic 等信息。
接着就是创建 Message
对象,指定了消息所属 topic,标签,以及消息体,然后调用 DefaultMQProducer 将消息发送到 Broker 去。
java
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException {
// 创建消息生产者,并指定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// 设置xNameServer地址
producer.setNamesrvAddr("127.0.0.1:9876");
// 启动生产者
producer.start();
// 创建消息
byte[] body = ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET);
Message msg = new Message("TopicTest", "TagA", body);
// 发送消息
SendResult sendResult = producer.send(msg);
System.out.printf("Results: %s%n", sendResult);
// 关闭生产者
producer.shutdown();
}
}
消息生产者
1、DefaultMQProducer
消息生产者 DefaultMQProducer
继承自 ClientConfig
,实现了 MQProducer
接口,MQProducer 又是继承自 MQAdmin
接口。MQAdmin 提供了创建topic、查询消息、查询偏移量等API;MQProducer 则主要是发送消息相关API。
从 DefaultMQProducer 的构造方法知道,一个消息生产者是归属于一个命名空间(namespace
)下的一个组(producerGroup
),也就是说我们在使用RocketMQ时,可以通过命名空间和组对消息进行一个分类。而发送消息的真正实现组件是 DefaultMQProducerImpl
,DefaultMQProducer 启动时会启动这个组件。
从 DefaultMQProducer 的属性可以了解到:
- 创建topic时,默认的读写队列数量是
4
; - 发送消息的超时时间是
30秒
; - 发送消息最大大小是
4MB
,消息超过4KB
后就进行压缩; - 支持消息发送失败后重试,也支持重试其它broker。
java
public class DefaultMQProducer extends ClientConfig implements MQProducer {
// 生产者实现
protected final transient DefaultMQProducerImpl defaultMQProducerImpl;
// 生产组
private String producerGroup;
// topic 队列默认数量
private volatile int defaultTopicQueueNums = 4;
// 发送消息超时时间 30 秒
private int sendMsgTimeout = 30000;
// 消息压缩阈值,超过 4KB 就压缩数据
private int compressMsgBodyOverHowmuch = 1024 * 4;
// 同步模式下,发送消息失败后重试次数
private int retryTimesWhenSendFailed = 2;
// 异步模式下,发送消息失败后重试次数
private int retryTimesWhenSendAsyncFailed = 2;
// 发送消息失败后是否重试其它的Broker
private boolean retryAnotherBrokerWhenNotStoreOK = false;
// 消息最大大小
private int maxMessageSize = 1024 * 1024 * 4; // 4M
public DefaultMQProducer(final String namespace, // 消息生产者命名空间
final String producerGroup, // 消息生产组所属组
RPCHook rpcHook // 钩子函数) {
this.namespace = namespace;
this.producerGroup = producerGroup;
defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
}
}
// 发送消息
public SendResult send(Message msg) throws Exception {
msg.setTopic(withNamespace(msg.getTopic())); // 设置命名空间
return this.defaultMQProducerImpl.send(msg); // 发送消息
}
2、DefaultMQProducerImpl
DefaultMQProducerImpl 是发送消息的实现类,发送消息首先要确定向哪个Broker发送,所以它用一个topic路由表 topicPublishInfoTable
来维护topic与broker的路由关系。
而真正在与broker进行网络调用的组件则是 MQClientInstance
,它是客户端与Broker进行通信的组件。生产者客户端启动完成之后,会向其注册生产者对象,然后向所有broker发送心跳,上报自己的元数据。
java
public class DefaultMQProducerImpl implements MQProducerInner {
// topic 发布信息映射表
private final ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable = new ConcurrentHashMap<>();
// MQ 网络客户端实例
private MQClientInstance mQClientFactory;
// 消息发送容错策略
private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy();
// 压缩器
private final Compressor compressor = CompressorFactory.getCompressor(compressType);
public void start(final boolean startFactory) throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
// 创建 MQClientInstance,同一个客户端只会有一个,MQClientManager 用一个 factoryTable 表来存
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
// 注册生产组,将 生产者放入 producerTable
boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
// topic 发布信息,一个新建的 TopicPublishInfo
this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());
// 启动MQClient
if (startFactory) {
mQClientFactory.start();
}
this.serviceState = ServiceState.RUNNING;
break;
...
}
// 客户端向所有broker发送心跳,保持与broker的连接
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
}
}
3、MQClientInstance
MQClientInstance 正常情况下一个客户端只会有一个实例,从它的结构中可以大概了解到,MQClientInstance 就是生产者和消费者的客户端实例,它负责管理生产者、消费者组件,它会定时去更新路由数据,Broker地址表等基础信息,还负责消息的拉取、重平衡等。
MQClientInstance 中有一个组件 MQClientAPIImpl
,它是最后一步真正调用Broker的组件,其内部是基于 NettyRemotingClient
实现的远程调用与Broker通信,这个组件前面有介绍过。
java
public class MQClientInstance {
// 客户端配置
private final ClientConfig clientConfig;
// 客户端ID
private final String clientId;
// producer 组件映射表
private final ConcurrentMap<String/* group */, MQProducerInner> producerTable = new ConcurrentHashMap<>();
// consumer 组件映射表
private final ConcurrentMap<String/* group */, MQConsumerInner> consumerTable = new ConcurrentHashMap<>();
// admin 组件映射表
private final ConcurrentMap<String/* group */, MQAdminExtInner> adminExtTable = new ConcurrentHashMap<>();
// netty config
private final NettyClientConfig nettyClientConfig;
// 客户端通信API实现组件
private final MQClientAPIImpl mQClientAPIImpl;
// 管理客户端网络通信组件
private final MQAdminImpl mQAdminImpl;
// 服务端调用客户端的处理器
private final ClientRemotingProcessor clientRemotingProcessor;
// topic 路由数据
private final ConcurrentMap<String/* Topic */, TopicRouteData> topicRouteTable = new ConcurrentHashMap<>();
// broker 地址表
private final ConcurrentMap<String/* Broker Name */, HashMap<Long/* brokerId */, String/* address */>> brokerAddrTable = new ConcurrentHashMap<>();
// broker 版本表
private final ConcurrentMap<String/* Broker Name */, HashMap<String/* address */, Integer>> brokerVersionTable = new ConcurrentHashMap<>();
// 定时任务执行器
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {...);
// 拉取消息服务
private final PullMessageService pullMessageService;
// 重平衡服务
private final RebalanceService rebalanceService;
private final DefaultMQProducer defaultMQProducer;
// 消费者状态管理器
private final ConsumerStatsManager consumerStatsManager;
// 发送心跳总数
private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0);
}
Topic路由数据结构
作为消息客户端,首先需要知道每个topic被发送到哪个Broker上,DefaultMQProducerImpl 用一个topic信息表topicPublishInfoTable
来维护topic与broker间的关系。
每个 Broker 启动时,会向 NameServer 注册自己的信息,包括自己的地址、Topic信息等等,NameServer 中的路由信息管理组件 RouteInfoManager
就维护了所有的Broker元数据。客户端启动后,就会从 RouteInfoManager 获取所有的Broker元数据到本地,并且 MQClientInstance 中会启动一个定时任务,每隔30秒
更新一次topic信息表。
这个topic信息表的结构如下图,一个 topic 会对应一个 TopicPublishInfo
,它包含了多个消息队列 MessageQueue
和一个topic路由数据 TopicRouteData
。MessageQueue 主要记录了这个 topic 发送到哪些 broker 中的哪些队列中了。TopicRouteData 主要包含多个队列信息 QueueData
和 Broker信息 BrokerData
;QueueData 是用来表示这个 topic 在每个 broker 上的配置信息(读写队列数量、权限等),BrokerData 则表示这个 topic 在哪些broker上, 以及对应的 broker 地址表。
从上图这个结构我们可以知道RocketMQ对topic路由的逻辑结构
设计,一个 topic 的消息可以发送到多个Broker集群中,可以分布在集群中多组broker上,在一个broker上每个topic还有多个读写队列。
所以 topicPublishInfoTable 就维护了这些关系,在发送消息时,就可以从这个信息表中去根据一定策略(默认是轮询策略)去计算消息发送到哪个集群的哪组broker中的哪个队列中,然后得到broker的地址,就可以将消息发送到这个broker去。
发送消息流程
1、发送消息核心流程
DefaultMQProducerImpl 发送消息的核心流程可以简化成如下几步:
- 根据 topic 获取路由信息 TopicPublishInfo
- 从 TopicPublishInfo 中以轮询的策略获取一个消息队列 MessageQueue
- 将消息发送到消息队列 MessageQueue
- 同步模式下,消息发送失败时,在没超时的情况下,默认支持重试2次
java
private SendResult sendDefaultImpl(Message msg, CommunicationMode communicationMode, SendCallback sendCallback, long timeout // 超时时间 ) throws Exception {
// 查询 topic 路由信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
MessageQueue mq = null;
SendResult sendResult = null;
// 推送消息支持一定次数的重试,同步模式下,失败重试次数,默认3次
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
for (; times < timesTotal; times++) {
// 要发送的broker
String lastBrokerName = null == mq ? null : mq.getBrokerName();
// 轮询选择一个消息队列
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
if (mqSelected != null) {
mq = mqSelected;
try {
// 时间信息,判断是否超时
beginTimestampPrev = System.currentTimeMillis();
long costTime = beginTimestampPrev - beginTimestampFirst;
if (timeout < costTime) {
callTimeout = true;
break;
}
// 发送消息到某个消息队列
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
...
} catch (Exception e) {...}
}
}
...
}
发送消息时,会根据消息队列中的 brokerName 找到对应的 broker 地址,MQClientInstance 中有一个broker地址表 brokerAddrTable
维护了broker的地址信息。得到broker地址后,再对消息做一些设置,然后将消息根据协议转码成 SendMessageRequestHeader,最后再调用 MQClientInstance 的 MQClientAPIImpl
同步或异步的方式将消息发送到Broker上。
java
private SendResult sendKernelImpl(final Message msg, MessageQueue mq, CommunicationMode communicationMode, // 发送模式
SendCallback sendCallback, TopicPublishInfo topicPublishInfo, long timeout) throws Exception {
long beginStartTime = System.currentTimeMillis();
// 找出要发送到哪个 broker 上
String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
try {
// 发送消息时设置消息唯一ID
// 命名空间设置
// 消息超过4KB压缩标识
// 事务消息标识
...
// 发送消息请求头协议
SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); // 消息组
requestHeader.setTopic(msg.getTopic()); // topic
requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey()); // 默认topic
requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums()); // 默认队列数
requestHeader.setQueueId(mq.getQueueId()); // 发送到哪个队列
requestHeader.setSysFlag(sysFlag); // 消息系统标识
...
SendResult sendResult = null;
switch (communicationMode) {
case ASYNC: // 同步发送
// 异步发送消息
sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(brokerAddr, ..., msg, sendCallback, ...);
break;
case ONEWAY:
case SYNC:
// 同步发送消息
sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(brokerAddr, ... msg);
break;
}
// 发送消息钩子函数
...
return sendResult;
} catch (RemotingException e) {...}
}
2、消息发送处理器
通过消息发送编码可以找到 Broker 端对应的消息处理器是 SendMessageProcessor
。
将 SendMessageProcessor 对发送消息的处理逻辑简化下,它首先会对消息做一个预发送处理,再对消息检查,如果检查不通过就直接返回失败;检查通过,则根据协议将消息转成内部的消息结构 MessageExtBrokerInner。
如果是事务消息,则将消息提交到事务Topic里面(两阶段提交的Prepared阶段)。不是事务消息,则将消息写入消息存储 MessageStore,这块逻辑我们前面已经分析过了,就是将消息追加到 CommitLog
中,然后分发到消费队列 ConsumeQueue
和索引 IndexFile
中。
java
private CompletableFuture<RemotingCommand> asyncSendMessage(ChannelHandlerContext ctx, RemotingCommand request,
SendMessageContext mqtraceContext,
SendMessageRequestHeader requestHeader) {
// 响应,消息预发送
final RemotingCommand response = preSend(ctx, request, requestHeader);
final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader)response.readCustomHeader();
// 消息检查
super.msgCheck(ctx, requestHeader, response);
if (response.getCode() != -1) {
return CompletableFuture.completedFuture(response);
}
// broker内部的消息结构
MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
msgInner.setTopic(requestHeader.getTopic());
msgInner.setQueueId(requestHeader.getQueueId());
msgInner.setBody(request.getBody());
msgInner.setFlag(requestHeader.getFlag());
Map<String, String> origProps = MessageDecoder.string2messageProperties(requestHeader.getProperties());
MessageAccessor.setProperties(msgInner, origProps);
...
CompletableFuture<PutMessageResult> putMessageResult = null;
String transFlag = origProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
// 事务消息
if (transFlag != null && Boolean.parseBoolean(transFlag)) {
// 事务消息准备
putMessageResult = this.brokerController.getTransactionalMessageService().asyncPrepareMessage(msgInner);
} else {
// 基于消息存储区追加消息
putMessageResult = this.brokerController.getMessageStore().asyncPutMessage(msgInner);
}
// 处理消息结果
return handlePutMessageResultFuture(putMessageResult, response, request, msgInner, responseHeader, mqtraceContext, ctx, queueIdInt);
}
在消息检查中,主要对topic本身的写入权限、名称等做了校验。最重要的是,如果当前Broker中 TopicConfigManager 没有对应 topic 的配置元数据,则会自动创建一个根据客户端指定的默认topic配置创建一个 TopicConfig
元数据。
这块我们前面也分析过,TopicConfigManager 提供了一个方法,在发送消息时自动创建 TopicConfig,创建好之后,还会向所有 NameServer 重新注册,上报最新的topic信息。
java
protected RemotingCommand msgCheck(ChannelHandlerContext ctx, SendMessageRequestHeader requestHeader, RemotingCommand response) {
// 写入权限检查
// topic 名称检查
// 不允许客户端发送消息的topic检查
// 获取 TopicConfig
TopicConfig topicConfig = getTopicConfigManager().selectTopicConfig(requestHeader.getTopic());
if (null == topicConfig) {
int topicSysFlag = 0;
// 在消息发送时,创建 Topic
topicConfig = getTopicConfigManager().createTopicInSendMessageMethod(
requestHeader.getTopic(), requestHeader.getDefaultTopic(),
RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
requestHeader.getDefaultTopicQueueNums(), topicSysFlag);
...
}
// queueId 合法性检查
return response;
}
Topic 路由数据获取
通过前面的分析,我们大概就了解topic元数据是如何创建,以及客户端是如何找到要发送的broker,这节来看下一个新的topic是什么时候创建以及路由数据是如何来的。
DefaultMQProducerImpl 内部发送消息时,会根据 topic 查找路由发布信息 TopicPublishInfo
。如果是一个全新的topic,那本地 topicPublishInfoTable
表里肯定是没有的,这时会从 NameServer 获取新 topic 的路由数据,因为可能其它客户端去创建了这个topic。如果TopicPublishInfo里没有路由信息(broker地址),说明这是一个新的topic,那路由数据从何而来呢?
java
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
// 一个新 topic 时没有路由信息
if (null == topicPublishInfo || !topicPublishInfo.ok()) {
// 放入一个空的
this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
// 从 nameserver 拉取路由信息(可能其它客户端创建了topic)
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
}
// 有路由信息,表面已经创建ok
if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
return topicPublishInfo;
} else {
// 没有路由信息,说明还没创建这个topic,这时拉取默认topic的路由数据更新到当前topic
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
return topicPublishInfo;
}
}
从NameServer更新路由信息的方法有一个 isDefault 参数来控制是否获取默认topic的路由信息。每个Broker启动时,TopicConfigManager 都会创建一个 TBW102
的默认topic,Broker会将自己的topic配置、地址等信息注册到 NameServer,通过这个默认的 TBW102
就可以知道一个topic分布到哪些个broker上。
所以当生产者发送消息是一个新的topic时,它会获取 TBW102 的路由数据复制为自己的路由数据, 然后更新本地的broker地址表 brokerAddrTable
,更新生产者表 producerTable
中的路由发布信息,更新消费者表 consumerTable
中的订阅信息等。
java
public boolean updateTopicRouteInfoFromNameServer(
final String topic, // topic 名称
boolean isDefault, // 是否取默认topic路由数据
DefaultMQProducer defaultMQProducer) {
try {
try {
TopicRouteData topicRouteData;
if (isDefault && defaultMQProducer != null) {
// 从NS获取默认topic(TBW102)路由数据
topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(), clientConfig.getMqClientApiTimeout());
if (topicRouteData != null) {
// 设置读写队列数量
for (QueueData data : topicRouteData.getQueueDatas()) {
// TBW102 的读写队列数为 8,本地默认读写队列为 4
int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
data.setReadQueueNums(queueNums);
data.setWriteQueueNums(queueNums);
}
}
} else {
// 直接获取topic的路由数据
topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, clientConfig.getMqClientApiTimeout());
}
if (topicRouteData != null) {
// TopicRouteData 路由表
TopicRouteData old = this.topicRouteTable.get(topic);
boolean changed = topicRouteDataIsChange(old, topicRouteData);
if (changed) {
// 更新broker地址表
for (BrokerData bd : topicRouteData.getBrokerDatas()) {
this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
}
// 更新生产者表的topic发布信息
if (!producerTable.isEmpty()) {
// 路由数据转换为topic发布信息
TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
publishInfo.setHaveTopicRouterInfo(true);
Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
while (it.hasNext()) {
MQProducerInner impl = it.next().getValue();
// 更新topic发布信息
impl.updateTopicPublishInfo(topic, publishInfo);
}
}
// 更新消费者表订阅信息
if (!consumerTable.isEmpty()) {
// 路由数据转换为订阅信息
Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
while (it.hasNext()) {
MQConsumerInner impl = it.next().getValue();
// 更新订阅信息
impl.updateTopicSubscribeInfo(topic, subscribeInfo);
}
}
// 放入路由表
this.topicRouteTable.put(topic, topicRouteData);
return true;
}
}
} catch (MQClientException e) {...}
return false;
}
生产者总结
1、生产者架构
生产者发送消息的组件是 DefaultMQProducer
,它主要是定义消息的命名空间、生产者组、指定默认的topic等消息基本属性。而消息发送能力是代理给 DefaultMQProducerImpl
来完成。
DefaultMQProducerImpl 是消息生产者实现,会注册到 MQClientInstance
作为一个生产者组件。它主要是自动去创建消息的topic,对消息进行一些必要的校验和设置等,然后找到topic对应的路由信息。
MQClientInstance 才是消息客户端实例,它负责管理客户端中的生产者和消费者,负责更新集群中的broker地址信息和topic路由信息。维护了与Broker通信的客户端组件 MQClientAPIImpl,其基于 NettyRemotingClient
实现与broker的远程通信。
2、消息发送流程
消息发送流程其实最核心的逻辑便是获取topic的路由,找到broker地址后,将消息发送到broker就行了。
首先,每组broker启动时都会创建一个默认的topic,即 TBW102
,然后每个broker都会注册到NameServer,那么NS中的路由信息管理器 RouteInfoManager 就知道了 TBW102 的路由信息,其实最主要的信息就是 TBW102 这个 topic 可以发送到哪些 broker 上。
客户端会每隔30秒,更新本地的topic路由信息,因为 broker 可能会下线,路由信息可能有变更。
发送消息时,如果本地路由表中没有对应topic的路由信息,那么就会从NS复制 TBW102 的路由信息,以此来获取 topic 的路由数据。新topic是在消息发送到broker后,broker检查到本地没有TopicConfig,才会去创建topic配置,并注册到NS去。
消息发送到broker后,就是将消息写入CommitLog,然后将消息分发到消费队列 COnsumeQueue 和构建索引 IndexFile。