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     }
tryToFindTopicPublishInfo()

  第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             }
View Code

  无论请求发送成功与否,都执行 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 }
View Code

  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 }
public class OnewayProducer ()

  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 }
View Code

    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 }
View Code
posted @ 2021-02-16 23:20  左扬  阅读(502)  评论(0编辑  收藏  举报
levels of contents