RocketMq消费发送
流程图
1.生产者启动流程
public void start(final boolean startFactory) throws MQClientException {
//1.检查 productGroup 是否符合要求;
this.checkConfig();
//2. 并改变生产者 instanceName 为进程 ID
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
this.defaultMQProducer.changeInstanceNameToPID();
}
//3.创建MQClientInstance.整个jvm实例中只存在MQClientManager实例,维护一个缓存表
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
//4.向MQClientInstance注册,将当前生产者加入到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);
}
this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());
//5.启动MQClientInstance,如果MQClientInstance已经启动,则本次启动不会真正执行
if (startFactory) {
mQClientFactory.start();
}
log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
this.defaultMQProducer.isSendMessageWithVIPChannel());
this.serviceState = ServiceState.RUNNING;
break;
}
- 检查 productGroup 是否符合要求;并改变生产者 instanceName 为进程 ID
public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
String clientId = clientConfig.buildMQClientId();
MQClientInstance instance = this.factoryTable.get(clientId);
if (null == instance) {
instance =
new MQClientInstance(clientConfig.cloneClientConfig(),
this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);
if (prev != null) {
instance = prev;
log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
} else {
log.info("Created new MQClientInstance for clientId:[{}]", clientId);
}
}
return instance;
}
- 创建MQClientInstance 实例,整个jvm实例中只存在一个MQClientManager实例,维护一个MQClientInstance缓存表ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable = new ConcurrentHashMap<String, MQClientInstance>();也就是同一个clientId只会创建一个MQClientInstance
clientld 为客户端 IP+ instance+ (unitname 可选),如果在 一台物理服务器部署两个应用用程序,应用程序 clientld 相同,会混乱吗?
为了避免这个问题 如果 instance 为默认值 DEFAULT 的话, RocketMQ 会自动将instance 设置为进程 ID ,这样避免了不同进程的相互影响,但同 一个JVM 的不同消费者和生产者在启动的时候获取都是同一个clientId.
MQClientlnstance 封装了 RocketMQ 网络处理 API ,是消息生产者( Producer )、消息消费者(Consumer )与 NameServer、Broker 打交道的网络通道
public boolean registerProducer(final String group, final DefaultMQProducerImpl producer) {
if (null == group || null == producer) {
return false;
}
MQProducerInner prev = this.producerTable.putIfAbsent(group, producer);
if (prev != null) {
log.warn("the producer group[{}] exist already.", group);
return false;
}
return true;
}
if (startFactory) {
mQClientFactory.start();
}
- 向MQClientInstance注册,将当前生产者加入到MQClientInstance管理中,方便后续调用网络请求,进行心跳检测
- 启动MQClientInstance,如果MQClientInstance已经启动,则本次启动不会真正执行
2.消费发送
第一次发送消息时,本地没有缓存 topic 的路由信息,查询 NameServer 尝试获取,如果路由信息未找到,再次尝试用默认主题 DefaultMQProducerlmpl#createTopicKey 去查询,如果 BrokerConfig#autoCreateTopicEnable true 时, NameServer 将返回路由信息,如果autoCreateTopicEnab为false 将抛出无法找到 topic 路由异常.
public SendResult send(
Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
return send(msg, this.defaultMQProducer.getSendMsgTimeout());
}
1.消息长度验证
- 消费发送默认是以同步方式发送,默认超时时间为3秒
private SendResult sendDefaultImpl(
Message msg,
final CommunicationMode communicationMode,
final SendCallback sendCallback,
final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
this.makeSureStateOK();
//消息长度验证
Validators.checkMessage(msg, this.defaultMQProducer);
//查找主题路由信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
//如果有路由信息,选择一个队列进行发送
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
}
//消息发送
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
//最终未找到路由信息,抛出No route info of this topi
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);
}
2.查找主题路由信息
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
//1.看看是否缓存了topic的路由信息
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
//如果缓存不存在且消息队列为0
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;
}
}
- tryToFindTopicPublishlnfo 是查找主题的路由信息的方法,如果生产者中缓存了 topic的路由信息,如果该路由信息中包含了消息队列,则直接返回该路由信息,如果没有缓存或没有包含消息队列, 则向nameServer 查询该 topic 路由信息 ,如果最终未找到路由信息,则抛出异常 无法找到主题相关路由信息异常
MQClientlnstance#updateTopicRouteInfoFromNameServer这个方法的功能是消息生产者更新和维护路由缓存
org.apache.rocketmq.client.impl.factory.MQClientInstance#updateTopicRouteInfoFromNameServer
public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
TopicRouteData topicRouteData;
if (isDefault && defaultMQProducer != null) {
//:如果isDefault为true,则使用默认主题去查询,如果查询到路由信息,则替换路由信息中读写队列个数为
// 消息生产者默认的队列个数(defaultTopicQueueNums );如isDefault为false ,则使用参数topic去查询;
// 如果未查询到路由信息,则返回 false ,表示路由信息未变化
topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
1000 * 3);
//如果默认的topic存在,遍历里面的队列重新设置队列数量4
if (topicRouteData != null) {
for (QueueData data : topicRouteData.getQueueDatas()) {
int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
data.setReadQueueNums(queueNums);
data.setWriteQueueNums(queueNums);
}
}
} else {
topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
}
}
- 第一次发送消息时,本地没有缓存 topic 的路由信息,查询 NameServer 尝试获取,如果路由信息未找到,再次尝试用默认主题 DefaultMQProducerlmpl#createTopicKey 去查询,如果 BrokerConfig#autoCreateTopicEnable true 时, NameServer 将返回路由信息,如果autoCreateTopicEnab为false 将抛出无法找到 topic 路由异常.
- 如果isDefault为true,则使用默认主题去查询,如果查询到路由信息,则替换路由信息中读写队列个数为消息生产者默认的队列个数(defaultTopicQueueNums );如isDefault为false ,则使用参数topic去查询;如果未查询到路由信息,则返回 false ,表示路由信息未变化
TopicRouteData old = this.topicRouteTable.get(topic);
//路由信息找到,与本地缓存中的路由信息进行对比,判断路由信息是否发了改变
boolean changed = topicRouteDataIsChange(old, topicRouteData);
if (!changed) {
//更新 MQClientlnstance Broker 地址缓存表
changed = this.isNeedUpdateTopicRouteInfo(topic);
} else {
log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
}
-
如果路由信息找到,与本地缓存中的路由信息进行对比,判断路由信息是否发了改变, 如果未发生变化,则直接返回 false
-
更新 MQClientlnstance Broker 地址缓存表
TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
for (BrokerData bd : topicRouteData.getBrokerDatas()) {
this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
}
// Update Pub info
{
TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
publishInfo.setHaveTopicRouterInfo(true);
Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, MQProducerInner> entry = it.next();
MQProducerInner impl = entry.getValue();
if (impl != null) {
impl.updateTopicPublishInfo(topic, publishInfo);
}
}
}
-
根据 topicRouteData 中的 List 转换成问 List列表 ,然后更新该 MQClientInstance所管辖的所
有消息发送关于 topic 的路由信息
3.选择消息队列
首先在一次消息发送过程中,可能会多次执行选择消息队列这个方法, lastBrokerName就是上一次选择的执行发送消息失败的 Broker 第一次执行消息队列选择时,lastBrokerName为null ,此时直接 sendWhichQueue 自增再获取值 与当前路由 表中消息队列个数取模, 返回该位置 MessageQueue(selectOneMessageQueue() 方法如果消息发送再失败的话,下次进行消息队列选择时规避上次 MesageQueue 在的 Broker 则还很有可能再次失败.
该算法在一次消息发送过程中能成功规避故障的 Broker,但如果 Broker 若机,由于路由算法中的消息队 是按 Broker 的,如果上一次根据路由算法选择的是若机的 Broker排序的第一个队列 ,那么随后的下次选择的是若 Broker 第二个队列,消息发送很有可能会失败,再再次引发重试,带来不必要的性能损耗,那么有什么方法在一次消息发送失败后,暂时将该 Broker 排除在消息队列选择范围外呢?或许有朋友会问, Broker不可用后,路由信息中为什么还会包含该 Broker的路由信息呢?
其实这不难解释:首先, NameServer检查Broker 是否可用是有延迟的,最短为一次心跳检测间隔( 10s ); 其次, NameServer
不会检测到 Broker 岩机后马上推送消息给消息生产者,而是消息生产者每隔 30s 更新一次路由信息,所以消息生产者 快感知 Broker 最新的路由信息也需要 30s ,如果能引人一种机制,可以将Broker 宕机期间,如果一次消息发送失败后,可以将该 Broker 暂时排除在消息队列的选择范围中
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
//sendLatencyFaultEnable=true ,启用 Broker 障延迟机制
if (this.sendLatencyFaultEnable) {
try {
int index = tpInfo.getSendWhichQueue().getAndIncrement();
for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (pos < 0)
pos = 0;
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
return mq;
}
}
final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
if (writeQueueNums > 0) {
final MessageQueue mq = tpInfo.selectOneMessageQueue();
if (notBestBroker != null) {
mq.setBrokerName(notBestBroker);
mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
}
return mq;
} else {
latencyFaultTolerance.remove(notBestBroker);
}
} catch (Exception e) {
log.error("Error occurred when selecting message queue", e);
}
return tpInfo.selectOneMessageQueue();
}
// sendLatencyFaultEnable=false ,默认不启用 Broker 故障延迟机制
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
4.消息发送
String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
if (null == brokerAddr) {
tryToFindTopicPublishInfo(mq.getTopic());
brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
}
-
根据 MessageQueue 获取 Broker 的网络地址 如果 MQClientlnstance的brokeraddrTable 没有缓存该 Broke 的信息,则从 NameServer 主动更新一下 topic 的路由信息,如果路由更新后还 找不到 Broker 信息,则抛出 MQClientExcetion ,提示 Broker不
存在
if (!(msg instanceof MessageBatch)) {
MessageClientIDSetter.setUniqID(msg);
}
boolean topicWithNamespace = false;
if (null != this.mQClientFactory.getClientConfig().getNamespace()) {
msg.setInstanceId(this.mQClientFactory.getClientConfig().getNamespace());
topicWithNamespace = true;
}
int sysFlag = 0;
boolean msgBodyCompressed = false;
if (this.tryToCompressMessage(msg)) {
sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
msgBodyCompressed = true;
}
final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
}
-
为消息分配全局唯一Id ,如果消息 默认超 4K(compressMsgBodyOverHowmuch), 会对消息体采用 zip 压缩,并设置消息的系统标记为 MessageSysFlag.COMPRESSED_FLAG,如果是事务 Prepared 消息,则设 消息的系统标记为 MessageSysF ag .TRANSACTION_
PREPARED_TYPE
SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
requestHeader.setTopic(msg.getTopic());
requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
requestHeader.setQueueId(mq.getQueueId());
requestHeader.setSysFlag(sysFlag);
requestHeader.setBornTimestamp(System.currentTimeMillis());
requestHeader.setFlag(msg.getFlag());
requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
requestHeader.setReconsumeTimes(0);
requestHeader.setUnitMode(this.isUnitMode());
requestHeader.setBatch(msg instanceof MessageBatch);
if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
String reconsumeTimes = MessageAccessor.getReconsumeTime(msg);
if (reconsumeTimes != null) {
requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes));
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME);
}
String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(msg);
if (maxReconsumeTimes != null) {
requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes));
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES);
}
}
3.:构建消息发送请求 主要包含如下重要信息:生产者组、主题名称、默认创建主题 Key 、该主题在单个 Broker 默认 队列数、队 ID (队列序号)、消息系统标( MessageSysFlag )消息发送时间 、消息标记( RocketMQ 对消息中的 flag 不做任何处理供应用程序使用) 消息扩展属性 、消息重试次数、是否是批量消息等等
SendResult sendResult = null;
switch (communicationMode) {
case ASYNC:
sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
brokerAddr,
mq.getBrokerName(),
tmpMessage,
requestHeader,
timeout - costTimeAsync,
communicationMode,
sendCallback,
topicPublishInfo,
this.mQClientFactory,
this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(),
context,
this);
break;
case ONEWAY:
case SYNC:
sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
brokerAddr,
mq.getBrokerName(),
msg,
requestHeader,
timeout - costTimeSync,
communicationMode,
context,
this);
break;
default:
assert false;
break;
}
- 根据发送方式,同步、异步、单向方式进行网络传输