RocketMQ-发送消息
迪丽瓦拉
2024-06-03 09:03:42
0

源码版本号:版本号:4.9.4

启动流程

  1. 生成 MQClientInstance
  2. 将当前生产者注册到MQClientInstance的producerTable中
  3. 启动MQClientInstance
  4. 给broker发送心跳信息

启动生产者

public class Producer {public static void main(String[] args) throws Exception {DefaultMQProducer producer = new DefaultMQProducer("producerGroupTest");producer.setNamesrvAddr("localhost:9876");// 启动生产者实例producer.start();}
}

DefaultMQProducer#start方法内部调用的是DefaultMQProducerImpl#start方法

public class DefaultMQProducerImpl implements MQProducerInner {/*** 找到184行*/public void start() throws MQClientException {this.start(true);}/*** 找到188行*/public void start(final boolean startFactory) throws MQClientException {switch (this.serviceState) {case CREATE_JUST:this.serviceState = ServiceState.START_FAILED;/*** 1.校验producerGroup不能为空* 2.校验producerGroup长度不能超过最大值, 默认为255* 3.校验producerGroup是否包含非法字符*/this.checkConfig();// 如果 producerGroup != "CLIENT_INNER_PRODUCER"if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {// 计算实例名称this.defaultMQProducer.changeInstanceNameToPID();}// 获取 MQClientInstance 实例this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);// 将生产者注册到 MQClientInstance 的 producerTable 中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);}// 往 topicPublishInfoTable 放入了默认topic=TBW102的信息,this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());// 启动 MQClientInstanceif (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;}// 给broker发送心跳信息this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();// 开启一个定时任务清理超时的RPC请求RequestFutureHolder.getInstance().startScheduledTask(this);}
}

计算实例名称

DefaultMQProducer 继承自 ClientConfig,计算实例名称代码如下所示

public class ClientConfig {/*** 找到110行*/public void changeInstanceNameToPID() {if (this.instanceName.equals("DEFAULT")) {// 后面还拼接了一个纳秒this.instanceName = UtilAll.getPid() + "#" + System.nanoTime();}}/*** 找到75行* 这里是生成一个实例id*/public String buildMQClientId() {StringBuilder sb = new StringBuilder();sb.append(this.getClientIP());sb.append("@");sb.append(this.getInstanceName());if (!UtilAll.isBlank(this.unitName)) {sb.append("@");sb.append(this.unitName);}if (enableStreamRequestType) {sb.append("@");sb.append(RequestType.STREAM);}return sb.toString();}
}

获取MQClientInstance实例

public class MQClientManager {/*** 找到47行* 生产者和消费者都会调用这个方法生成MQClientInstance*/public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {/*** 每个生产者活消费者都有一个唯一的clientId, 组成形式:IP@InstanceName* IP地址在某些情况下却有可能会重复, 而InstanceName由于加了精确到纳秒的时间戳, 非常极端的情况才可能会重复*/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;}
}

instanceName由进程pid当前纳秒时间戳拼接,clientIdIPinstanceName组成。

在2021年3月以前的版本中,instanceName没有拼接当前纳秒时间戳
相当于同一个JVM进程中所有的生产者和消费者使用的是同一个MQClientInstance来管理。
instanceName拼接当前纳秒时间戳,使得clientId唯一,同一个JVM中每个生产者和消费者都独立拥有一个MQClientInstance

消息发送流程

  1. 构造消息
  2. 校验消息
    topic不能为空,并且topic的长度不能大于127(默认值),消息内容不能为空,并且消息长度不能超过4MB
  3. 获取topic的队列信息。每个生产者都维护了一个Map,key为topic名称,value为topic的队列信息,这些信息可以知道队列在哪个broker上以及broker对应的地址
    如果Map中不存在,则会从NameServer中拉取topic对应的队列信息
  4. 第3步已经拿到了topic的所有队列信息,需要从队列中选出一个进行发送

发送消息

public class Producer {public static void main(String[] args) throws Exception {DefaultMQProducer producer = new DefaultMQProducer("producerGroupTest");producer.setNamesrvAddr("localhost:9876");// 启动Producer实例producer.start();byte[] bytes = ("消息内容").getBytes(RemotingHelper.DEFAULT_CHARSET);Message msg = new Message("TopicTest001","TagA", UUID.randomUUID().toString(), bytes);SendResult sendResult = producer.send(msg);// 通过sendResult返回消息是否成功送达System.out.printf("%s%n", sendResult);}
}

构造消息

消息类Message字段信息如下,

public class Message implements Serializable {// 代表一类消息的集合, 即将消息发送到哪里private String topic;private int flag;// 放置一些配置信息:如tags、keysprivate Map properties;// 消息内容private byte[] body;// 则是使用事务消息时的相关字段private String transactionId;// 构造函数public Message(String topic, String tags, String keys, int flag, byte[] body, boolean waitStoreMsgOK) {this.topic = topic;this.flag = flag;this.body = body;// 设置tags. 最终保存到 properties 属性中, key为 MessageConst.PROPERTY_TAGSif (tags != null && tags.length() > 0) {this.setTags(tags);}// 保存keys. 最终保存到 properties 属性中, key为 MessageConst.PROPERTY_KEYSif (keys != null && keys.length() > 0) {this.setKeys(keys);}// waitStoreMsgOK 表示是否要在这条 Message 落到磁盘上之后才返回应答 // 该变量的值默认为 true// 最终保存到 properties 属性中, key为 MessageConst.PROPERTY_WAIT_STORE_MSG_OKthis.setWaitStoreMsgOK(waitStoreMsgOK);}
}

消息发送方式

SYNC:生产者发送消息到Broker, 需要等待Broker返回发送结果, 才能进行下一条消息的发送

ASYNC:不需要等待Broker返回发送结果, 通过回调的方式来处理Broker的响应

ONEWAY:只管发送消息, 不会关心Broker的返回, 也没有任何回调函数

发送消息入口

发送消息的入口是 DefaultMQProducerImpl#sendDefaultImpl 方法

public class DefaultMQProducerImpl implements MQProducerInner {/*** 版本号:4.9.4* 找到536行* @param msg 消息* @param communicationMode 发送发送:SYNC ASYNC ONEWAY* @param sendCallback 回调函数* @param timeout 超时时间, 默认是3000毫秒*/private SendResult sendDefaultImpl(Message msg,final CommunicationMode communicationMode,final SendCallback sendCallback,final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {// 确保生产者已经启动成功, 即生产者的状态为ServiceState.RUNNINGthis.makeSureStateOK();/*** 校验消息* 1.topic不能为空, 并且topic的长度不能大于127(默认值)* 2.消息内容不能为空, 并且消息长度不能超过4MB*/Validators.checkMessage(msg, this.defaultMQProducer);// 记录当前处理时间, 后面用来判断是否超时long beginTimestampFirst = System.currentTimeMillis();long beginTimestampPrev = beginTimestampFirst;long endTimestamp = beginTimestampFirst;/*** 获取topic的信息, 主要是获取topic的队列(MessageQueue)信息, MessageQueue包含[topic,brokerName,queueId]* 生产者会从MessageQueue列表中选取一个队列进行发送* DefaultMQProducerImpl中的topicPublishInfoTable保存每个topic对应的队列信息* 首次发送时, topicPublishInfoTable中没有, 会从NameServer中获取, 然后保存到这个Map中. * 也会有定时任务不断地去更新这个Map, 具体的获取及定时任务的执行后面再做分析*/TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());// 判断获取到的信息是否是可用的, 即TopicPublishInfo里面的消息队列不为空if (topicPublishInfo != null && topicPublishInfo.ok()) {// 是否超时标志boolean callTimeout = false;// 记录上一次选中的队列, 如果首次发送失败, 在重试过程中则会使用到这个信息MessageQueue mq = null;Exception exception = null;SendResult sendResult = null;/*** 设置最大请求次数*  如果为同步: timesTotal = 1 + 2,有两次重试次数*  如果为异步: timesTotal = 1, 重试操作会在回调接口中进行*/int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;int times = 0;// 记录每次消息发送到哪个brokerString[] brokersSent = new String[timesTotal];for (; times < timesTotal; times++) {/*** 获取上一次发送队列的brokerName, 如果是首次发送, lastBrokerName = null* 这个主要是在选队列的时候会有用到* 比如发送消息给broker-a失败了, 那么第二次重试的时候就不选broker-a的队列 */String lastBrokerName = null == mq ? null : mq.getBrokerName();// 选取一个队列, 具体的选取逻辑后面再分析MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);if (mqSelected != null) {// 将选到的队列赋值给mqmq = mqSelected;// 将每次发送到哪个broker记录下来brokersSent[times] = mq.getBrokerName();try {beginTimestampPrev = System.currentTimeMillis();/*** 省略一些代码......这里会计算是否已经超时, 如果已经超时则直接退出并抛出异常*//*** 第575行* 发送消息*/sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);endTimestamp = System.currentTimeMillis();/*** 如果启用了故障延迟机制* 第三个参数为false: 使用(endTimestamp - beginTimestampPrev)作为broker故障规避时长* 相当于(endTimestamp - beginTimestampPrev)s内不会选这个broker队列* endTimestamp - beginTimestampPrev = 发送消息的延迟时间*/this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);/*** 省略......* 如果发送方式是SYNC, 则返回sendResult, 否则返回null*/} catch (Exception e) {/*** 这里有很多类型的异常, 为了查看方便, 将其省略了...* 处理逻辑都差不多*/endTimestamp = System.currentTimeMillis();/*** 发送失败* 如果启用了故障延迟机制* 第三个参数为true: 默认使用30s作为broker故障规避时长, 相当于30s内不会选这个broker的所有队列*/this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);// 省略打印日志信息exception = e;continue;}// 省略......}}}// 如果获取不到 topicPublishInfo 则抛出异常}
}

tryToFindTopicPublishInfo

在发送消息的过程中, 会通过topic获取对应的消息队列信息

public class DefaultMQProducerImpl implements MQProducerInner {/*** 保存每个topic对应的消息队列信息*/private final ConcurrentMap topicPublishInfoTable =new ConcurrentHashMap();/*** MQClientInstance实例*/private MQClientInstance mQClientFactory;/*** 版本号:4.9.4* 找到671行*/private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {// 先从缓存中获取TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);if (null == topicPublishInfo || !topicPublishInfo.ok()) {this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());/*** 从NameServer中去获取* NameServer为什么会有这些topic数据呢?每个broker启动后, 都会定时地向NameServer发送自己哪有些topic信息*/this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);topicPublishInfo = this.topicPublishInfoTable.get(topic);}if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {return topicPublishInfo;} else {/*** 如果前面没有获取到, 说明NameServer中没有这个topic信息, 即这个topic还没有被创建* 这里的第二个参数传true, 代表去查询默认的topic信息, 即查询TBW102(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC)* TBW102是一个默认的topic*/this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);topicPublishInfo = this.topicPublishInfoTable.get(topic);return topicPublishInfo;}}
}

selectOneMessageQueue

从队列中选择一个要发送消息的队列

最终执行到MQFaultStrategy#selectOneMessageQueue()方法,选取一个要发送的队列

public class MQFaultStrategy {// 是否开启故障延迟机制, 默认为falseprivate boolean sendLatencyFaultEnable = false;/*** 找到58行* @param tpInfo * @param lastBrokerName * 上一次发送消息的brokerName, 首次进来为null, 第二次进来说明上一次发送失败了*/public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {if (this.sendLatencyFaultEnable) {try {/*** 省略代码......* 遍历 tpInfo.getMessageQueueList(), 判断每一个队列对应的brokerName是否是可用的* 如果存在可用的则直接返回, 这里如何判断是否可用?* 因为开启了故障延迟, 如果发送失败了, 则会调用MQFaultStrategy#updateFaultItem方法* 记录发送失败的这个brokerName在一段时间内不可用, 这样我们就能查到某个brokerName是否可用了*//*** 省略代码......* 走到这里说明上一步没有找到能发送消息的可用队列* 根据一定的规则获取一个队列信息* 如果存在可用的则直接返回*/} catch (Exception e) {log.error("Error occurred when selecting message queue", e);}/*** 走到这里, 说明在故障延迟机制下, 没有找到* 该方法比较简单:根据内部的一个自增值跟队列长度进行取模运算得到一个索引下标*/return tpInfo.selectOneMessageQueue();}/*** 默认情况会走到这里* 该方法比较简单:* 1.如果lastBrokerName为null, 则根据内部的一个自增值跟队列长度进行取模运算得到一个索引下标* 2.如果lastBrokerName不为null, 则遍历队列列表, 找到一个不等于lastBrokerName的队列*  如果找不到, 则按照lastBrokerName为null的情况选择一个队列*/return tpInfo.selectOneMessageQueue(lastBrokerName);}
}

TopicPublishInfo信息如下

{"haveTopicRouterInfo": true,"messageQueueList": [{"brokerName": "broker-a","queueId": 0,"topic": "Topic20230221"}, {"brokerName": "broker-a","queueId": 1,"topic": "Topic20230221"}, {"brokerName": "broker-a","queueId": 2,"topic": "Topic20230221"}, {"brokerName": "broker-a","queueId": 3,"topic": "Topic20230221"}],"orderTopic": false,"sendWhichQueue": {},"topicRouteData": {"brokerDatas": [{"brokerAddrs": {0: "10.39.172.135:10911"},"brokerName": "broker-a","cluster": "DefaultCluster"}],"filterServerTable": {},"queueDatas": [{"brokerName": "broker-a","perm": 6,"readQueueNums": 4,"topicSysFlag": 0,"writeQueueNums": 4}]}
}

总结

消息队列负载机制:消息生产者在发送消息时,如果本地路由表中未缓存topic的路由信息,向NameServer发送获取路由信息请求,更新本地路由信息表,并且会有定时任务每隔30s从NameServer更新路由表。

消息发送异常机制:消息发送高可用主要通过两个手段:重试与Broker规避。Broker规避就是在一次消息发送过程中发现错误,在某一时间段内,消息生产者不会选择该Broker(消息服务器)上的消息队列,提高发送消息的成功率。

相关内容