Fork 出属于自己的仓库。为什么要 Fork ?既然开始阅读、调试源码,我们可能会写一些注释,有了自己的仓库,可以进行自由的提交。
12 |
13 | 使用 IntelliJ IDEA 从 Fork 出来的仓库拉取代码。拉取完成后,Maven 会下载依赖包,可能会花费一些时间,耐心等待下。
14 |
15 | 在等待的过程中,我来简单说下,搭建调试环境的过程:
16 |
17 | * 启动 RocketMQ Namesrv
18 | * 启动 RocketMQ Broker
19 | * 启动 RocketMQ Producer
20 | * 启动 RocketMQ Consumer
21 |
22 | 最小化的 RocketMQ 的环境,暂时不考虑 Namesrv 集群、Broker 集群、Consumer 集群。
23 |
24 | 另外,本文使用的 RocketMQ 版本是 4.4.0-SNAPSHOT 。
25 |
26 | ## 1.3.启动 RocketMQ Namesrv
27 |
28 | 打开 org.apache.rocketmq.namesrv.NameServerInstanceTest 单元测试类,参考 #startup() 方法,我们编写 #main(String[] args) 静态方法,代码如下:
29 |
30 | ```java
31 | public static void main(String[] args) throws Exception {
32 | // NamesrvConfig 配置
33 | final NamesrvConfig namesrvConfig = new NamesrvConfig();
34 | // NettyServerConfig 配置
35 | final NettyServerConfig nettyServerConfig = new NettyServerConfig();
36 | nettyServerConfig.setListenPort(9876); // 设置端口
37 | // 创建 NamesrvController 对象,并启动
38 | NamesrvController namesrvController = new NamesrvController(namesrvConfig, nettyServerConfig);
39 | namesrvController.initialize();
40 | namesrvController.start();
41 | // 睡觉,就不起来
42 | Thread.sleep(DateUtils.MILLIS_PER_DAY);
43 | }
44 | ```
45 |
46 | 然后,右键运行,RocketMQ Namesrv 就启动完成。输出日志如下:
47 |
48 | ```
49 | 01:45:34.236 [NettyEventExecutor] INFO RocketmqRemoting - NettyEventExecutor service started
50 | 01:45:34.236 [FileWatchService] INFO RocketmqCommon - FileWatchService service started
51 |
52 | ```
53 |
54 | 最后,这是一个可选的步骤,命令行中输入 telnet 127.0.0.1 9876 ,看看是否能连接上 RocketMQ Namesrv 。
55 |
56 | ## 1.4.启动 RocketMQ Broker
57 |
58 | 打开 org.apache.rocketmq.broker.BrokerControllerTest 单元测试类,参考 #testBrokerRestart() 方法,我们编写 #main(String[] args) 方法,代码如下:
59 |
60 | ```java
61 | // BrokerControllerTest.java
62 |
63 | public static void main(String[] args) throws Exception {
64 | // 设置版本号
65 | System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
66 | // NettyServerConfig 配置
67 | final NettyServerConfig nettyServerConfig = new NettyServerConfig();
68 | nettyServerConfig.setListenPort(10911);
69 | // BrokerConfig 配置
70 | final BrokerConfig brokerConfig = new BrokerConfig();
71 | brokerConfig.setBrokerName("broker-a");
72 | brokerConfig.setNamesrvAddr("127.0.0.1:9876");
73 | // MessageStoreConfig 配置
74 | final MessageStoreConfig messageStoreConfig = new MessageStoreConfig();
75 | messageStoreConfig.setDeleteWhen("04");
76 | messageStoreConfig.setFileReservedTime(48);
77 | messageStoreConfig.setFlushDiskType(FlushDiskType.ASYNC_FLUSH);
78 | messageStoreConfig.setDuplicationEnable(false);
79 |
80 | // BrokerPathConfigHelper.setBrokerConfigPath("/Users/yunai/百度云同步盘/开发/Javascript/Story/incubator-rocketmq/conf/broker.conf");
81 | // 创建 BrokerController 对象,并启动
82 | BrokerController brokerController = new BrokerController(//
83 | brokerConfig, //
84 | nettyServerConfig, //
85 | new NettyClientConfig(), //
86 | messageStoreConfig);
87 | brokerController.initialize();
88 | brokerController.start();
89 | // 睡觉,就不起来
90 | System.out.println("你猜");
91 | Thread.sleep(DateUtils.MILLIS_PER_DAY);
92 | }
93 | ```
94 |
95 | 然后,右键运行,RocketMQ Broker 就启动完成了。输出日志如下:
96 |
97 | ```
98 | 你猜
99 | ```
100 |
101 | 不要懵逼,我们打开下 RocketMQ Namesrv 那,已经输出日志如下:
102 |
103 | ```java
104 | 01:52:21.930 [RemotingExecutorThread_1] DEBUG RocketmqNamesrv - receive request, 103 127.0.0.1:62023 RemotingCommand [code=103, language=JAVA, version=293, opaque=0, flag(B)=0, remark=null, extFields={brokerId=0, bodyCrc32=137512002, clusterName=DefaultCluster, brokerAddr=192.0.108.1:10911, haServerAddr=192.0.108.1:10912, compressed=false, brokerName=broker-a}, serializeTypeCurrentRPC=JSON]
105 | 01:52:21.956 [RemotingExecutorThread_1] INFO RocketmqNamesrv - new topic registered, BenchmarkTest QueueData [brokerName=broker-a, readQueueNums=1024, writeQueueNums=1024, perm=6, topicSynFlag=0]
106 | 01:52:21.957 [RemotingExecutorThread_1] INFO RocketmqNamesrv - new topic registered, OFFSET_MOVED_EVENT QueueData [brokerName=broker-a, readQueueNums=1, writeQueueNums=1, perm=6, topicSynFlag=0]
107 | 01:52:21.957 [RemotingExecutorThread_1] INFO RocketmqNamesrv - new topic registered, broker-a QueueData [brokerName=broker-a, readQueueNums=1, writeQueueNums=1, perm=7, topicSynFlag=0]
108 | 01:52:21.957 [RemotingExecutorThread_1] INFO RocketmqNamesrv - new topic registered, TBW102 QueueData [brokerName=broker-a, readQueueNums=8, writeQueueNums=8, perm=7, topicSynFlag=0]
109 | 01:52:21.957 [RemotingExecutorThread_1] INFO RocketmqNamesrv - new topic registered, SELF_TEST_TOPIC QueueData [brokerName=broker-a, readQueueNums=1, writeQueueNums=1, perm=6, topicSynFlag=0]
110 | 01:52:21.957 [RemotingExecutorThread_1] INFO RocketmqNamesrv - new topic registered, DefaultCluster QueueData [brokerName=broker-a, readQueueNums=16, writeQueueNums=16, perm=7, topicSynFlag=0]
111 | 01:52:21.957 [RemotingExecutorThread_1] INFO RocketmqNamesrv - new broker registered, 192.0.108.1:10911 HAServer: 192.0.108.1:10912
112 | ```
113 |
114 | 妥妥的,原来 RocketMQ Broker 已经启动完成,并且注册到 RocketMQ Namesrv 上。
115 |
116 | 最后,这是一个可选的步骤,命令行中输入 telnet 127.0.0.1 10911 ,看看是否能连接上 RocketMQ Broker 。
117 |
118 | ## 1.5.启动 RocketMQ Producer
119 |
120 | 打开 org.apache.rocketmq.example.quickstart.Producer 示例类,代码如下:
121 |
122 | ```java
123 | /*
124 | * Licensed to the Apache Software Foundation (ASF) under one or more
125 | * contributor license agreements. See the NOTICE file distributed with
126 | * this work for additional information regarding copyright ownership.
127 | * The ASF licenses this file to You under the Apache License, Version 2.0
128 | * (the "License"); you may not use this file except in compliance with
129 | * the License. You may obtain a copy of the License at
130 | *
131 | * http://www.apache.org/licenses/LICENSE-2.0
132 | *
133 | * Unless required by applicable law or agreed to in writing, software
134 | * distributed under the License is distributed on an "AS IS" BASIS,
135 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
136 | * See the License for the specific language governing permissions and
137 | * limitations under the License.
138 | */
139 | package org.apache.rocketmq.example.quickstart;
140 |
141 | import org.apache.rocketmq.client.exception.MQClientException;
142 | import org.apache.rocketmq.client.producer.DefaultMQProducer;
143 | import org.apache.rocketmq.client.producer.SendResult;
144 | import org.apache.rocketmq.common.message.Message;
145 | import org.apache.rocketmq.remoting.common.RemotingHelper;
146 |
147 | /**
148 | * This class demonstrates how to send messages to brokers using provided {@link DefaultMQProducer}.
149 | */
150 | public class Producer {
151 | public static void main(String[] args) throws MQClientException, InterruptedException {
152 |
153 | /*
154 | * Instantiate with a producer group name.
155 | */
156 | DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
157 |
158 | /*
159 | * Specify name server addresses.
160 | *
161 | *
162 | * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR
163 | *
164 | * {@code
165 | * producer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
166 | * }
167 | *
168 | */
169 |
170 | /*
171 | * Launch the instance.
172 | */
173 | producer.setNamesrvAddr("127.0.0.1:9876"); // 哈哈哈哈
174 | producer.start();
175 |
176 | for (int i = 0; i < 1000; i++) {
177 | try {
178 |
179 | /*
180 | * Create a message instance, specifying topic, tag and message body.
181 | */
182 | Message msg = new Message("TopicTest" /* Topic */,
183 | "TagA" /* Tag */,
184 | ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
185 | );
186 |
187 | /*
188 | * Call send message to deliver message to one of brokers.
189 | */
190 | SendResult sendResult = producer.send(msg);
191 |
192 | System.out.printf("%s%n", sendResult);
193 | } catch (Exception e) {
194 | e.printStackTrace();
195 | Thread.sleep(1000);
196 | }
197 | }
198 |
199 | /*
200 | * Shut down once the producer instance is not longer in use.
201 | */
202 | producer.shutdown();
203 | }
204 | }
205 |
206 | ```
207 | 注意,在 哈哈哈哈处,我们增加了 producer.setNamesrvAddr("127.0.0.1:9876") 代码块,指明 Producer 使用的 RocketMQ Namesrv 。
208 |
209 | 然后,右键运行,RocketMQ Producer 就启动完成。输出日志如下:
210 |
211 | ## 1.6.启动 RocketMQ Consumer
212 |
213 | 打开 org.apache.rocketmq.example.quickstart.Consumer 示例类,代码如下:
214 |
215 | ```java
216 | // Consumer.java
217 |
218 | public class Consumer {
219 |
220 | public static void main(String[] args) throws InterruptedException, MQClientException {
221 |
222 | /*
223 | * Instantiate with specified consumer group name.
224 | */
225 | DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
226 |
227 | /*
228 | * Specify name server addresses.
229 | *
230 | *
231 | * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR
232 | *
233 | * {@code
234 | * consumer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
235 | * }
236 | *
237 | */
238 |
239 | /*
240 | * Specify where to start in case the specified consumer group is a brand new one.
241 | */
242 | consumer.setNamesrvAddr("127.0.0.1:9876"); // 哈哈哈哈
243 | consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
244 |
245 | /*
246 | * Subscribe one more more topics to consume.
247 | */
248 | consumer.subscribe("TopicTest", "*");
249 |
250 | /*
251 | * Register callback to execute on arrival of messages fetched from brokers.
252 | */
253 | consumer.registerMessageListener(new MessageListenerConcurrently() {
254 |
255 | @Override
256 | public ConsumeConcurrentlyStatus consumeMessage(List msgs,
257 | ConsumeConcurrentlyContext context) {
258 | System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
259 | return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
260 | }
261 | });
262 |
263 | /*
264 | * Launch the consumer instance.
265 | */
266 | consumer.start();
267 |
268 | System.out.printf("Consumer Started.%n");
269 | }
270 |
271 | }
272 | ```
273 |
274 | 注意,在 哈哈哈哈处,我们还增加了 consumer.setNamesrvAddr("127.0.0.1:9876") 代码块,指明 Consumer 使用的 RocketMQ Namesrv 。
275 |
276 | 然后,右键运行,RocketMQ Consumer 就启动完成。输入日志如下:
277 |
278 | ```
279 | ```
--------------------------------------------------------------------------------
/源码阅读/2.RocketMQ路由中心.md:
--------------------------------------------------------------------------------
1 | >建议购买正版书籍: RocketMQ技术内幕
2 |
3 | # 2.路由中心NameServer
4 |
5 | 本章主要介绍RocketMQ 路由管理、服务注册及服务发现的机制, NameServer 是整个RocketMQ 的“大脑” 。相信大家对“服务发现”这个词语并不陌生,分布式服务SOA 架构体系中会有服务注册中心,分布式服务SOA 的注册中心主要提供服务调用的解析服务,指引服务调用方(消费者)找到“远方”的服务提供者,完成网络通信,那么RocketMQ 的路由中心存储的是什么数据呢?作为一款高性能的消息中间件,如何避免NameServer 的单点故障,提供高可用性呢?让我们带着上述疑问, 一起进入RocketMQ NameServer 的精彩
6 | 世界中来。
7 |
8 | ## 2.1.NameServer 架构设计
9 |
10 | 消息中间件的设计思路一般基于主题的订阅发布机制消息生产者( Producer )发送某一主题的消息到消息服务器,消息服务器负责该消息的持久化存储,消息消费者(Consumer)订阅感兴趣的主题,**消息服务器根据订阅信息(路由信息)将消息推送到消费者( PUSH 模式)或者消息消费者主动向消息服务器拉取消息( PULL 模式),从而实现消息生产者与消息消费者解调**。为了避免消息服务器的单点故障导致的整个系统瘫痪,**通常会部署多台消息服务器共同承担消息的存储**。那消息生产者如何知道消息要发往哪台消息服务器呢?如果某一台消息服务器若机了,那么生产者如何在不重启服务的情况下感知呢?
11 |
12 | NameServer 就是为了解决上述问题而设计的。
13 |
14 | RocketMQ 的逻辑部署图如图2-1 所示。
15 |
16 | 
17 |
18 | Broker 消息服务器在启动时向所有Name Server 注册,消息生产者(Producer)在发送消息之前先从Name Server 获取Broker 服务器地址列表,然后根据负载算法从列表中选择一台消息服务器进行消息发送。NameServer 与每台Broker 服务器保持长连接,并间隔30s 检测Broker 是否存活,如果检测到Broker 右机, 则从路由注册表中将其移除。**但是路由变化不会马上通知消息生产者,为什么要这样设计呢?这是为了降低NameServer 实现的复杂性,在消息发送端提供容错机制来保证消息发送的高可用性,这部分在3.4 节中会有详细的描述**。
19 |
20 | NameServer 本身的高可用可通过部署多台Nameserver 服务器来实现,但彼此之间互不通信,也就是NameServer 服务器之间在某一时刻的数据并不会完全相同,但这对消息发送不会造成任何影响,这也是RocketMQ NameServer 设计的一个亮点, RocketMQNameServer 设计追求简单高效。
21 |
22 | ## 2.2.NameServer 启动流程
23 |
24 | 从源码的角度窥探一下Names 巳rver 启动流程,重点关注Na meServer 相关启动参数. NameServer 启动类: org .apache.rocketrr吨name srv.NamesrvStartup 。
25 |
26 | Step1 : 首先来解析配置文件,需要填充NameServerConfig 、NettyServerConfig 属性值。
27 |
28 | ```java
29 | public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
30 | System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
31 | //PackageConflictDetect.detectFastjson();
32 |
33 | Options options = ServerUtil.buildCommandlineOptions(new Options());
34 | commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
35 | if (null == commandLine) {
36 | System.exit(-1);
37 | return null;
38 | }
39 |
40 | final NamesrvConfig namesrvConfig = new NamesrvConfig();
41 | final NettyServerConfig nettyServerConfig = new NettyServerConfig();
42 | nettyServerConfig.setListenPort(9876);
43 | if (commandLine.hasOption('c')) {
44 | String file = commandLine.getOptionValue('c');
45 | if (file != null) {
46 | InputStream in = new BufferedInputStream(new FileInputStream(file));
47 | properties = new Properties();
48 | properties.load(in);
49 | MixAll.properties2Object(properties, namesrvConfig);
50 | MixAll.properties2Object(properties, nettyServerConfig);
51 |
52 | namesrvConfig.setConfigStorePath(file);
53 |
54 | System.out.printf("load config properties file OK, %s%n", file);
55 | in.close();
56 | }
57 | }
58 |
59 | if (commandLine.hasOption('p')) {
60 | InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
61 | MixAll.printObjectProperties(console, namesrvConfig);
62 | MixAll.printObjectProperties(console, nettyServerConfig);
63 | System.exit(0);
64 | }
65 |
66 | MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
67 |
68 | if (null == namesrvConfig.getRocketmqHome()) {
69 | System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV);
70 | System.exit(-2);
71 | }
72 |
73 | LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
74 | JoranConfigurator configurator = new JoranConfigurator();
75 | configurator.setContext(lc);
76 | lc.reset();
77 | configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");
78 |
79 | log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
80 |
81 | MixAll.printObjectProperties(log, namesrvConfig);
82 | MixAll.printObjectProperties(log, nettyServerConfig);
83 |
84 | final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
85 |
86 | // remember all configs to prevent discard
87 | controller.getConfiguration().registerConfig(properties);
88 |
89 | return controller;
90 | }
91 | ```
92 |
93 | 从代码我们可以知道先创建NameServerConfig ( NameServer 业务参数)、N ettyServerConfig( NameServer 网络参数) , 然后在解析启动时把指定的配置文件或启动命令中的选项值,填充到nameServerConfig , nettyServerConfig 对象。参数来源有如下两种方式。
94 |
95 | 1. -c configFile 通过,c 命令指定配置文件的路径。
96 | 2. 使用“--属性名 属性值”,例如--listenPort 9876 。
97 |
98 |
99 | NameSeverConfig 属性
100 |
101 | ```java
102 | private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
103 | private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
104 | private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties";
105 | private String productEnvName = "center";
106 | private boolean clusterTest = false;
107 | private boolean orderMessageEnable = false;
108 |
109 | ```
110 |
111 | * rocketmqhome: rocketmq 主目录,可以通过-Drocketmq.home.dir=path 或通过设置环境变量ROCKETMQ_HOME 来配置RocketMQ 的主目录。
112 | * kvConfigPath: NameServer 存储KV 配置属性的持久化路径。
113 | * configStorePath:nameServer 默认配置文件路径,不生效。name Server 启动时如果要通过配置文件配置NameServer 启动属性的话,请使用-c 选项。
114 | * orderMessageEnable : 是否支持顺序消息,默认是不支持。
115 |
116 | ```java
117 | private int listenPort = 8888;
118 | private int serverWorkerThreads = 8;
119 | private int serverCallbackExecutorThreads = 0;
120 | private int serverSelectorThreads = 3;
121 | private int serverOnewaySemaphoreValue = 256;
122 | private int serverAsyncSemaphoreValue = 64;
123 | private int serverChannelMaxIdleTimeSeconds = 120;
124 |
125 | private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
126 | private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
127 | private boolean serverPooledByteBufAllocatorEnable = true;
128 | ```
129 |
130 | * listenPort: N ameServer 监昕端口,该值默认会被初始化为9876 0
131 | * serverWorkerThreads: Net ty 业务线程池线程个数。
132 | * serverCallbackExecutorThreads : Netty public 任务线程池线程个数, Netty 网络设计,根据业务类型会创建不同的线程池,比如处理消息发送、消息消费、心跳检测等。如果该业务类型( R e que stCode )未注册线程池, 则由public 线程池执行。
133 | * serverSelectorThreads: IO 线程池线程个数,主要是NameServer 、Brok e r 端解析请求、返回相应的线程个数,这类线程主要是处理网络请求的,解析请求包, 然后转发到各个业务线程池完成具体的业务操作,然后将结果再返回调用方。
134 | * serverOnewaySemaphore Value: send oneway 消息请求井发度( Broker 端参数) 。
135 | * serverAsyncSemaphore Value : 异步消息发送最大并发度( Broker 端参数) 。
136 | * serverChannelMaxld l eTimeSeconds :网络连接最大空闲时间,默认120s 。如果连接空闲时间超过该参数设置的值,连接将被关闭。
137 | * serverSocketSndBufSize :网络socket 发送缓存区大小, 默认64k 。
138 | * serverSocketRcvBufSize :网络socket 接收缓存区大小,默认6 4k 。
139 | * serverPooledByteBufAllocatorEnable: ByteBuffer 是否开启缓存, 建议开启。
140 | * useEpollNativeSelector : 是否启用Epoll IO 模型, Linux 环境建议开启。\
141 |
142 | 在启动NameServer 时,可以先使用./mqnameserver -c configFile -p 打印当前配置属性。
143 |
144 | Step2 :根据启动属性创建NamesrvController 实例,并初始化该实例, NameServerController实例为NameServer核心控制器。
145 |
146 | ```java
147 | public static NamesrvController main0(String[] args) {
148 |
149 | try {
150 | NamesrvController controller = createNamesrvController(args);
151 | start(controller);
152 | String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
153 | log.info(tip);
154 | System.out.printf("%s%n", tip);
155 | return controller;
156 | } catch (Throwable e) {
157 | e.printStackTrace();
158 | System.exit(-1);
159 | }
160 |
161 | return null;
162 | }
163 | public static NamesrvController start(final NamesrvController controller) throws Exception {
164 |
165 | if (null == controller) {
166 | throw new IllegalArgumentException("NamesrvController is null");
167 | }
168 |
169 | boolean initResult = controller.initialize();
170 | if (!initResult) {
171 | controller.shutdown();
172 | System.exit(-3);
173 | }
174 |
175 | Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable() {
176 | @Override
177 | public Void call() throws Exception {
178 | controller.shutdown();
179 | return null;
180 | }
181 | }));
182 |
183 | controller.start();
184 |
185 | return controller;
186 | }
187 | public boolean initialize() {
188 |
189 | this.kvConfigManager.load();
190 |
191 | this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
192 |
193 | this.remotingExecutor =
194 | Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
195 |
196 | this.registerProcessor();
197 |
198 | this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
199 |
200 | @Override
201 | public void run() {
202 | NamesrvController.this.routeInfoManager.scanNotActiveBroker();
203 | }
204 | }, 5, 10, TimeUnit.SECONDS);
205 |
206 | this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
207 |
208 | @Override
209 | public void run() {
210 | NamesrvController.this.kvConfigManager.printAllPeriodically();
211 | }
212 | }, 1, 10, TimeUnit.MINUTES);
213 |
214 | if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
215 | // Register a listener to reload SslContext
216 | try {
217 | fileWatchService = new FileWatchService(
218 | new String[] {
219 | TlsSystemConfig.tlsServerCertPath,
220 | TlsSystemConfig.tlsServerKeyPath,
221 | TlsSystemConfig.tlsServerTrustCertPath
222 | },
223 | new FileWatchService.Listener() {
224 | boolean certChanged, keyChanged = false;
225 | @Override
226 | public void onChanged(String path) {
227 | if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) {
228 | log.info("The trust certificate changed, reload the ssl context");
229 | reloadServerSslContext();
230 | }
231 | if (path.equals(TlsSystemConfig.tlsServerCertPath)) {
232 | certChanged = true;
233 | }
234 | if (path.equals(TlsSystemConfig.tlsServerKeyPath)) {
235 | keyChanged = true;
236 | }
237 | if (certChanged && keyChanged) {
238 | log.info("The certificate and private key changed, reload the ssl context");
239 | certChanged = keyChanged = false;
240 | reloadServerSslContext();
241 | }
242 | }
243 | private void reloadServerSslContext() {
244 | ((NettyRemotingServer) remotingServer).loadSslContext();
245 | }
246 | });
247 | } catch (Exception e) {
248 | log.warn("FileWatchService created error, can't load the certificate dynamically");
249 | }
250 | }
251 |
252 | return true;
253 | }
254 | ```
255 |
256 | 加载KV 配置,创建NettyServer网络处理对象,然后开启两个定时任务,在RocketMQ中此类定时任务统称为心跳检测。
257 |
258 | 加载KV 配置,创建NettyServer 网络处理对象,然后开启两个定时任务,在RocketMQ中此类定时任务统称为心跳检测。
259 |
260 | * 定时任务I: NameServer 每隔I Os 扫描一次Broker , 移除处于不激活状态的Broker c
261 | * 定时任务2: names 巳rver 每隔10 分钟打印一次KV 配置。
262 |
263 | Step3 :注册JVM 钩子函数并启动服务器, 以便监昕Broker 、消息生产者的网络请求。
264 |
265 | ```java
266 | Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable() {
267 | @Override
268 | public Void call() throws Exception {
269 | controller.shutdown();
270 | return null;
271 | }
272 | }));
273 | public void addShutdownHook(Thread hook) {
274 | SecurityManager sm = System.getSecurityManager();
275 | if (sm != null) {
276 | sm.checkPermission(new RuntimePermission("shutdownHooks"));
277 | }
278 | ApplicationShutdownHooks.add(hook);
279 | }
280 | ```
281 |
282 | 这里主要是向读者展示一种常用的编程技巧,如果代码中使用了线程池,一种优雅停机的方式就是注册一个JVM 钩子函数, 在JVM 进程关闭之前,先将线程池关闭,及时释放资源。
283 |
284 | ## 2.3.NameServer 路由注册、故障剔除
285 |
286 | NameServer 主要作用是为消息生产者和消息消费者提供关于主题Topic 的路由信息,那么NameServer 需要存储路由的基础信息,还要能够管理Broker 节点,包括路由注册、路由删除等功能。
287 |
288 | ### 2.3.1.路由元信息
289 |
290 | NameServer 路由实现类: org.apache.rocketmq.namesrv.routeinfo.RoutelnfoManager , 在了解路由注册之前,我们首先看一下NameServer 到底存储哪些信息。
291 |
292 | ```java
293 | private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
294 | private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
295 | private final ReadWriteLock lock = new ReentrantReadWriteLock();
296 | private final HashMap> topicQueueTable;
297 | private final HashMap brokerAddrTable;
298 | private final HashMap> clusterAddrTable;
299 | private final HashMap brokerLiveTable;
300 | private final HashMap/* Filter Server */> filterServerTable;
301 |
302 | ```
303 |
304 | * *topicQueueTable: Topic 消息队列路由信息,消息发送时根据路由表进行负载均衡。
305 | * brokerAddrTable : Broker 基础信息, 包含brokerName 、所属集群名称、主备Broker地址。
306 | * clusterAddrTable: Broker 集群信息,存储集群中所有Broker 名称。
307 | * brokerLiveTable: Broker 状态信息。NameServer 每次收到心跳包时会替换该信息。
308 | * filterServerTable : Broker 上的FilterServer 列表,用于类模式消息过滤,详细介绍请参考第6 章的内容。
309 |
310 | QueueData 、BrokerData 、BrokerLiveinfo 类图如图2-2 所示。
311 |
312 | RocketMQ 基于订阅发布机制, 一个Topic 拥有多个消息队列,一个Broker 为每一主题默认创建4 个读队列4 个写队列。多个Broker 组成一个集群, BrokerName 由相同的多台Broker组成Master-Slave 架构, brokerId 为0 代表Master , 大于0 表示Slave 。BrokerLivelnfo 中的lastUpdateTimestamp 存储上次收到Broker 心跳包的时间。
313 |
314 | 
315 |
316 | RocketMQ2 主2 从部署图如图2-3 所示。
317 |
318 | 
319 |
320 | 对应运行时数据结构如图2-4 和图2-5 所示。
321 |
322 | 
323 |
324 | 
325 |
326 | ### 2.3.2.路由注册
327 |
328 | RocketMQ 路由注册是通过Broker 与Name Server 的心跳功能实现的。Broker 启动时向集群中所有的NameServ巳r 发送心跳语句,每隔3 0s 向集群中所有NameServer 发送心跳包, NameServer 收到Broker 心跳包时会更新brokerLiveTable 缓存中BrokerLiveInfo 的lastUpdateTimestamp ,然后Nam巳Server 每隔10s 扫描brokerLiveTable ,如果连续120s 没有收到心跳包, NameServ er 将移除该Broker 的路由信息同时关闭Socket 连接。
329 |
330 | #### Broker 发送心跳包
331 |
332 | Broker 发送心跳包的核心代码如下所示。
333 |
334 | ```java
335 | //org.apache.rocketmq.broker.BrokerController#start
336 | this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
337 |
338 | @Override
339 | public void run() {
340 | try {
341 | BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
342 | } catch (Throwable e) {
343 | log.error("registerBrokerAll Exception", e);
344 | }
345 | }
346 | }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
347 |
348 | ```
349 |
350 | ```java
351 | org.apache.rocketmq.broker.out.BrokerOuterAPI#registerBrokerAll
352 |
353 | public List registerBrokerAll(
354 | final String clusterName,
355 | final String brokerAddr,
356 | final String brokerName,
357 | final long brokerId,
358 | final String haServerAddr,
359 | final TopicConfigSerializeWrapper topicConfigWrapper,
360 | final List filterServerList,
361 | final boolean oneway,
362 | final int timeoutMills,
363 | final boolean compressed) {
364 |
365 | final List registerBrokerResultList = Lists.newArrayList();
366 | List nameServerAddressList = this.remotingClient.getNameServerAddressList();
367 | if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
368 |
369 | final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader();
370 | requestHeader.setBrokerAddr(brokerAddr);
371 | requestHeader.setBrokerId(brokerId);
372 | requestHeader.setBrokerName(brokerName);
373 | requestHeader.setClusterName(clusterName);
374 | requestHeader.setHaServerAddr(haServerAddr);
375 | requestHeader.setCompressed(compressed);
376 |
377 | RegisterBrokerBody requestBody = new RegisterBrokerBody();
378 | requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper);
379 | requestBody.setFilterServerList(filterServerList);
380 | final byte[] body = requestBody.encode(compressed);
381 | final int bodyCrc32 = UtilAll.crc32(body);
382 | requestHeader.setBodyCrc32(bodyCrc32);
383 | final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
384 | for (final String namesrvAddr : nameServerAddressList) {
385 | brokerOuterExecutor.execute(new Runnable() {
386 | @Override
387 | public void run() {
388 | try {
389 | RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
390 | if (result != null) {
391 | registerBrokerResultList.add(result);
392 | }
393 |
394 | log.info("register broker to name server {} OK", namesrvAddr);
395 | } catch (Exception e) {
396 | log.warn("registerBroker Exception, {}", namesrvAddr, e);
397 | } finally {
398 | countDownLatch.countDown();
399 | }
400 | }
401 | });
402 | }
403 |
404 | try {
405 | countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
406 | } catch (InterruptedException e) {
407 | }
408 | }
409 |
410 | return registerBrokerResultList;
411 | }
412 | ```
413 |
414 | 该方法主要是遍历NameServer 列表, Broker 消息服务器依次向NameServer发送心跳包。
415 |
416 | ```java
417 | //org.apache.rocketmq.broker.out.BrokerOuterAPI#registerBroker
418 | private RegisterBrokerResult registerBroker(
419 | final String namesrvAddr,
420 | final boolean oneway,
421 | final int timeoutMills,
422 | final RegisterBrokerRequestHeader requestHeader,
423 | final byte[] body
424 | ) throws RemotingCommandException, MQBrokerException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException,
425 | InterruptedException {
426 | RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader);
427 | request.setBody(body);
428 |
429 | if (oneway) {
430 | try {
431 | this.remotingClient.invokeOneway(namesrvAddr, request, timeoutMills);
432 | } catch (RemotingTooMuchRequestException e) {
433 | // Ignore
434 | }
435 | return null;
436 | }
437 |
438 | RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills);
439 | assert response != null;
440 | switch (response.getCode()) {
441 | case ResponseCode.SUCCESS: {
442 | RegisterBrokerResponseHeader responseHeader =
443 | (RegisterBrokerResponseHeader) response.decodeCommandCustomHeader(RegisterBrokerResponseHeader.class);
444 | RegisterBrokerResult result = new RegisterBrokerResult();
445 | result.setMasterAddr(responseHeader.getMasterAddr());
446 | result.setHaServerAddr(responseHeader.getHaServerAddr());
447 | if (response.getBody() != null) {
448 | result.setKvTable(KVTable.decode(response.getBody(), KVTable.class));
449 | }
450 | return result;
451 | }
452 | default:
453 | break;
454 | }
455 |
456 | throw new MQBrokerException(response.getCode(), response.getRemark());
457 | }
458 | ```
459 |
460 | 发送心跳包具体逻辑,首先封装请求包头(Header)
461 |
462 | * brokerAddr: broker 地址。
463 | * brokerId: brokerld. (O:Master; 大于0: Slave)
464 | * brokerName: broker名称。
465 | * clusterName: 集群名称。
466 | * haServerAddr: master 地址,初次请求时该值为空, slave 向Nameserver 注册后返回。
467 | * requestBody:
468 | * filterServerList 。消息过滤服务器列表。
469 | * topicConfigWrapper。主题配置, topicConfigWrapper 内部封装的是TopicConfigManager中的topicConfigTable ,内部存储的是Broker 启动时默认的一些Topic, MixAll.SELF_TEST_TOPIC 、MixAll.DEFAULT_TOPIC ( AutoCreateTopicEnable=true ). MixAll.BENCHMARK_TOPIC 、MixAll.OFFSET_MOVED_EVENT 、BrokerConfig#brokerClusterName 、BrokerConfig#brokerName 。Broker中Topic 默认存储在${ Rocket_Home}/store/confg/topic.json 中。
470 |
471 | >RocketMQ 网络传输基于Netty , 具体网络实现细节本书不会过细去剖析,在这里介绍一下网络跟踪方法: 每一个请求, RocketMQ 都会定义一个RequestCode ,然后在服务端会对应相应的网络处理器(processor 包中) , 只需整库搜索R巳questCode 即可找到相应的处理逻辑。如果对Netty 感兴趣,可以参考笔者发布的《源码研究Netty 系列》( http://blog.csdn.net/column/details/ 15042.html ) 。
472 |
473 | #### NameServer 处理心跳包
474 |
475 | org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor 网络处理器解析请求类型, 如果请求类型为RequestCode.REGISTER_BROKER ,则请求最终转发到RoutelnfoManager#registerBroker 。
476 |
477 | **clusterAddrTable 维护**
478 |
479 | ```java
480 | //org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker
481 | this.lock.writeLock().lockInterruptibly();
482 |
483 | Set brokerNames = this.clusterAddrTable.get(clusterName);
484 | if (null == brokerNames) {
485 | brokerNames = new HashSet();
486 | this.clusterAddrTable.put(clusterName, brokerNames);
487 | }
488 | brokerNames.add(brokerName);
489 | ```
490 |
491 | Step1:路由注册需要加写锁,防止并发修改RoutelnfoManager 中的路由表。首先判断Broker 所属集群是否存在, 如果不存在,则创建,然后将broker 名加入到集群Broker 集合中。
492 |
493 | **brokeAddrTable 维护**
494 |
495 | ```java
496 | //org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker
497 | BrokerData brokerData = this.brokerAddrTable.get(brokerName);
498 | if (null == brokerData) {
499 | registerFirst = true;
500 | brokerData = new BrokerData(clusterName, brokerName, new HashMap());
501 | this.brokerAddrTable.put(brokerName, brokerData);
502 | }
503 | String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
504 | registerFirst = registerFirst || (null == oldAddr);
505 | ```
506 |
507 | Step2 :维护BrokerData 信息,首先从brokerAddrTable 根据BrokerName 尝试获取Broker 信息,如果不存在, 则新建BrokerData 并放入到brokerAddrTable , registerFirst 设置为true ;如果存在, 直接替换原先的, registerFirst 设置为false,表示非第一次注册。
508 |
509 | **topicQueueTable 维护**
510 |
511 | ```java
512 | //org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker
513 | if (null != topicConfigWrapper
514 | && MixAll.MASTER_ID == brokerId) {
515 | if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
516 | || registerFirst) {
517 | ConcurrentMap tcTable =
518 | topicConfigWrapper.getTopicConfigTable();
519 | if (tcTable != null) {
520 | for (Map.Entry entry : tcTable.entrySet()) {
521 | this.createAndUpdateQueueData(brokerName, entry.getValue());
522 | }
523 | }
524 | }
525 | }
526 | ```
527 |
528 | Step3 :如果Broker为Master ,并且Broker Topic 配置信息发生变化或者是初次注册,则需要创建或更新Topic 路由元数据,填充topicQueueTable,其实就是为默认主题自动注册路由信息,其中包含MixAll.DEFAULT_TOPIC 的路由信息。当消息生产者发送主题时,如果该主题未创建并且BrokerConfig 的autoCreateTopicEnable 为true 时, 将返回MixAll.DEFAULT_TOPIC 的路由信息。
529 |
530 | **createAndUpdateQueueData**
531 |
532 | ```java
533 | private void createAndUpdateQueueData(final String brokerName, final TopicConfig topicConfig) {
534 | QueueData queueData = new QueueData();
535 | queueData.setBrokerName(brokerName);
536 | queueData.setWriteQueueNums(topicConfig.getWriteQueueNums());
537 | queueData.setReadQueueNums(topicConfig.getReadQueueNums());
538 | queueData.setPerm(topicConfig.getPerm());
539 | queueData.setTopicSynFlag(topicConfig.getTopicSysFlag());
540 |
541 | List queueDataList = this.topicQueueTable.get(topicConfig.getTopicName());
542 | if (null == queueDataList) {
543 | queueDataList = new LinkedList();
544 | queueDataList.add(queueData);
545 | this.topicQueueTable.put(topicConfig.getTopicName(), queueDataList);
546 | log.info("new topic registered, {} {}", topicConfig.getTopicName(), queueData);
547 | } else {
548 | boolean addNewOne = true;
549 |
550 | Iterator it = queueDataList.iterator();
551 | while (it.hasNext()) {
552 | QueueData qd = it.next();
553 | if (qd.getBrokerName().equals(brokerName)) {
554 | if (qd.equals(queueData)) {
555 | addNewOne = false;
556 | } else {
557 | log.info("topic changed, {} OLD: {} NEW: {}", topicConfig.getTopicName(), qd,
558 | queueData);
559 | it.remove();
560 | }
561 | }
562 | }
563 |
564 | if (addNewOne) {
565 | queueDataList.add(queueData);
566 | }
567 | }
568 | }
569 | ```
570 |
571 | 根据TopicConfig 创建QueueData 数据结构,然后更新topicQueueTable 。
572 |
573 | ```java
574 | BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
575 | new BrokerLiveInfo(
576 | System.currentTimeMillis(),
577 | topicConfigWrapper.getDataVersion(),
578 | channel,
579 | haServerAddr));
580 | if (null == prevBrokerLiveInfo) {
581 | log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
582 | }
583 | ```
584 |
585 | Step4 : 更新BrokerLiveInfo ,存活Broker 信息表, BrokeLiveInfo 是执行路由删除的重要依据。
586 |
587 | ```java
588 | if (filterServerList != null) {
589 | if (filterServerList.isEmpty()) {
590 | this.filterServerTable.remove(brokerAddr);
591 | } else {
592 | this.filterServerTable.put(brokerAddr, filterServerList);
593 | }
594 | }
595 |
596 | if (MixAll.MASTER_ID != brokerId) {
597 | String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
598 | if (masterAddr != null) {
599 | BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
600 | if (brokerLiveInfo != null) {
601 | result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
602 | result.setMasterAddr(masterAddr);
603 | }
604 | }
605 | }
606 | ```
607 |
608 | Step5:注册Broker 的过滤器Server 地址列表,一个Broker 上会关联多个FilterServer消息过滤服务器,此部分内容将在第6 章详细介绍;如果此Broker 为从节点,则需要查找该Broker 的Master 的节点信息,并更新对应的masterAddr 属性。
609 |
610 | >设计亮点: NameServe 与Broker 保持长连接, Broker 状态存储在brokerLiveTable 中,NameServer 每收到一个心跳包,将更新brokerLiveTable 中关于Broker 的状态信息以及路由表( topicQueueTable 、brokerAddrTab le 、brokerLiveTable 、fi lterServerTable ) 。更新上述路由表( HashTable )使用了锁粒度较少的读写锁,允许多个消息发送者(Producer )并发读,保证消息发送时的高并发。但同一时刻NameServer 只处理一个Broker 心跳包,多个心跳包请求串行执行。
611 |
612 | ### 2.3.3.路由删除
613 |
614 | 根据上面章节的介绍, Broker 每隔30s 向NameServer 发送一个心跳包,心跳包中包含BrokerId 、Broker 地址、Broker 名称、Broker 所属集群名称、Broker 关联的FilterServer 列表。但是如果Broker 若机, NameServer 无法收到心跳包,此时NameServer 如何来剔除这些失效的Broker 呢? Name Server 会每隔I Os 扫描brokerLiveTable 状态表,如果BrokerLive 的lastUpdateTimestamp 的时间戳距当前时间超过120s ,则认为Broker 失效,移除该Broker,关闭与Broker 连接,并同时更新topicQueueTable 、brokerAddrTable 、brokerLiveTable 、filterServerTable 。
615 |
616 | RocktMQ 有两个触发点来触发路由删除。
617 |
618 | 1. NameServer 定时扫描brokerLiveTable 检测上次心跳包与当前系统时间的时间差,如果时间戳大于120s ,则需要移除该Broker 信息。
619 | 2. Broker 在正常被关闭的情况下,会执行unr巳gisterBroker 指令。
620 |
621 | 由于不管是何种方式触发的路由删除,路由删除的方法都是一样的,就是从topicQueueTable 、brokerAddrTable 、brokerLiveTable 、filterServerTable 删除与该Broker 相关的信息,但RocketMQ 这两种方式维护路由信息时会抽取公共代码,本文将以第一种方式展开分析。
622 |
623 | ```java
624 | public void scanNotActiveBroker() {
625 | Iterator> it = this.brokerLiveTable.entrySet().iterator();
626 | while (it.hasNext()) {
627 | Entry next = it.next();
628 | long last = next.getValue().getLastUpdateTimestamp();
629 | if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
630 | RemotingUtil.closeChannel(next.getValue().getChannel());
631 | it.remove();
632 | log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
633 | this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
634 | }
635 | }
636 | }
637 | ```
638 |
639 | 我们应该不会忘记scanNotActi veBrok巳r 在NameServer 中每10s 执行一次。逻辑也很简单,遍历brokerLivelnfo 路由表( HashMap ),检测BrokerLiv巳Info 的lastUpdateTimestamp上次收到心跳包的时间如果超过当前时间l 20s, NameServer 则认为该Broker 已不可用,故需要将它移除,关闭Channel ,然后删除与该Broker 相关的路由信息,路由表维护过程,需要申请写锁。
640 |
641 | ```java
642 | //RoutelnfoManager#onChannelDestroy
643 | this.lock.writeLock().lockInterruptibly ( ) ;
644 | this.brokerLiveTable.remove(brokerAddrFound);
645 | this.filterServerTable.remove(brokerAddrFound);
646 | ```
647 |
648 | Step I :申请写锁,根据brokerAddress 从brokerLiveTable 、filterServerTable 移除,如
649 | 代码清单所示。
650 |
651 | ```java
652 | String brokerNameFound = null;
653 | boolean removeBrokerName = false;
654 | Iterator> itBrokerAddrTable =
655 | this.brokerAddrTable.entrySet().iterator();
656 | while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
657 | BrokerData brokerData = itBrokerAddrTable.next().getValue();
658 |
659 | Iterator> it = brokerData.getBrokerAddrs().entrySet().iterator();
660 | while (it.hasNext()) {
661 | Entry entry = it.next();
662 | Long brokerId = entry.getKey();
663 | String brokerAddr = entry.getValue();
664 | if (brokerAddr.equals(brokerAddrFound)) {
665 | brokerNameFound = brokerData.getBrokerName();
666 | it.remove();
667 | log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",
668 | brokerId, brokerAddr);
669 | break;
670 | }
671 | }
672 |
673 | if (brokerData.getBrokerAddrs().isEmpty()) {
674 | removeBrokerName = true;
675 | itBrokerAddrTable.remove();
676 | log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
677 | brokerData.getBrokerName());
678 | }
679 | }
680 | ```
681 |
682 | Step2 :维护brokerAddrTable 。遍历从HashMapbrokerAddrTable,从BrokerData 的HashMapbrokerAddrs 中,找到具体的Broker ,从BrokerData 中移除,如果移除后在BrokerData 中不再包含其他Broker ,则在brokerAddrTable 中移除该brokerName 对应的条目。
683 |
684 | ```java
685 | if (brokerNameFound != null && removeBrokerName) {
686 | Iterator>> it = this.clusterAddrTable.entrySet().iterator();
687 | while (it.hasNext()) {
688 | Entry> entry = it.next();
689 | String clusterName = entry.getKey();
690 | Set brokerNames = entry.getValue();
691 | boolean removed = brokerNames.remove(brokerNameFound);
692 | if (removed) {
693 | log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",
694 | brokerNameFound, clusterName);
695 |
696 | if (brokerNames.isEmpty()) {
697 | log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
698 | clusterName);
699 | it.remove();
700 | }
701 |
702 | break;
703 | }
704 | }
705 | }
706 |
707 | ```
708 |
709 | Step3 : 根据BrokerName ,从clusterAddrTable 中找到Broker 并从集群中移除。如果移除后,集群中不包含任何Broker ,则将该集群从clusterAddrTable 中移除。
710 |
711 | ```java
712 | if (removeBrokerName) {
713 | Iterator>> itTopicQueueTable =
714 | this.topicQueueTable.entrySet().iterator();
715 | while (itTopicQueueTable.hasNext()) {
716 | Entry> entry = itTopicQueueTable.next();
717 | String topic = entry.getKey();
718 | List queueDataList = entry.getValue();
719 |
720 | Iterator itQueueData = queueDataList.iterator();
721 | while (itQueueData.hasNext()) {
722 | QueueData queueData = itQueueData.next();
723 | if (queueData.getBrokerName().equals(brokerNameFound)) {
724 | itQueueData.remove();
725 | log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
726 | topic, queueData);
727 | }
728 | }
729 |
730 | if (queueDataList.isEmpty()) {
731 | itTopicQueueTable.remove();
732 | log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
733 | topic);
734 | }
735 | }
736 | }
737 | ```
738 |
739 | Step4 : 根据brokerName ,遍历所有主题的队列,如果队列中包含了当前Broker 的队列,则移除,如果topic 只包含待移除Broker 的队列的话,从路由表中删除该topic ,如代码清单2-21 所示。
740 |
741 | ```java
742 | finally {
743 | this.lock.writeLock().unlock();
744 | }
745 | ```
746 |
747 | Step5 :释放锁,完成路由删除。
748 |
749 | ### 2.3.4.路由发现
750 |
751 | RocketMQ 路由发现是非实时的,当Topic 路由出现变化后, NameServer 不主动推送给客户端, 而是由客户端定时拉取主题最新的路由。根据主题名称拉取路由信息的命令编码为: GET_ROUTEINTO_BY_TOPIC 。RocketMQ 路由结果如图2-6 所示。
752 |
753 | 
754 |
755 | * orderTopicConf :顺序消息配置内容,来自于kvConfig 。
756 | * List queueData: topic 队列元数据。
757 | * List brokerDatas: topic 分布的broker 元数据。
758 | * HashMap< String/* brokerAdress*/,List /* filt巳rServer* /> : broker 上过滤服务器地址列表。
759 | * NameServer 路由发现实现类:DefaultRequestProcessor#getRoutelnfoByTopic ,如代码清单2-22 所示。
760 |
761 | ```java
762 | public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
763 | RemotingCommand request) throws RemotingCommandException {
764 | final RemotingCommand response = RemotingCommand.createResponseCommand(null);
765 | final GetRouteInfoRequestHeader requestHeader =
766 | (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
767 |
768 | TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
769 |
770 | if (topicRouteData != null) {
771 | if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
772 | String orderTopicConf =
773 | this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
774 | requestHeader.getTopic());
775 | topicRouteData.setOrderTopicConf(orderTopicConf);
776 | }
777 |
778 | byte[] content = topicRouteData.encode();
779 | response.setBody(content);
780 | response.setCode(ResponseCode.SUCCESS);
781 | response.setRemark(null);
782 | return response;
783 | }
784 |
785 | response.setCode(ResponseCode.TOPIC_NOT_EXIST);
786 | response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
787 | + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
788 | return response;
789 | }
790 | ```
791 |
792 | * Step1:调用RouterlnfoManager 的方法,从路由表topicQueueTable 、brokerAddrTable 、filterServerTable 中分别填充TopicRouteData 中的List 由于this.endTransaction 的执行, 其业务事务并没有提交, 故在使用事务消息TransactionListener#execute 方法时除了记录事务消息状态后, 应该返回LocalTransaction.UNKNOW,事务消息的提交与回滚通过下面提到的事务消息状态回查时再决定是否提交或回滚。
134 |
135 |
136 |
137 | 事务消息发送的整体流程就介绍到这里了, 接下来我们再重点介绍一下prepare 消息发送的全过程。
138 |
139 | ```java
140 | //org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendKernelImpl
141 |
142 | final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
143 | if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
144 | sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
145 | }
146 | ```
147 |
148 | 在消息发送之前, 如果消息为prepare 类型,则设置消息标准为prepare 消息类型,方便消息服务器正确识别事务类型的消息。
149 |
150 | ```java
151 | String traFlag = oriProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
152 | if (traFlag != null && Boolean.parseBoolean(traFlag)) {
153 | if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) {
154 | response.setCode(ResponseCode.NO_PERMISSION);
155 | response.setRemark(
156 | "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
157 | + "] sending transaction message is forbidden");
158 | return response;
159 | }
160 | putMessageResult = this.brokerController.getTransactionalMessageService().prepareMessage(msgInner);
161 | } else {
162 | putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
163 | }
164 | ```
165 |
166 | Broker 端在收到消息存储请求时, 如果消息为prep are 消息, 则执行prepareMessage 方法,否则走普通消息的存储流程。
167 |
168 | ```java
169 | //org.apache.rocketmq.broker.transaction.queue.TransactionalMessageBridge#putHalfMessage
170 | public PutMessageResult putHalfMessage(MessageExtBrokerInner messageInner) {
171 | return store.putMessage(parseHalfMessageInner(messageInner));
172 | }
173 |
174 | private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
175 | MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
176 | MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
177 | String.valueOf(msgInner.getQueueId()));
178 | msgInner.setSysFlag(
179 | MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
180 | msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
181 | msgInner.setQueueId(0);
182 | msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
183 | return msgInner;
184 | }
185 | ```
186 |
187 | 这里是事务消息与非事务消息发送流程的主要区别, 如果是事务消息则备份消息的原主题与原消息消费队列, 然后将主题变更为RMQ_SYS_TRANS_HALF_TOPIC ,消费队列变更为0 , 然后消息按照普通消息存储在commitlog 文件进而转发到RMQ_SYS_ TRANS_HALF_TOPIC 主题对应的消息消费队列。也就是说,事务消息在未提交之前并不会存入消息原有主题, 自然也不会被消费者消费。既然变更了主题, RocketMQ 通常会采用定时任务(单独的线程)去消费该主题, 然后将该消息在满足特定条件下恢复消息主题,进而被消费者消费。这种实现应该并不陌生, 它与RocketMQ 定时消息的处理过程如出一辙。
188 |
189 |
190 |
191 | RocketMQ 事务发送流程图如图所示。
192 |
193 | 
194 |
195 |
196 |
197 | 接下来重点分析一下调用结束事务DefaultMQProducerlmp l# endTransaction 。
198 |
199 | ## 8.3.提交或回滚事务
200 |
201 | 本节继续探讨两阶段提交的第二个阶段: 提交或回滚事务。
202 |
203 | ```java
204 | String transactionId = sendResult.getTransactionId();
205 | final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName());
206 | EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader();
207 | requestHeader.setTransactionId(transactionId);
208 | requestHeader.setCommitLogOffset(id.getOffset());
209 | switch (localTransactionState) {
210 | case COMMIT_MESSAGE:
211 | requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
212 | break;
213 | case ROLLBACK_MESSAGE:
214 | requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
215 | break;
216 | case UNKNOW:
217 | requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
218 | break;
219 | default:
220 | break;
221 | }
222 | ```
223 |
224 | 根据消息所属的消息队列获取Broker 的IP 与端口信息,然后发送结束事务命令,其关键就是根据本地执行事务的状态分别发送提交、回滚或“不作为”的命令。Broker 服务端的结束事务处理器为: EndTransactionProcessor 。
225 |
226 | ```java
227 | OperationResult result = new OperationResult();
228 | if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
229 | result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
230 | if (result.getResponseCode() == ResponseCode.SUCCESS) {
231 | RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
232 | if (res.getCode() == ResponseCode.SUCCESS) {
233 | MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
234 | msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));
235 | msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());
236 | msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());
237 | msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());
238 | RemotingCommand sendResult = sendFinalMessage(msgInner);
239 | if (sendResult.getCode() == ResponseCode.SUCCESS) {
240 | this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
241 | }
242 | return sendResult;
243 | }
244 | return res;
245 | }
246 | }
247 | ```
248 |
249 | 如果结束事务动作为提交事务,则执行提交事务逻辑,其关键实现如下。
250 |
251 | 1. 首先从结束事务请求命令中获取消息的物理偏移量( commitlogOffset ),其实现逻辑由TransactionalMessageService#commitMessage 实现。
252 | 2. 然后恢复消息的主题、消费队列,构建新的消息对象,由TransactionalMessageService#endMessageTransaction 实现。
253 | 3. 然后将消息再次存储在commitlog 文件中,此时的消息主题则为业务方发送的消息,将被转发到对应的消息消费队列,供消息消费者消费,其实现由TransactionalMessageService#sendFinalMessage 实现。
254 | 4. **消息存储后,删除prepare 消息,其实现方法并不是真正的删除,而是将prepare 消息存储到RMQ_SYS_TRANS_OP_HALF_TOPIC 主题中,表示该事务消息( prepare 状态的消息)已经处理过(提交或回滚),为未处理的事务进行事务回查提供查找依据。****
255 |
256 | 事务的回滚与提交的唯一差别是无须将消息恢复原主题,直接删除prepare 消息即可,同样是将预处理消息存储在RMQ_SYS_TRANS_OP_HALF_TOPIC 主题中,表示已处理过该消息。
257 |
258 | ## 8.4.事务消息回查事务状态
259 |
260 | 上节重点梳理了RocketMQ 基于两阶段协议发送与提交回滚消息,本节将深入学习事务状态、消息回查,事务消息存储在消息服务器时主题被替换为肌Q_SYS_ TRANS_ HALF_TOPIC ,执行完本地事务返回本地事务状态为UN KNOW 时,结束事务时将不做任何处理,而是通过事务状态定时回查以期得到发送端明确的事务操作(提交事务或回滚事务) 。
261 |
262 | RocketMQ 通过TransactionalMessageCheckService 线程定时去检测RMQ_SYS_ TRANS_HALF_TOPIC 主题中的消息,回查消息的事务状态。TransactionalMessageCheckService 的检测频率默认为1分钟,可通过在broker.conf 文件中设置transactionChecklnterval 来改变默认值,单位为毫秒。
263 |
264 | ```
265 | @Override
266 | protected void onWaitEnd() {
267 | long timeout = brokerController.getBrokerConfig().getTransactionTimeOut();
268 | int checkMax = brokerController.getBrokerConfig().getTransactionCheckMax();
269 | long begin = System.currentTimeMillis();
270 | log.info("Begin to check prepare message, begin time:{}", begin);
271 | this.brokerController.getTransactionalMessageService().check(timeout, checkMax, this.brokerController.getTransactionalMessageCheckListener());
272 | log.info("End to check prepare message, consumed time:{}", System.currentTimeMillis() - begin);
273 | }
274 | ```
275 |
276 | transactionTimeOut :事务的过期时间只有当消息的存储时间加上过期时间大于系统当前时间时,才对消息执行事务状态回查,否则在下一次周期中执行事务回查操作。
277 |
278 | transactionCheckMax : 事务回查最大检测次数,如果超过最大检测次数还是无法获知消息的事务状态, RocketMQ 将不会继续对消息进行事务状态回查,而是直接丢弃即相当于回滚事务。
279 |
280 | 接下来重点分析TransactionalMessageService#check 的实现逻辑,其实现类为org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl 。
281 |
282 | ```java
283 | //org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#check
284 | String topic = MixAll.RMQ_SYS_TRANS_HALF_TOPIC;
285 | Set msgQueues = transactionalMessageBridge.fetchMessageQueues(topic);
286 | if (msgQueues == null || msgQueues.size() == 0) {
287 | log.warn("The queue of topic is empty :" + topic);
288 | return;
289 | }
290 | ```
291 |
292 | 获取RMQ_SYS_TRANS_HALF_TOPIC 主题下的所有消息队列,然后依次处理。
293 |
294 | ```java
295 | //org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#check
296 | long startTime = System.currentTimeMillis();
297 | MessageQueue opQueue = getOpQueue(messageQueue);
298 | long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue);
299 | long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue);
300 | log.info("Before check, the queue={} msgOffset={} opOffset={}", messageQueue, halfOffset, opOffset);
301 | if (halfOffset < 0 || opOffset < 0) {
302 | log.error("MessageQueue: {} illegal offset read: {}, op offset: {},skip this queue", messageQueue,
303 | halfOffset, opOffset);
304 | continue;
305 | }
306 | ```
307 |
308 | 根据事务消息消费队列获取与之对应的消息队列,其实就是获取已处理消息的消息消费队列,其主题为: RMQ_SYS_TRANS_OP_HALF _TOPIC 。
309 |
310 | ```java
311 | //org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#check
312 | List doneOpOffset = new ArrayList<>();
313 | HashMap removeMap = new HashMap<>();
314 | PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, doneOpOffset);
315 | if (null == pullResult) {
316 | log.error("The queue={} check msgOffset={} with opOffset={} failed, pullResult is null",
317 | messageQueue, halfOffset, opOffset);
318 | continue;
319 | }
320 | ```
321 |
322 | fillOpRemoveMap 主要的作用是根据当前的处理进度依次从已处理队列拉取32 条消息,方便判断当前处理的消息是否已经处理过,如果处理过则无须再次发送事务状态回查请求,避免重复发送事务回查请求。事务消息的处理涉及如下两个主题。
323 |
324 | RMQ_SYS_TRANS_ HALF_TOPIC: prepare 消息的主题, 事务消息首先进入到该主题。
325 |
326 | RMQ_SYS_TRANS_OP_HALF _TOPIC:当消息服务器收到事务消息的提交或因滚请求后, 会将消息存储在该主题下。
327 |
328 | ```java
329 | // single thread
330 | //获取空消息的次数
331 | int getMessageNullCount = 1;
332 | //当前处理RMQ_SYS_TRANS_HALF_TOPIC#queueId的最新进度。
333 | long newOffset = halfOffset;
334 | //当前处理消息的队列偏移量,其主题依然为RMQ_SYS一TRANS_HALF_TOPIC
335 | long i = halfOffset;
336 | while (true) {
337 | //这段代码大家应该并不陌生, RocketMQ 处理任务的一个通用处理逻辑就是
338 | //为每个任务一次只分配某个固定时长,超过该时长则需等待下次任务调度。RocketMQ 为待
339 | //检测主题RMQ_SYS_TRANS_HALF_TOPIC 的每个队列做事务状态回查,一次最多不超过
340 | //60 秒,目前该值不可配置。
341 | if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) {
342 | log.info("Queue={} process time reach max={}", messageQueue, MAX_PROCESS_TIME_LIMIT);
343 | break;
344 | }
345 | //如果该消息已被处理,则继续处理下一条消息。
346 | if (removeMap.containsKey(i)) {
347 | log.info("Half offset {} has been committed/rolled back", i);
348 | removeMap.remove(i);
349 | } else {
350 | //根据消息队列偏移量i 从消费队列中获取消息。
351 | GetResult getResult = getHalfMsg(messageQueue, i);
352 | MessageExt msgExt = getResult.getMsg();
353 | if (msgExt == null) {
354 | //从待处理任务队列中拉取消息,如果未拉取到消息,则根据允许重复次数进
355 | //行操作,默认重试一次,目前不可配置。其具体实现如下。
356 | //如果超过重试次数,直接跳出,结束该消息队列的事务状态回查。
357 | if (getMessageNullCount++ > MAX_RETRY_COUNT_WHEN_HALF_NULL) {
358 | break;
359 | }
360 | //如果是由于没有新的消息而返回为空(拉取状态为: PullStatus.NO_NEW_MSG),
361 | //则结束该消息队列的事务状态回查。
362 | if (getResult.getPullResult().getPullStatus() == PullStatus.NO_NEW_MSG) {
363 | log.debug("No new msg, the miss offset={} in={}, continue check={}, pull result={}", i,
364 | messageQueue, getMessageNullCount, getResult.getPullResult());
365 | break;
366 | //其他原因,则将偏移量i 设置为: getResult.getPullResult().getNextBeginOffset(),重新拉取。
367 | } else {
368 | log.info("Illegal offset, the miss offset={} in={}, continue check={}, pull result={}",
369 | i, messageQueue, getMessageNullCount, getResult.getPullResult());
370 | i = getResult.getPullResult().getNextBeginOffset();
371 | newOffset = i;
372 | continue;
373 | }
374 | }
375 |
376 | //判断该消息是否需要discard (吞没、丢弃、不处理)或skip (跳过),其依据如下。
377 | //needDiscard 依据:如果该消息回查的次数超过允许的最大回查次数,则该消息将被
378 | //丢弃,即事务消息提交失败,具体实现方式为每回查一次,在消息属性TRANSACTION_CHECK_TIMES 中增1 , 默认最大回查次数为5 次。
379 | //needSkip 依据: 如果事务消息超过文件的过期时间,默认为72 小时,则跳过该消息。
380 | if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) {
381 | listener.resolveDiscardMsg(msgExt);
382 | newOffset = i + 1;
383 | i++;
384 | continue;
385 | }
386 | if (msgExt.getStoreTimestamp() >= startTime) {
387 | log.debug("Fresh stored. the miss offset={}, check it later, store={}", i,
388 | new Date(msgExt.getStoreTimestamp()));
389 | break;
390 | }
391 | //valueOfCurrentMinusBorn :消息已存储的时间,为系统当前时-间减去消息存储的时间戳。
392 | long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp();
393 | //checkImmunityTime : 立即检测事务消息的时间,其设计的意义是,应用程序在发送
394 | //事务消息后,事务不会马上提交,该时间就是假设事务消息发送成功后,应用程序
395 | //事务提交的时间, 在这段时间内, RocketMQ 任务事务未提交,故不应该在这个时间段向应用程序发送回查请求。
396 | //transactionTimeout : 事务消息的超时时间,这个时间是从OP 拉取的消息的最后一
397 | //条消息的存储时间与check 方法开始的时间,如果时间差超过了transactionTimeo时,
398 | //就算时间小于ch eckimmunityT ime 时间,也发送事务回查指令。
399 | long checkImmunityTime = transactionTimeout;
400 | //MessageConst.PROPERTY CHECK_IMMUNITY_TIME_IN_SECONDS : 消息事务消息
401 | //回查请求的最晚时间,单位为秒,指的是程序发送事务消息时,可以指定该事务消息的有
402 | //效时间,只有在这个时间内收到回查消息才有效, 默认为null 。
403 | String checkImmunityTimeStr = msgExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS);
404 | //如果消息指定了事务消息过期时间属性(PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS),如果当前时间已超过该值。
405 | if (null != checkImmunityTimeStr) {
406 | checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout);
407 | if (valueOfCurrentMinusBorn < checkImmunityTime) {
408 | if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt)) {
409 | newOffset = i + 1;
410 | i++;
411 | continue;
412 | }
413 | }
414 | //如果当前时间还未过(应用程序事务结束时间),则跳出本次处理,等下一次再试
415 | } else {
416 | if ((0 <= valueOfCurrentMinusBorn) && (valueOfCurrentMinusBorn < checkImmunityTime)) {
417 | log.debug("New arrived, the miss offset={}, check it later checkImmunity={}, born={}", i,
418 | checkImmunityTime, new Date(msgExt.getBornTimestamp()));
419 | break;
420 | }
421 | }
422 | List opMsg = pullResult.getMsgFoundList();
423 | //判断是否需要发送事务回查消息,具体逻辑如下。
424 | //如果操作队列( RMQ_SYS_TRANS_OP_HALF_TOPIC ) 中没有已处理消息并且已经超过应用程序事务结束时间即transactionTimeOut 值。
425 | //如果操作队列不为空并且最后一条消息的存储时间已经超过transactionTimeOut 值。
426 | boolean isNeedCheck = (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime)
427 | || (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout))
428 | || (valueOfCurrentMinusBorn <= -1);
429 | if (isNeedCheck) {
430 | //如果需要发送事务状态回查消息,则先将消息再次发送到RMQ_SYS_TRANS_HALF_TOPIC 主题中,发送成功则返回true , 否则返回false
431 | if (!putBackHalfMsgQueue(msgExt, i)) {
432 | //putBackHalfMsgQueue:
433 | //在执行事务消息回查之前, 竟然在此把该消息存储在commitlog 文件, 新的消息设置最新的物理偏移量。为什么需要这样处理呢?
434 | // 主要是因为下文的发送事务消息是异步处理的,无法立刻知道其处理结果,为了简化prepare 消息队列和处理队列的消息消费进度
435 | //处理, 先存储, 然后消费进度向前推动,重复发送的消息在事务回查之前会判断是否处理过。
436 | // 另外一个目的就是需要修改消息的检查次数, RocketMQ 的存储设计采用顺序写,去修改已存储的消息,其性能无法高性能。
437 | continue;
438 | }
439 | listener.resolveHalfMsg(msgExt);
440 | } else {
441 | //如果无法判断是否发送回查消息,则加载更多的己处理消息进行筛选。
442 | pullResult = fillOpRemoveMap(removeMap, opQueue, pullResult.getNextBeginOffset(), halfOffset, doneOpOffset);
443 | log.info("The miss offset:{} in messageQueue:{} need to get more opMsg, result is:{}", i,
444 | messageQueue, pullResult);
445 | continue;
446 | }
447 | }
448 | newOffset = i + 1;
449 | i++;
450 | }
451 | if (newOffset != halfOffset) {
452 | //保存( Prepare )消息队列的回查进度。
453 | transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
454 | }
455 | long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
456 | if (newOpOffset != opOffset) {
457 | //保存处理队列( OP ) 的进度。
458 | transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
459 | }
460 | ```
461 |
462 | 上述讲解了Transactiona!MessageCheckService 回查定时线程的发送回查消息的整体流程与实现细节, 通过异步方式发送消息回查的实现过程
463 |
464 | ```java
465 | public void sendCheckMessage(MessageExt msgExt) throws Exception {
466 | CheckTransactionStateRequestHeader checkTransactionStateRequestHeader = new CheckTransactionStateRequestHeader();
467 | checkTransactionStateRequestHeader.setCommitLogOffset(msgExt.getCommitLogOffset());
468 | checkTransactionStateRequestHeader.setOffsetMsgId(msgExt.getMsgId());
469 | checkTransactionStateRequestHeader.setMsgId(msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX));
470 | checkTransactionStateRequestHeader.setTransactionId(checkTransactionStateRequestHeader.getMsgId());
471 | checkTransactionStateRequestHeader.setTranStateTableOffset(msgExt.getQueueOffset());
472 | msgExt.setTopic(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC));
473 | msgExt.setQueueId(Integer.parseInt(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_QUEUE_ID)));
474 | msgExt.setStoreSize(0);
475 | String groupId = msgExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP);
476 | Channel channel = brokerController.getProducerManager().getAvaliableChannel(groupId);
477 | if (channel != null) {
478 | brokerController.getBroker2Client().checkProducerTransactionState(groupId, channel, checkTransactionStateRequestHeader, msgExt);
479 | } else {
480 | LOGGER.warn("Check transaction failed, channel is null. groupId={}", groupId);
481 | }
482 | }
483 | ```
484 |
485 | 首先构建事务状态回查请求消息,核心参数包含消息offsetld 、消息ID (索引) 、消息事务ID 、事务消息队列中的偏移量、消息主题、消息队列。然后根据消息的生产者组,从中随机选择一个消息发送者。最后向消息发送者发送事务回查命令。
486 |
487 | 事务回查命令的最终处理者为C lientRemotingProssor 的processRequest 方法,最终将任务提交到TransactionMQProducer 的线程池中执行,最终调用应用程序实现的TransactionListener 的checkLoca!Transaction 方法,返回事务状态。如果事务状态为Loca lTransactionState#COMMIT_MESSAGE , 则向消息服务器发送提交事务消息命令;如果事务状态为Loca!TransactionState#ROLLBACK MESSAGE ,则向Broker 服务器发送回滚事务操作; 如果事务状态为UN OWN ,则服务端会忽略此次提交。
488 |
489 |
490 |
491 | ## 8.5.小结
492 |
493 | RocketMQ 事务消息基于两阶段提交和事务状态回查机制来实现,所谓的两阶段提交,即首先发送prepare 消息,待事务提交或回滚时发送commit , rollback 命令。再结合定时任务, RocketMQ 使用专门的线程以特定的频率对RocketMQ 服务器上的prepare 信息进行处理, 向发送端查询事务消息的状态来决定是否提交或回滚消息。
494 |
495 | 
--------------------------------------------------------------------------------
/源码阅读/9.RocketMQ消息查询.md:
--------------------------------------------------------------------------------
1 | # 9.RocketMQ消息查询
2 |
3 | ## 9.1.消息查询的方式
4 |
5 | 对于 Producer 发送到 Broker 服务器的消息,RocketMQ 支持多种方式来方便地查询消息:
6 |
7 | ### 9.1.1.根据键查询消息
8 |
9 | 如下所示,在构建消息的时候,指定了这条消息的键为 “OrderID001”:
10 |
11 | ```java
12 | Message msg =
13 | new Message("TopicTest",
14 | "TagA",
15 | "OrderID001", // Keys
16 | "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
17 |
18 | ```
19 |
20 | 那么,当这条消息发送成功后,我们可以使用 queryMsgByKey 命令查询到这条消息的详细信息:
21 |
22 | ```java
23 | MQAdminStartup.main(new String[] {
24 | "queryMsgByKey",
25 | "-n",
26 | "localhost:9876",
27 | "-t",
28 | "TopicTest",
29 | "-k",
30 | "OrderID001"
31 | });
32 | ```
33 |
34 | ### 9.1.2.根据偏移量来查询
35 |
36 | 消息在发送成功之后,其返回的 SendResult 类中**包含了这条消息的唯一偏移量 ID** (注意此处指的是 offsetMsgId):
37 |
38 | 
39 |
40 | 用户可以使用 queryMsgById 命令查询这条消息的详细信息:
41 |
42 | ```java
43 | MQAdminStartup.main(new String[] {
44 | "queryMsgById",
45 | "-n",
46 | "localhost:9876",
47 | "-i",
48 | "0A6C73D900002A9F0000000000004010"
49 | });
50 | ```
51 |
52 | ### 9.1.3.根据唯一键查询消息
53 |
54 | 消息在发送成功之后,其返回的 SendResult 类中包含了这条消息的唯一 ID:
55 |
56 | 
57 |
58 | 用户可以使用 queryMsgByUniqueKey 命令查询这条消息的详细信息:
59 |
60 | ```java
61 | MQAdminStartup.main(new String[] {
62 | "queryMsgByUniqueKey",
63 | "-n",
64 | "localhost:9876",
65 | "-i",
66 | "0A6C73D939B318B4AAC20CBA5D920000",
67 | "-t",
68 | "TopicTest"
69 | });
70 |
71 | ```
72 |
73 | ### 9.1.4.根据消息队列偏移量查询消息
74 |
75 | 消息发送成功之后的 SendResult 中还包含了消息队列的其它信息,如消息队列 ID、消息队列偏移量等信息:
76 |
77 | ```java
78 | SendResult [sendStatus=SEND_OK,
79 | msgId=0A6C73D93EC518B4AAC20CC4ACD90000,
80 | offsetMsgId=0A6C73D900002A9F000000000000484E,
81 | messageQueue=MessageQueue [topic=TopicTest,
82 | brokerName=zk-pc,
83 | queueId=3],
84 | queueOffset=24]
85 | ```
86 |
87 | 根据这些信息,使用 queryMsgByOffset 命令也可以查询到这条消息的详细信息:
88 |
89 | ```java
90 | MQAdminStartup.main(new String[] {
91 | "queryMsgByOffset",
92 | "-n",
93 | "localhost:9876",
94 | "-t",
95 | "TopicTest",
96 | "-b",
97 | "zk-pc",
98 | "-i",
99 | "3",
100 | "-o",
101 | "24"
102 | });
103 | ```
104 |
105 | ## 9.2.(偏移量) ID 查询
106 |
107 | (偏移量) ID 是在消息发送到 Broker 服务器存储的时候生成的,其包含如下几个字段:
108 |
109 | * Broker 服务器 IP 地址
110 | * Broker 服务器端口号
111 | * 消息文件 CommitLog 写偏移量
112 |
113 | 
114 |
115 | ```java
116 | public class CommitLog {
117 |
118 | class DefaultAppendMessageCallback implements AppendMessageCallback {
119 |
120 | public AppendMessageResult doAppend(final long fileFromOffset, /** 其它参数 **/) {
121 | String msgId = MessageDecoder
122 | .createMessageId(this.msgIdMemory,
123 | msgInner.getStoreHostBytes(hostHolder),
124 | wroteOffset);
125 | // ...
126 | }
127 |
128 | }
129 | }
130 | ```
131 |
132 | ## 9.3.使用 ID 查询
133 |
134 | Admin 端查询的时候,首先对 msgId 进行解析,取出 Broker 服务器的 IP 、端口号和消息偏移量:
135 |
136 | ```java
137 | public class MessageDecoder {
138 |
139 | public static MessageId decodeMessageId(final String msgId)
140 | throws UnknownHostException {
141 | byte[] ip = UtilAll.string2bytes(msgId.substring(0, 8));
142 | byte[] port = UtilAll.string2bytes(msgId.substring(8, 16));
143 | // offset
144 | byte[] data = UtilAll.string2bytes(msgId.substring(16, 32));
145 | // ...
146 | }
147 |
148 | }
149 | ```
150 |
151 | 获取到偏移量之后,Admin 会对 Broker 服务器发送一个 VIEW_MESSAGE_BY_ID 的请求命令,Broker 服务器在收到请求后,会依据偏移量定位到 CommitLog 文件中的相应位置,然后取出消息,返回给 Admin 端:
152 |
153 | ```java
154 | public class DefaultMessageStore implements MessageStore {
155 |
156 | @Override
157 | public SelectMappedBufferResult selectOneMessageByOffset(long commitLogOffset) {
158 | SelectMappedBufferResult sbr = this.commitLog
159 | .getMessage(commitLogOffset, 4);
160 | // 1 TOTALSIZE
161 | int size = sbr.getByteBuffer().getInt();
162 | return this.commitLog.getMessage(commitLogOffset, size);
163 | }
164 |
165 | }
166 | ```
167 |
168 | 
169 |
170 | ## 9.4.消息队列偏移量查询
171 |
172 | 根据队列偏移量查询是最简单的一种查询方式,Admin 会启动一个 PullConsumer ,然后利用用户传递给 Admin 的队列 ID、队列偏移量等信息,从服务器拉取一条消息过来:
173 |
174 | ```java
175 | public class QueryMsgByOffsetSubCommand implements SubCommand {
176 |
177 | @Override
178 | public void execute(CommandLine commandLine, Options options, RPCHook rpcHook) throws SubCommandException {
179 | // 根据参数构建 MessageQueue
180 | MessageQueue mq = new MessageQueue();
181 | mq.setTopic(topic);
182 | mq.setBrokerName(brokerName);
183 | mq.setQueueId(Integer.parseInt(queueId));
184 |
185 | // 从 Broker 服务器拉取消息
186 | PullResult pullResult = defaultMQPullConsumer.pull(mq, "*", Long.parseLong(offset), 1);
187 | }
188 |
189 | }
190 | ```
191 |
192 | 
193 |
194 | ## 9.5.消息索引文件
195 |
196 | 在继续讲解剩下两种查询方式之前,我们必须先介绍以下 Broker 端的消息索引服务。
197 |
198 | 在之前提到过,每当一条消息发送过来之后,其会封装为一个 DispatchRequest 来下发给各个转发服务,而 CommitLogDispatcherBuildIndex 构建索引服务便是其中之一:
199 |
200 | ```java
201 | class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
202 |
203 | @Override
204 | public void dispatch(DispatchRequest request) {
205 | if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
206 | DefaultMessageStore.this.indexService.buildIndex(request);
207 | }
208 | }
209 |
210 | }
211 | ```
212 |
213 | ### 9.5.1.索引文件结构
214 |
215 | 消息的索引信息是存放在磁盘上的,文件以时间戳命名的,默认存放在 $HOME/store/index 目录下。由下图来看,一个索引文件的结构被分成了三部分:
216 |
217 | * 前 40 个字节存放固定的索引头信息,包含了存放在这个索引文件中的消息的最小/大存储时间、最小/大偏移量等状况
218 | * 中间一段存储了 500 万个哈希槽位,每个槽内部存储的是索引文件的地址 (索引槽)
219 | * 最后一段存储了 2000 万个索引内容信息,是实际的索引信息存储的地方。每一个槽位存储了这条消息的键哈希值、存储偏移量、存储时间戳与下一个索引槽地址
220 |
221 | 
222 |
223 | RocketMQ 在内存中还维护了一个索引文件列表,对于每一个索引文件,前一个文件的最大存储时间是下一个文件的最小存储时间,前一个文件的最大偏移量是下一个文件的最大偏移量。每一个索引文件都索引了在某个时间段内、某个偏移量段内的所有消息,当文件满了,就会用前一个文件的最大偏移量和最大存储时间作为起始值,创建下一个索引文件:
224 |
225 | 
226 |
227 | ### 9.5.2.添加消息
228 |
229 | 当有新的消息过来后,构建索引服务会取出这条消息的键,然后对字符串 “话题#键” 构建索引。构建索引的步骤如下:
230 |
231 | * 找出哈希槽:生成字符串哈希码,取余落到 500W 个槽位之一,并取出其中的值,默认为 0
232 | * 找出索引槽:IndexHeader 维护了 indexCount,实际存储的索引槽就是直接依次顺延添加的
233 | * 存储索引内容:找到索引槽后,放入键哈希值、存储偏移量、存储时间戳与下一个索引槽地址。下一个索引槽地址就是第一步哈希槽中取出的值,0 代表这个槽位是第一次被索引,而不为 0 代表这个槽位之前的索引槽地址。由此,通过索引槽地址可以将相同哈希槽的消息串联起来,像单链表那样。
234 | * 更新哈希槽:更新原有哈希槽中存储的值
235 |
236 | 我们以实际例子来说明。假设我们需要依次为键的哈希值为 “{16,29,29,8,16,16}” 这几条消息构建索引,我们在这个地方忽略了索引信息中存储的存储时间和偏移量字段,只是存储键哈希和下一索引槽信息,那么:
237 |
238 | 1. 放入 16:将 “16|0” 存储在第 1 个索引槽中,并更新哈希槽为 16 的值为 1,即哈希槽为 16 的第一个索引块的地址为 1
239 | 2. 放入 29:将 “29|0” 存储在第 2 个索引槽中,并更新哈希槽为 29 的值为 2,即哈希槽为 29 的第一个索引块的地址为 2
240 | 3. 放入 29:取出哈希槽为 29 中的值 2,然后将 “29|2” 存储在第 3 个索引槽中,并更新哈希槽为 29 的值为 3,即哈希槽为 29 的第一个索引块的地址为 3。而在找到索引块为 3 的索引信息后,又能取出上一个索引块的地址 2,构成链表为: “[29]->3->2”
241 | 4. 放入 8:将 “8|0” 存储在第 4 个索引槽中,并更新哈希槽为 8 的值为 4,即哈希槽为 8 的第一个索引块的地址为 4
242 | 5. 放入 16:取出哈希槽为 16 中的值 1,然后将 “16|1” 存储在第 5 个索引槽中,并更新哈希槽为 16 的值为 5。构成链表为: “[16]->5->1”
243 | 6. 放入 16:取出哈希槽为 16 中的值 5,然后将 “16|5” 存储在第 6 个索引槽中,并更新哈希槽为 16 的值为 6。构成链表为: “[16]->6->5->1”
244 |
245 | 
246 |
247 | ### 9.5.3.查询消息
248 |
249 | 当需要根据键来查询消息的时候,其会按照倒序回溯整个索引文件列表,对于每一个在时间上能够匹配用户传入的 begin 和 end 时间戳参数的索引文件,会一一进行消息查询:
250 |
251 | ```java
252 | public class IndexService {
253 |
254 | public QueryOffsetResult queryOffset(String topic, String key, int maxNum, long begin, long end) {
255 | // 倒序
256 | for (int i = this.indexFileList.size(); i > 0; i--) {
257 | // 位于时间段内
258 | if (f.isTimeMatched(begin, end)) {
259 | // 消息查询
260 | }
261 | }
262 | }
263 |
264 | }
265 | ```
266 |
267 | 而具体到每一个索引文件,其查询匹配消息的过程如下所示:
268 |
269 | * 确定哈希槽:根据键生成哈希值,定位到哈希槽
270 | * 定位索引槽:哈希槽中的值存储的就是链表的第一个索引槽地址
271 | * 遍历索引槽:沿着索引槽地址,依次取出下一个索引槽地址,即沿着链表遍历,直至遇见下一个索引槽地址为非法地址 0 停止
272 | * 收集偏移量:在遇到匹配的消息之后,会将相应的物理偏移量放到列表中,最后根据物理偏移量,从 CommitLog 文件中取出消息
273 |
274 | ```java
275 | public class DefaultMessageStore implements MessageStore {
276 |
277 | @Override
278 | public QueryMessageResult queryMessage(String topic, String key, int maxNum, long begin, long end) {
279 |
280 | for (int m = 0; m < queryOffsetResult.getPhyOffsets().size(); m++) {
281 | long offset = queryOffsetResult.getPhyOffsets().get(m);
282 | // 根据偏移量从 CommitLog 文件中取出消息
283 | }
284 | }
285 | }
286 | ```
287 |
288 | 以查询哈希值 16 的消息为例,图示如下:
289 |
290 | 
291 |
292 | ## 9.6.唯一键查询
293 |
294 | ### 9.6.1.构建键
295 |
296 | 消息的唯一键是在客户端发送消息前构建的:
297 |
298 | ```java
299 | public class DefaultMQProducerImpl implements MQProducerInner {
300 | private SendResult sendKernelImpl(final Message msg, /** 其它参数 **/) throws XXXException {
301 | // ...
302 | if (!(msg instanceof MessageBatch)) {
303 | MessageClientIDSetter.setUniqID(msg);
304 | }
305 | }
306 | }
307 | ```
308 |
309 | 创建唯一 ID 的算法:
310 |
311 | ```java
312 | public class MessageClientIDSetter {
313 |
314 | public static String createUniqID() {
315 | StringBuilder sb = new StringBuilder(LEN * 2);
316 | sb.append(FIX_STRING);
317 | sb.append(UtilAll.bytes2string(createUniqIDBuffer()));
318 | return sb.toString();
319 | }
320 |
321 | }
322 | ```
323 |
324 | 唯一键是根据客户端的进程 ID、IP 地址、ClassLoader 哈希码、时间戳、计数器这几个值来生成的一个唯一的键,然后作为这条消息的附属属性发送到 Broker 服务器的:
325 |
326 | ```java
327 | public class MessageClientIDSetter {
328 |
329 | public static void setUniqID(final Message msg) {
330 | if (msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX) == null) {
331 | msg.putProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, createUniqID());
332 | }
333 | }
334 |
335 | }
336 | ```
337 |
338 | ### 9.6.2.索引键
339 |
340 | 当服务器收到客户端发送过来的消息之后,索引服务便会取出客户端生成的 uniqKey 并为之建立索引,放入到索引文件中:
341 |
342 | ```java
343 | public class IndexService {
344 |
345 | public void buildIndex(DispatchRequest req) {
346 | // ...
347 | if (req.getUniqKey() != null) {
348 | indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
349 | }
350 | // ...
351 | }
352 |
353 | }
354 | ```
355 |
356 | ### 9.6.3.使用键查询
357 |
358 | 客户端在生成消息唯一键的时候,在 ByteBuffer 的第 11 位到第 14 位放置的是当前的时间与当月第一天的时间的毫秒差:
359 |
360 | ```java
361 | public class MessageClientIDSetter {
362 |
363 | private static byte[] createUniqIDBuffer() {
364 | long current = System.currentTimeMillis();
365 | if (current >= nextStartTime) {
366 | setStartTime(current);
367 | }
368 |
369 | // 时间差 [当前时间 - 这个月 1 号的时间]
370 | // putInt 占据的是第 11 位到第 14 位
371 | buffer.putInt((int) (System.currentTimeMillis() - startTime));
372 | }
373 |
374 | private synchronized static void setStartTime(long millis) {
375 | Calendar cal = Calendar.getInstance();
376 | cal.setTimeInMillis(millis);
377 | cal.set(Calendar.DAY_OF_MONTH, 1);
378 | cal.set(Calendar.HOUR_OF_DAY, 0);
379 | cal.set(Calendar.MINUTE, 0);
380 | cal.set(Calendar.SECOND, 0);
381 | cal.set(Calendar.MILLISECOND, 0);
382 | // 开始时间设置为这个月的 1 号
383 | startTime = cal.getTimeInMillis();
384 | // ...
385 | }
386 |
387 | }
388 | ```
389 |
390 | 我们知道消息索引服务的查询需要用户传入 begin 和 end 这连个时间值,以进行这段时间内的匹配。所以 RocketMQ 为了加速消息的查询,于是在 Admin 端对特定 ID 进行查询的时候,首先取出了这段时间差值,然后与当月时间进行相加得到 begin 时间值:
391 |
392 | ```java
393 | public class MessageClientIDSetter {
394 |
395 | public static Date getNearlyTimeFromID(String msgID) {
396 | ByteBuffer buf = ByteBuffer.allocate(8);
397 | byte[] bytes = UtilAll.string2bytes(msgID);
398 | buf.put((byte) 0);
399 | buf.put((byte) 0);
400 | buf.put((byte) 0);
401 | buf.put((byte) 0);
402 | // 取出第 11 位到 14 位
403 | buf.put(bytes, 10, 4);
404 |
405 | buf.position(0);
406 | // 得到时间差值
407 | long spanMS = buf.getLong();
408 |
409 | Calendar cal = Calendar.getInstance();
410 | long now = cal.getTimeInMillis();
411 | cal.set(Calendar.DAY_OF_MONTH, 1);
412 | cal.set(Calendar.HOUR_OF_DAY, 0);
413 | cal.set(Calendar.MINUTE, 0);
414 | cal.set(Calendar.SECOND, 0);
415 | cal.set(Calendar.MILLISECOND, 0);
416 | long monStartTime = cal.getTimeInMillis();
417 | if (monStartTime + spanMS >= now) {
418 | cal.add(Calendar.MONTH, -1);
419 | monStartTime = cal.getTimeInMillis();
420 | }
421 | // 设置为这个月(或者上个月) + 时间差值
422 | cal.setTimeInMillis(monStartTime + spanMS);
423 | return cal.getTime();
424 | }
425 |
426 | }
427 | ```
428 |
429 | 由于发送消息的客户端和查询消息的 Admin 端可能不在一台服务器上,而且从函数的命名 getNearlyTimeFromID 与上述实现来看,Admin 端的时间戳得到的是一个近似起始值,它尽可能地加速用户的查询。而且太旧的消息(超过一个月的消息)是查询不到的。
430 |
431 | 当 begin 时间戳确定以后,Admin 便会将其它必要的信息如话题、Key等信息封装到 QUERY_MESSAGE 的包中,然后向 Broker 服务器传递这个请求,来进行消息的查询。Broker 服务器在获取到这个查询消息的请求后,便会根据 Key 从索引文件中查询符合的消息,最终返回到 Admin 端。
432 |
433 | ## 9.7.键查询消息
434 |
435 | ### 9.7.1.构建键
436 |
437 | 我们提到过,在发送消息的时候,可以填充一个 keys 的值,这个值将会作为消息的一个属性被发送到 Broker 服务器上:
438 |
439 | ```java
440 | public class Message implements Serializable {
441 |
442 | public void setKeys(String keys) {
443 | this.putProperty(MessageConst.PROPERTY_KEYS, keys);
444 | }
445 |
446 | }
447 | ```
448 |
449 | ### 9.7.2.索引键
450 |
451 | 当服务器收到客户端发送过来的消息之后,索引服务便会取出这条消息的 keys 并将其用空格进行分割,分割后的每一个字符串都会作为一个单独的键,创建索引,放入到索引文件中:
452 |
453 | ```java
454 | public class IndexService {
455 |
456 | public void buildIndex(DispatchRequest req) {
457 | // ...
458 | if (keys != null && keys.length() > 0) {
459 | // 使用空格进行分割
460 | String[] keyset = keys.split(MessageConst.KEY_SEPARATOR);
461 | for (int i = 0; i < keyset.length; i++) {
462 | String key = keyset[i];
463 | if (key.length() > 0) {
464 | indexFile = putKey(indexFile, msg, buildKey(topic, key));
465 | }
466 | }
467 | }
468 | }
469 |
470 | }
471 | ```
472 |
473 | 由此我们也可以得知,keys 键的设置通过使用空格分割字符串,一条消息可以指定多个键。
474 |
475 |
476 | ### 9.7.3.使用键查询
477 |
478 | keys 键查询的方式也是通过将参数封装为 QUERY_MESSAGE 请求包中去请求服务器返回相应的信息。由于键本身不能和时间戳相关联,因此 begin 值设置的是 0,这是和第五节的不同之处:
479 |
480 | ```java
481 | public class QueryMsgByKeySubCommand implements SubCommand {
482 |
483 | private void queryByKey(final DefaultMQAdminExt admin, final String topic, final String key)
484 | throws MQClientException, InterruptedException {
485 | // begin: 0
486 | // end: Long.MAX_VALUE
487 | QueryResult queryResult = admin.queryMessage(topic, key, 64, 0, Long.MAX_VALUE);
488 | }
489 |
490 | }
491 | ```
--------------------------------------------------------------------------------
/源码阅读/assets/2019-07-28-182136.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clsaa/RocketMQ-Notes/acab0b3cf001c4fec608c39229439274c3b7493a/源码阅读/assets/2019-07-28-182136.png
--------------------------------------------------------------------------------
/源码阅读/assets/2019-08-03-094005-20190803174242695.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clsaa/RocketMQ-Notes/acab0b3cf001c4fec608c39229439274c3b7493a/源码阅读/assets/2019-08-03-094005-20190803174242695.png
--------------------------------------------------------------------------------
/源码阅读/assets/2019-08-03-094005.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clsaa/RocketMQ-Notes/acab0b3cf001c4fec608c39229439274c3b7493a/源码阅读/assets/2019-08-03-094005.png
--------------------------------------------------------------------------------
/源码阅读/assets/2019-08-03-094227-20190803174248610.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clsaa/RocketMQ-Notes/acab0b3cf001c4fec608c39229439274c3b7493a/源码阅读/assets/2019-08-03-094227-20190803174248610.png
--------------------------------------------------------------------------------
/源码阅读/assets/2019-08-03-094227-20190803174314915.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clsaa/RocketMQ-Notes/acab0b3cf001c4fec608c39229439274c3b7493a/源码阅读/assets/2019-08-03-094227-20190803174314915.png
--------------------------------------------------------------------------------
/源码阅读/assets/2019-08-03-094227.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clsaa/RocketMQ-Notes/acab0b3cf001c4fec608c39229439274c3b7493a/源码阅读/assets/2019-08-03-094227.png
--------------------------------------------------------------------------------
/源码阅读/assets/2019-08-03-121649.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clsaa/RocketMQ-Notes/acab0b3cf001c4fec608c39229439274c3b7493a/源码阅读/assets/2019-08-03-121649.png
--------------------------------------------------------------------------------
/源码阅读/assets/2019-08-03-125305.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clsaa/RocketMQ-Notes/acab0b3cf001c4fec608c39229439274c3b7493a/源码阅读/assets/2019-08-03-125305.png
--------------------------------------------------------------------------------