文章目录
- 一、基本用法
- 二、消息发送原理
- 2.1 生产者启动
- 2.2 查找主题路由信息
- 2.2.1 几个关键的元数据:
- 2.2.2 查找过程
- 2.3 选择消息队列(MessageQueue)
- 2.4 发送消息
- 三、关键源码
- 3.1 启动源码
- 3.2 查找主题路由元素
- 3.3 选择消息队列
一、基本用法
基本用法主要有同步发送,异步发送和指定队列发送,具体见下。这里主要介绍
// 1. 创建生产者对象,指定生产者group。生产者group的目的是当MQ需要回查事务状态时,会选择group中的任意一个producer进行会擦汗
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
//2. 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876;localhost:9878");
//3. 是否启用broker容错规避机制
// 默认不启用,如果启用,则发送消息失败时,会暂时规避发送失败的Broker
producer.setSendLatencyFaultEnable(true);
// 4.设置同步发送失败重试次数,默认是2次,这里重试的次数是在超时时间内,也就是说
// 无论重试多少次,总耗时都不会超过超时时间
producer.setRetryTimesWhenSendFailed(10);
// 5. 启动生产者,全局整个进程只有一个MQClientInstance实例,可以有多个生产消费者,每个生产消费者注册到MQClientInstance实例上
producer.start();
// 6. 创建一个消息对象,设置主题,tag和消息体
Message msg = new Message("topic1" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
//7.1 同步发送
SendResult sendResult = producer.send(msg);
//7.2 异步发送
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.printf("%s%n", sendResult);
}
// 当Broker返回错误时才会调用,超时不会
@Override
public void onException(Throwable e) {
System.out.printf("%s%n", e);
}
});
// 7.3 局部顺序消息,指定队列发送
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
MessageQueue q = mqs.get(0);
System.out.println(q);
return mqs.get(0);
}
// 这个i参数就是传给MessageQueueSelector接口的select方法的第三个参数
}, i);
二、消息发送原理
消息发送主要包含以下几步:
- 启动,实例化MQClientInstance或将生产者注册到MQClientInstance。
- 查找主题路由信息。
- 从路由信息中,选择要发送的消息队列。
- 发送消息。
2.1 生产者启动
MQClientInstance一般全局只有一个,创建生产者时会将生产者加入到MQClientInstance中进行管理,以便后续调用网络请求,心跳检测等。
启动的关键代码在DefaultMQProducerImpl类中:
this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);
关键对象是MQClientInstance,它记录了主题路由信息,同时连接着通信客户端,进行网请求。
2.2 查找主题路由信息
2.2.1 几个关键的元数据:
每个消息发送者的每个主题都依赖一个元数据:TopicPublishInfo:
- TopicPublishInfo:
属性:
- orderTopic是否顺序消息
- List messageQueueList 该主题队列的消息队列,MessageQueue有三个属性:topic,brokerName和queueId
- sendWhichQueue,用于选择消息队列,每选择一次消息队列就自增一。
- TopicRouteData 主题路由数据,主要包含:
- List topic队列,一个topic可能有多个Broker,每个Broker有多个队列,每个队列是一个QueueData
- List topic分布的broker元数据,一个BrokderData代表一个Broker
2.2.2 查找过程
- 使用主题查询NameSever,获取TopicRouteData对象,然后跟本地TopicRouteData比较,如果变更了,则向下执行。
- 更新MQClientInstance中的本地broker缓存,将TopicRouteData封装成TopicPublishInfo。
- 封装过程是遍历QueueData列表,找到所有可写的QueueData和合法的QueueData,创建MessageQueue,在topicRouteData2TopicPublishInfo方法中创建了messageQueueList。
- 然后更新MQClientInstance管理的生产者的TopicPublishInfo。
2.3 选择消息队列(MessageQueue)
- 根据TopicPublishInfo对象中的sendWhichQueue值从messageQueueList队列中选择一个MessageQueue,每选择一次消息队列加一。
- 选择消息队列采用重试机制,由retryTimesWhenSendFailed配置指定同步方式进行重试。每次失败后将选择下一个MessageQueue。
- 如果开启了Broker故障延迟机制,则将该Broker暂时排除在消息队列选择范围中。
- 当指定Selector时,就是从messageQueueList队列中选一个,这个是Selector中的一个参数
2.4 发送消息
发送消息有三种方式,同步发送,异步发送和单向发送。
发送消息的状态结果有4种,分别是:
- FLUSH_DISK_TIMEOUT。
- FLUSH_SLAVE_TIMEOUT.
- SLAVE_NOT_AVAILABLE.
- SEND_OK
这四种状态需要结合配置的刷盘策略(同步刷盘,异步刷盘)和主从同步策略来分析。
同步和异步发送的区别:
- 同步发送时,需要阻塞等待netty中的 channel.writeAndFlush(request).addListener()方法中对ResponseFuture设置结果,
而异步发送是在接收到服务端的响应后,在SimpleChannelInboundHandler类中的channelRead0方法中触发,EventLoop线程中执行回调函数或者在EventLoop线程中将任务放到一个ExecutorService中执行,不会同步等待结果。 - 同步发送重试会使用retryTimesWhenSendFailed进行重试,就算是服务端超时了也会重试,是在外层。 而异步重试必须是收到了的服务端的异常响应包才会重试,受到retryTimesWhenSendAsyncFailed参数影响。
三、关键源码
3.1 启动源码
启动主要是MQClientInstance对象的实例化和注册,MQClientInstance封装了RocketMQ网络处理API,是生产者消费者和NameServer,Broker打交道的网络通道。
DefaultMQProducerImpl类中的start方法
public void start(final boolean startFactory) throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
this.checkConfig();
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
this.defaultMQProducer.changeInstanceNameToPID();
}
// 这里使用单例模式获得了MQClientInstance对象
this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);
// 同时将当前生产者注册到MQClientInstance对象中
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);
}
//....后面代码省略
3.2 查找主题路由元素
这一块是贯穿了整个发送过程中的关键代码,是DefaultMQProducerImpl类中的sendDefaultImpl方法
// 一、查找主题路由信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
boolean callTimeout = false;
MessageQueue mq = null;
Exception exception = null;
SendResult sendResult = null;
// 注意这里,外层同步发送时会考虑getRetryTimesWhenSendFailed,异步时不会重试
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
String[] brokersSent = new String[timesTotal];
for (; times < timesTotal; times++) {
System.out.println("同步重试第:{}此, 一共:{}次" + times + " " + timesTotal);
log.info("同步重试第:{}此, 一共:{}次", times, timesTotal);
String lastBrokerName = null == mq ? null : mq.getBrokerName();
// 二、选择消息队列
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
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;
System.out.println("超时时间是"+ timeout);
// 如果重试后调用超时则直接break不再重试
if (timeout < costTime) {
System.out.println("已经超时了,耗时"+costTime);
callTimeout = true;
break;
}
// 三、执行消息发送
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
// 四、处理返回结果
switch (communicationMode) {
case ASYNC:
return null;
case ONEWAY:
return null;
case SYNC:
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
continue;
}
}
return sendResult;
default:
break;
}
} catch (RemotingException e) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
log.warn(msg.toString());
exception = e;
continue;
} catch (MQClientException e) {
// 后面代码省略.....
从上面代码可以看出,查找路由信息的方法是在tryToFindTopicPublishInfo中,它返回了TopicPublishInfo元数据。
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
// 1. 从缓存中获取
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
// 2. 如果缓存中无有效路由信息,则从NameServer中获取
if (null == topicPublishInfo || !topicPublishInfo.ok()) {
this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
// 3. 这个方法是由MQClientInstance去调用NameServer同时更新所有生产者和消费者的路由信息,这个过程需要上锁进行同步
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;
}
}
Namserver是有多个的,每次选一个,就算每个NameServer存的路由信息不一致也没关系。
3.3 选择消息队列
在3.2中我们可以看到这一段代码,可以看到,同步发送时这里用到了getRetryTimesWhenSendFailed来计算重试次数,异步时只执行一次。每次都调用selectOneMessageQueue来获取一个MessageQueue元数据,它代表一个Broker的一个队列。同时整体重试时间不超过timeout,如果超过直接break。
// 注意这里,外层同步发送时会考虑getRetryTimesWhenSendFailed,异步时不会重试
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
String[] brokersSent = new String[timesTotal];
for (; times < timesTotal; times++) {
log.info("同步重试第:{}此, 一共:{}次", times, timesTotal);
String lastBrokerName = null == mq ? null : mq.getBrokerName();
// 二、选择消息队列
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
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;
System.out.println("超时时间是"+ timeout);
// 如果重试后调用超时则直接break不再重试
if (timeout < costTime) {
System.out.println("已经超时了,耗时"+costTime);
callTimeout = true;
break;
}
//.... 省略循环体后面的代码
这里selectOneMessageQueue有三种方式,一种是开启了Broker规避,一种是未开启。如果未开启则很简单,只是根据sendWhichQueue值从messageQueueList队列中选择一个MessageQueue,每选择一次消息队列加一,依次遍历所有的MessageQueue。
如果开始了Broker规避,则会跳过失败的Broker。
另外一种是直接用户根据参数指定选择某一个队列,也就是直接从messageQueueList选择一个队列进行消息发送。
为什么要规避:
Broker不可用后,路由信息短期内还是包含这个Broker的,因为
- 首先NameServer检测Broker是否可用是有间隔的,一般是10秒。
- NameServer检测到Broker挂了之后不会马上推送消息给客户端,而是客户端每隔30秒更新一次路由信息,也就是Broker挂了之后客户端最快要等30秒才能将这个Broker从客户端移除。
- 所以一次消息发送失败后,可以将Broker暂时排除在消息队列的选择范围中。