RocketMQ(4.8.0)——消息发送流程
RocketMQ(4.8.0)——消息发送流程
一、概述
RocketMQ客户端的消息发送通常分为以下3层:
-
- 业务层:通常指直接调用 RocketMQ Client 发送API的业务代码。
- 消息处理层:指 RocketMQ Client 获取业务发送的消息对象后,一系列的参数检查、消息发送准备、参数包装等操作。
- 通信层:指 RocketMQ 基于 Netty 封装的一个 RPC 通信服务,RocketMQ 各个组件之间的通信全部使用该通信层。
总体来讲,消息发送流程首先是 RocketMQ 客户端接收业务层消息,然后通过 DefaultMQProducerImpl 发送一个 RPC 请求给 Broker,再由 Broker 处理请求并保存消息。
二、消息发送流程
以DefaultMQProducerImpl.send(Message msg)接口为例,讲解发送流程,代码路径:D:\rocketmq-master\client\src\main\java\org\apache\rocketmq\client\impl\producer\DefaultMQProducerImpl.java。
消息发送流程具体分为3步:
第一步:调用 defaultMQProducerImpl.send() 方法发送消息。
第二步:通过设置的发送超时时间,调用 defaultMQProducerImpl.send() 方法发送消息。设置的超时时间可以通过 sendMsgTimeout 进行变更,其默认值为3s。
第三步:执行defaultMQProducerImpl.sendDefaultImpl()方法。这是一个公共发送方法。
communicationMode:通信模式,同步、异步、单向。
sendCallback:对于异步模式,需要设置发送完成后的回调。该方法是发送消息的核心方法,执行过程分为 5 步:
第1步,两个检查:
-
- 生产者状态:没有运行的生产者不能发送消息。
- 消息及消息内容:消息检查主要检查消息是否为空,消息的 Topic 的名字是否为空或者是否符合规范,消息体大小是否符合要求,最大值为4MB,可以通过 maxMessageSize 进行设置。
第2步,执行 tryToFindTopicPublishInfo()方法:获取 Topic 路由信息,如果不存在则发出异常提醒用户。如果本地缓存没有路由信息,就通过 Namesrv 获取路由信息,更新到本地,再返回,
代码路径:D:\rocketmq-master\client\src\main\java\org\apache\rocketmq\client\impl\producer\DefaultMQProducerImpl.java,代码如下:
1 private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) { 2 TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic); 3 if (null == topicPublishInfo || !topicPublishInfo.ok()) { 4 this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo()); 5 this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic); 6 topicPublishInfo = this.topicPublishInfoTable.get(topic); 7 } 8 9 if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) { 10 return topicPublishInfo; 11 } else { 12 this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer); 13 topicPublishInfo = this.topicPublishInfoTable.get(topic); 14 return topicPublishInfo; 15 } 16 }
第3步,计算消息发送的重试次数,同步重试和异步重试的执行方法是不同的。
第4步,执行队列选择方法 selectOneMessageQueue()。根据队列对象中保存的上次发送消息的 Broker 的名字和 Topic 路由,选择 (轮询) 一个 Queue 将消息发送到 Broker。我们可以通过 sendLatencyFaultEnable 来设置是否总是发送到延迟级别较低的 Broker,默认值为 False。
第5步,执行 sendKernelImpl()方法。该方法是发送消息的核心方法。主要用于准备通信层的入参(比如 Broker 地址、请求体等),将请求传递给通信层,内部实现是基于 Netty 的,在封装为通信层 request 对象 RemotingCommand 前,会设置 RequestCode 表示当前请求是发送单个消息还是批量消息,代码路径:D:\rocketmq-master\client\src\main\java\org\apache\rocketmq\client\impl\producer\DefaultMQProducerImpl.java,代码如下:
1 if (sendSmartMsg || msg instanceof MessageBatch) { 2 SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader); 3 request = RemotingCommand.createRequestCommand(msg instanceof MessageBatch ? RequestCode.SEND_BATCH_MESSAGE : RequestCode.SEND_MESSAGE_V2, requestHeaderV2); 4 } else { 5 request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader); 6 }
无论请求发送成功与否,都执行 updateFaultItem() 方法,这就是第3步中讲的总是发送消息延迟级别较低的 Broker 的逻辑。
三、发送消息最佳实践
3.1 发送普通消息
普通消息,也叫并发消息,是发送效率最高、使用场景最多的一类消息,代码路径:D:\rocketmq-master\example\src\main\java\org\apache\rocketmq\example\quickstart\Producer.java,代码如下:
1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.rocketmq.example.quickstart; 18 19 import org.apache.rocketmq.client.exception.MQClientException; 20 import org.apache.rocketmq.client.producer.DefaultMQProducer; 21 import org.apache.rocketmq.client.producer.SendResult; 22 import org.apache.rocketmq.common.message.Message; 23 import org.apache.rocketmq.remoting.common.RemotingHelper; 24 25 /** 26 * This class demonstrates how to send messages to brokers using provided {@link DefaultMQProducer}. 27 */ 28 public class Producer { 29 public static void main(String[] args) throws MQClientException, InterruptedException { 30 31 /* 32 * Instantiate with a producer group name. 33 */ 34 DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); 35 36 /* 37 * Specify name server addresses. 38 * <p/> 39 * 40 * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR 41 * <pre> 42 * {@code 43 * producer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876"); 44 * } 45 * </pre> 46 */ 47 48 /* 49 * Launch the instance. 50 */ 51 producer.start(); 52 53 for (int i = 0; i < 1000; i++) { 54 try { 55 56 /* 57 * Create a message instance, specifying topic, tag and message body. 58 */ 59 Message msg = new Message("TopicTest" /* Topic */, 60 "TagA" /* Tag */, 61 ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ 62 ); 63 64 /* 65 * Call send message to deliver message to one of brokers. 66 */ 67 SendResult sendResult = producer.send(msg); 68 69 System.out.printf("%s%n", sendResult); 70 } catch (Exception e) { 71 e.printStackTrace(); 72 Thread.sleep(1000); 73 } 74 } 75 76 /* 77 * Shut down once the producer instance is not longer in use. 78 */ 79 producer.shutdown(); 80 } 81 }
3.2 发送顺序消息
消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。
顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。
下面用订单进行分区有序的示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。
1 package org.apache.rocketmq.example.order2; 2 3 import org.apache.rocketmq.client.producer.DefaultMQProducer; 4 import org.apache.rocketmq.client.producer.MessageQueueSelector; 5 import org.apache.rocketmq.client.producer.SendResult; 6 import org.apache.rocketmq.common.message.Message; 7 import org.apache.rocketmq.common.message.MessageQueue; 8 9 import java.text.SimpleDateFormat; 10 import java.util.ArrayList; 11 import java.util.Date; 12 import java.util.List; 13 14 /** 15 * Producer,发送顺序消息 16 */ 17 public class Producer { 18 19 public static void main(String[] args) throws Exception { 20 DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); 21 22 producer.setNamesrvAddr("127.0.0.1:9876"); 23 24 producer.start(); 25 26 String[] tags = new String[]{"TagA", "TagC", "TagD"}; 27 28 // 订单列表 29 List<OrderStep> orderList = new Producer().buildOrders(); 30 31 Date date = new Date(); 32 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 33 String dateStr = sdf.format(date); 34 for (int i = 0; i < 10; i++) { 35 // 加个时间前缀 36 String body = dateStr + " Hello RocketMQ " + orderList.get(i); 37 Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, body.getBytes()); 38 39 SendResult sendResult = producer.send(msg, new MessageQueueSelector() { 40 @Override 41 public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { 42 Long id = (Long) arg; //根据订单id选择发送queue 43 long index = id % mqs.size(); 44 return mqs.get((int) index); 45 } 46 }, orderList.get(i).getOrderId());//订单id 47 48 System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s", 49 sendResult.getSendStatus(), 50 sendResult.getMessageQueue().getQueueId(), 51 body)); 52 } 53 54 producer.shutdown(); 55 } 56 57 /** 58 * 订单的步骤 59 */ 60 private static class OrderStep { 61 private long orderId; 62 private String desc; 63 64 public long getOrderId() { 65 return orderId; 66 } 67 68 public void setOrderId(long orderId) { 69 this.orderId = orderId; 70 } 71 72 public String getDesc() { 73 return desc; 74 } 75 76 public void setDesc(String desc) { 77 this.desc = desc; 78 } 79 80 @Override 81 public String toString() { 82 return "OrderStep{" + 83 "orderId=" + orderId + 84 ", desc='" + desc + '\'' + 85 '}'; 86 } 87 } 88 89 /** 90 * 生成模拟订单数据 91 */ 92 private List<OrderStep> buildOrders() { 93 List<OrderStep> orderList = new ArrayList<OrderStep>(); 94 95 OrderStep orderDemo = new OrderStep(); 96 orderDemo.setOrderId(15103111039L); 97 orderDemo.setDesc("创建"); 98 orderList.add(orderDemo); 99 100 orderDemo = new OrderStep(); 101 orderDemo.setOrderId(15103111065L); 102 orderDemo.setDesc("创建"); 103 orderList.add(orderDemo); 104 105 orderDemo = new OrderStep(); 106 orderDemo.setOrderId(15103111039L); 107 orderDemo.setDesc("付款"); 108 orderList.add(orderDemo); 109 110 orderDemo = new OrderStep(); 111 orderDemo.setOrderId(15103117235L); 112 orderDemo.setDesc("创建"); 113 orderList.add(orderDemo); 114 115 orderDemo = new OrderStep(); 116 orderDemo.setOrderId(15103111065L); 117 orderDemo.setDesc("付款"); 118 orderList.add(orderDemo); 119 120 orderDemo = new OrderStep(); 121 orderDemo.setOrderId(15103117235L); 122 orderDemo.setDesc("付款"); 123 orderList.add(orderDemo); 124 125 orderDemo = new OrderStep(); 126 orderDemo.setOrderId(15103111065L); 127 orderDemo.setDesc("完成"); 128 orderList.add(orderDemo); 129 130 orderDemo = new OrderStep(); 131 orderDemo.setOrderId(15103111039L); 132 orderDemo.setDesc("推送"); 133 orderList.add(orderDemo); 134 135 orderDemo = new OrderStep(); 136 orderDemo.setOrderId(15103117235L); 137 orderDemo.setDesc("完成"); 138 orderList.add(orderDemo); 139 140 orderDemo = new OrderStep(); 141 orderDemo.setOrderId(15103111039L); 142 orderDemo.setDesc("完成"); 143 orderList.add(orderDemo); 144 145 return orderList; 146 } 147 }
1 package org.apache.rocketmq.example.order2; 2 3 import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; 4 import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; 5 import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; 6 import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; 7 import org.apache.rocketmq.common.consumer.ConsumeFromWhere; 8 import org.apache.rocketmq.common.message.MessageExt; 9 10 import java.util.List; 11 import java.util.Random; 12 import java.util.concurrent.TimeUnit; 13 14 /** 15 * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交) 16 */ 17 public class ConsumerInOrder { 18 19 public static void main(String[] args) throws Exception { 20 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3"); 21 consumer.setNamesrvAddr("127.0.0.1:9876"); 22 /** 23 * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br> 24 * 如果非第一次启动,那么按照上次消费的位置继续消费 25 */ 26 consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); 27 28 consumer.subscribe("TopicTest", "TagA || TagC || TagD"); 29 30 consumer.registerMessageListener(new MessageListenerOrderly() { 31 32 Random random = new Random(); 33 34 @Override 35 public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { 36 context.setAutoCommit(true); 37 for (MessageExt msg : msgs) { 38 // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序 39 System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody())); 40 } 41 42 try { 43 //模拟业务逻辑处理中... 44 TimeUnit.SECONDS.sleep(random.nextInt(10)); 45 } catch (Exception e) { 46 e.printStackTrace(); 47 } 48 return ConsumeOrderlyStatus.SUCCESS; 49 } 50 }); 51 52 consumer.start(); 53 54 System.out.println("Consumer Started."); 55 } 56 }
3.3 发送延迟消息
生产者发送消息后,消费者在指定时候才能消费消息,这类消息被称为延迟消息或定时消息。生产者发送延迟消息前需要设置延迟级别,目前开源版本支持18个延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h (D:\rocketmq-master\store\src\main\java\org\apache\rocketmq\store\config\MessageStoreConfig.java)
Broker 在接收用户发送的消息后,首先将消息保存到名为 SCHEDULE_TOPIC_XXXXX 的 Topic 中。此时,消费者无法消费该延迟消息。然后,由 Broker 端的定时投递任务定时投递给消费者。
1 import org.apache.rocketmq.client.producer.DefaultMQProducer; 2 import org.apache.rocketmq.common.message.Message; 3 4 public class ScheduledMessageProducer { 5 public static void main(String[] args) throws Exception { 6 // 实例化一个生产者来产生延时消息 7 DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); 8 // 启动生产者 9 producer.start(); 10 int totalMessagesToSend = 100; 11 for (int i = 0; i < totalMessagesToSend; i++) { 12 Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes()); 13 // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel) 14 message.setDelayTimeLevel(3); 15 // 发送消息 16 producer.send(message); 17 } 18 // 关闭生产者 19 producer.shutdown(); 20 } 21 }
3.4 发送事务消息
事务消息的发送、消费流程和延迟消息类似,都是先发送到对消费者不可见的 Topic 中。当事务消息被生产者提交后,会被二次投递到原始 Topic 中,此时消费者正常消费。
事务消息共有3种状态:
-
- TransactionStatus.CommitTransaction: 提交状态:提交事务,它允许消费者消费此消息。
- TransactionStatus.RollbackTransaction:回滚状态:回滚事务,它代表该消息将被删除,不允许被消费。
- TransactionStatus.Unknown: 中间状态:它代表需要检查消息队列来确定状态。
事务消息的发送具体分为2个步骤:
第1步:用户发送一个Half消息到 Broker,Broker 设置 queueOffset=0,即对消费者不可见。
第2步:用户本地事务处理成功,发送一个 Commit 消息到 Broker,Broker 修改 queueOffset 为正常值,达到重新投递的目的,此时消费者可以正常消费;如果本地事务处理失败,那么将发送一个 Rollback 消息给 Broker,Broker 将删除 Half 消息。
如果生产者忘记提交或回滚的话,那么Broker会定期回查生产者,确认生产者本地事务的执行状态,再决定是提交、回滚还是删除 Half 消息。
3.5 发送单向消息
单向消息的生产者只管发送过程,不管发送结果。单向消息主要用于日志传输等消息允许丢失的场景。
1 public class OnewayProducer { 2 public static void main(String[] args) throws Exception{ 3 // Instantiate with a producer group name 4 DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); 5 // Specify name server addresses 6 producer.setNamesrvAddr("localhost:9876"); 7 // Launch the producer instance 8 producer.start(); 9 for (int i = 0; i < 100; i++) { 10 // Create a message instance with specifying topic, tag and message body 11 Message msg = new Message("TopicTest" /* Topic */, 12 "TagA" /* Tag */, 13 ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ 14 ); 15 // Send in one-way mode, no return result 16 producer.sendOneway(msg); 17 } 18 // Shut down once the producer instance is not longer in use 19 producer.shutdown(); 20 } 21 }
3.6 批量消息发送
批量发送消息能显著提高传递小消息的性能。批量消息发送有以下3点注意事项(https://github.com/apache/rocketmq/blob/master/docs/cn/RocketMQ_Example.md#4-%E6%89%B9%E9%87%8F%E6%B6%88%E6%81%AF%E6%A0%B7%E4%BE%8B):
(1) 这一批消息总大小不应该超过4MB。
(2) 同一批批量消息的 Topic、waitStoreMsgOK 属性必须一致。
(3) 批量消息不支持延迟消息。
3.6.1 发送批量消息
1 String topic = "BatchTest"; 2 List<Message> messages = new ArrayList<>(); 3 messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes())); 4 messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes())); 5 messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes())); 6 try { 7 producer.send(messages); 8 } catch (Exception e) { 9 e.printStackTrace(); 10 //处理error 11 }
3.6.2 消息列表分割
复杂度只有当你发送大批量时才会增长,你可能不确定它是否超过了大小限制(4MB)。这时候你最好把你的消息列表分割一下:
1 public class ListSplitter implements Iterator<List<Message>> { 2 private final int SIZE_LIMIT = 1024 * 1024 * 4; 3 private final List<Message> messages; 4 private int currIndex; 5 public ListSplitter(List<Message> messages) { 6 this.messages = messages; 7 } 8 @Override public boolean hasNext() { 9 return currIndex < messages.size(); 10 } 11 @Override public List<Message> next() { 12 int startIndex = getStartIndex(); 13 int nextIndex = startIndex; 14 int totalSize = 0; 15 for (; nextIndex < messages.size(); nextIndex++) { 16 Message message = messages.get(nextIndex); 17 int tmpSize = calcMessageSize(message); 18 if (tmpSize + totalSize > SIZE_LIMIT) { 19 break; 20 } else { 21 totalSize += tmpSize; 22 } 23 } 24 List<Message> subList = messages.subList(startIndex, nextIndex); 25 currIndex = nextIndex; 26 return subList; 27 } 28 private int getStartIndex() { 29 Message currMessage = messages.get(currIndex); 30 int tmpSize = calcMessageSize(currMessage); 31 while(tmpSize > SIZE_LIMIT) { 32 currIndex += 1; 33 Message message = messages.get(curIndex); 34 tmpSize = calcMessageSize(message); 35 } 36 return currIndex; 37 } 38 private int calcMessageSize(Message message) { 39 int tmpSize = message.getTopic().length() + message.getBody().length(); 40 Map<String, String> properties = message.getProperties(); 41 for (Map.Entry<String, String> entry : properties.entrySet()) { 42 tmpSize += entry.getKey().length() + entry.getValue().length(); 43 } 44 tmpSize = tmpSize + 20; // 增加⽇日志的开销20字节 45 return tmpSize; 46 } 47 } 48 //把大的消息分裂成若干个小的消息 49 ListSplitter splitter = new ListSplitter(messages); 50 while (splitter.hasNext()) { 51 try { 52 List<Message> listItem = splitter.next(); 53 producer.send(listItem); 54 } catch (Exception e) { 55 e.printStackTrace(); 56 //处理error 57 } 58 }