├── .gitignore ├── 1-RocketMQ实战-核心概念详解.md ├── 10-SpringBoot整合RocketMQ.md ├── 2-RocketMQ实战-双master模式集群搭建.md ├── 3-RocketMQ实战-控制台.md ├── 4-RocketMQ实战-HelloWorld.md ├── 5-RocketMQ实战-顺序消息.md ├── 6-RocketMQ实战-事务消息.md ├── 7-RocketMQ实战-消息重试机制.md ├── 8-RocketMQ实战-幂等与去重.md ├── 9-RockerMQ实战-消息模式.md ├── README.md ├── RocketMQ运维指令整理.docx ├── SUMMARY.md └── 源码阅读 ├── 0.RocketMQ核心.md ├── 1.环境准备.md ├── 2.RocketMQ路由中心.md ├── 3.RocketMQ消息发送.md ├── 4.RocketMQ消息存储.md ├── 5.RocketMQ消息消费.md ├── 6.RocketMQ消息过滤.md ├── 7.RocketMQ主从同步(HA)机制.md ├── 8.RocketMQ事务消息.md ├── 9.RocketMQ消息查询.md └── assets ├── 2019-07-28-182136.png ├── 2019-08-03-094005-20190803174242695.png ├── 2019-08-03-094005.png ├── 2019-08-03-094227-20190803174248610.png ├── 2019-08-03-094227-20190803174314915.png ├── 2019-08-03-094227.png ├── 2019-08-03-121649.png └── 2019-08-03-125305.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf 17 | -------------------------------------------------------------------------------- /1-RocketMQ实战-核心概念详解.md: -------------------------------------------------------------------------------- 1 | * [RocketMQ用户指南](RocketMQ用户指南v3.2.4.pdf) 2 | * [rocketMQ使用手册](rocketMQ使用手册.pdf) -------------------------------------------------------------------------------- /10-SpringBoot整合RocketMQ.md: -------------------------------------------------------------------------------- 1 | # 10-SpringBoot整合RocketMQ 2 | 3 | ## 10.1 Maven依赖 4 | 5 | ### 10.1.1 父项目maven依赖 6 | 7 | ```xml 8 | 9 | 12 | 4.0.0 13 | 14 | com.clsaa.edu.rocketmq 15 | trade-system 16 | pom 17 | 1.0-SNAPSHOT 18 | 19 | trade-order 20 | trade-pay 21 | trade-web 22 | trade-coupon 23 | trade-common 24 | 25 | 26 | UTF-8 27 | UTF-8 28 | 1.8 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-parent 34 | 1.4.4.RELEASE 35 | 36 | 37 | 38 | 39 | 40 | com.alibaba.rockermq 41 | rocketmq-client 42 | 3.2.6 43 | 44 | 45 | 46 | commons-lang 47 | commons-lang 48 | 2.6 49 | 50 | 51 | 52 | 53 | 54 | 55 | ``` 56 | 57 | ### 10.1.2 子项目依赖 58 | 59 | ```xml 60 | 61 | 64 | 65 | trade-system 66 | com.clsaa.edu.rocketmq 67 | 1.0-SNAPSHOT 68 | 69 | 4.0.0 70 | 71 | trade-common 72 | 73 | 74 | 75 | com.alibaba.rocketmq 76 | rocketmq-client 77 | 3.2.6 78 | 79 | 80 | 81 | commons-lang 82 | commons-lang 83 | 84 | 85 | 86 | org.springframework.boot 87 | spring-boot-starter-actuator 88 | 89 | 90 | org.springframework.boot 91 | spring-boot-starter-web 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | org.springframework.boot 104 | spring-boot-starter-test 105 | 106 | 107 | 108 | 109 | 110 | ``` 111 | 112 | ## 10.2 配置文件 113 | 114 | ```yaml 115 | rocketmq: 116 | producer: 117 | # 发送同一类消息的设置为同一个group,保证唯一,默认不需要设置,rocketmq会使用ip@pid(pid代表jvm名字)作为唯一标示 118 | groupName: vehicleProducerGroup 119 | #mq的nameserver地址 120 | namesrvAddr: 123.206.175.47:9876;182.254.210.72:9876 121 | #如果需要同一个jvm中不同的producer往不同的mq集群发送消息,需要设置不同的instanceName 122 | instanceName: vehicleProducer 123 | #topic名称 124 | topic: TEST 125 | #根据实际情况设置消息的tag 126 | tag: TEST 127 | #消息最大长度 128 | maxMessageSize: 131072 # 1024*128 129 | #发送消息超时时间 130 | sendMsgTimeout: 10000 131 | consumer: 132 | namesrvAddr: 123.206.175.47:9876;182.254.210.72:9876 133 | groupName: vehicleProducerGroup 134 | topic: sms 135 | tag: verifycode 136 | consumeThreadMin: 20 137 | consumeThreadMax: 64 138 | ``` 139 | 140 | ## 10.3 启动类 141 | 142 | ``` 143 | package com.clsaa.edu.trade.common.rocketmq; 144 | 145 | import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer; 146 | import com.alibaba.rocketmq.client.exception.MQBrokerException; 147 | import com.alibaba.rocketmq.client.exception.MQClientException; 148 | import com.alibaba.rocketmq.client.producer.DefaultMQProducer; 149 | import com.alibaba.rocketmq.client.producer.SendResult; 150 | import com.alibaba.rocketmq.common.message.Message; 151 | import com.alibaba.rocketmq.remoting.exception.RemotingException; 152 | import org.springframework.boot.SpringApplication; 153 | import org.springframework.boot.SpringBootConfiguration; 154 | import org.springframework.boot.autoconfigure.SpringBootApplication; 155 | import org.springframework.context.ApplicationContext; 156 | import org.springframework.context.ConfigurableApplicationContext; 157 | 158 | /** 159 | * Created by eggyer on 2017/3/25. 160 | */ 161 | @SpringBootApplication 162 | public class RocketMQApp { 163 | public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { 164 | 165 | ApplicationContext context = SpringApplication.run(RocketMQApp.class,args); 166 | DefaultMQProducer defaultMQProducer = context.getBean(DefaultMQProducer.class); 167 | Message msg = new Message("TEST",// topic 168 | "TEST",// tag 169 | "KKK",//key用于标识业务的唯一性 170 | ("Hello RocketMQ !!!!!!!!!!" ).getBytes()// body 二进制字节数组 171 | ); 172 | SendResult result = defaultMQProducer.send(msg); 173 | System.out.println(result); 174 | DefaultMQPushConsumer consumer = context.getBean(DefaultMQPushConsumer.class); 175 | 176 | } 177 | } 178 | 179 | ``` 180 | 181 | ## 10.4 自定义异常 182 | 183 | ``` 184 | package com.clsaa.edu.trade.common.rocketmq.exception; 185 | 186 | /** 187 | * Created by eggyer on 2017/3/25. 188 | */ 189 | public class RocketMQException extends Exception{ 190 | public RocketMQException() { 191 | super(); 192 | } 193 | 194 | public RocketMQException(String message) { 195 | super(message); 196 | } 197 | 198 | public RocketMQException(String message, Throwable cause) { 199 | super(message, cause); 200 | } 201 | 202 | public RocketMQException(Throwable cause) { 203 | super(cause); 204 | } 205 | 206 | protected RocketMQException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 207 | super(message, cause, enableSuppression, writableStackTrace); 208 | } 209 | } 210 | 211 | ``` 212 | 213 | ## 10.5 实现 MessageListenerConcurrently接口 214 | 215 | ``` 216 | package com.clsaa.edu.trade.common.rocketmq.conf; 217 | 218 | import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; 219 | import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; 220 | import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently; 221 | import com.alibaba.rocketmq.common.message.MessageExt; 222 | 223 | import java.util.List; 224 | 225 | /** 226 | * Created by eggyer on 2017/3/25. 227 | */ 228 | public class MessageListener implements MessageListenerConcurrently{ 229 | 230 | private MessageProcessor messageProcessor; 231 | 232 | public void setMessageProcessor(MessageProcessor messageProcessor) { 233 | this.messageProcessor = messageProcessor; 234 | } 235 | 236 | @Override 237 | public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { 238 | for (MessageExt msg : msgs){ 239 | boolean result = messageProcessor.handleMessage(msg); 240 | if (!result){ 241 | return ConsumeConcurrentlyStatus.RECONSUME_LATER; 242 | } 243 | } 244 | return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; 245 | } 246 | } 247 | 248 | ``` 249 | 250 | ## 10.6 设计消息处理接口 251 | 252 | ``` 253 | package com.clsaa.edu.trade.common.rocketmq.conf; 254 | 255 | import com.alibaba.rocketmq.common.message.MessageExt; 256 | 257 | /** 258 | * Created by eggyer on 2017/3/25. 259 | */ 260 | public interface MessageProcessor { 261 | /** 262 | * 处理消息的接口 263 | * @param messageExt 264 | * @return 265 | */ 266 | public boolean handleMessage(MessageExt messageExt); 267 | } 268 | 269 | ``` 270 | 271 | ## 10.7 实现消息处理接口 272 | 273 | ``` 274 | package com.clsaa.edu.trade.common.rocketmq.conf; 275 | 276 | import com.alibaba.rocketmq.common.message.MessageExt; 277 | import org.springframework.stereotype.Component; 278 | 279 | /** 280 | * Created by eggyer on 2017/3/26. 281 | */ 282 | @Component 283 | public class MessageProcessorImplTest implements MessageProcessor { 284 | @Override 285 | public boolean handleMessage(MessageExt messageExt) { 286 | System.out.println("receive : " + messageExt.toString()); 287 | return true; 288 | } 289 | } 290 | 291 | ``` 292 | 293 | ## 10.8 实现生产者 294 | 295 | ``` 296 | package com.clsaa.edu.trade.common.rocketmq.conf; 297 | 298 | import com.alibaba.rocketmq.client.exception.MQClientException; 299 | import com.alibaba.rocketmq.client.producer.DefaultMQProducer; 300 | import com.clsaa.edu.trade.common.rocketmq.exception.RocketMQException; 301 | import org.apache.commons.lang.StringUtils; 302 | import org.slf4j.Logger; 303 | import org.slf4j.LoggerFactory; 304 | import org.springframework.beans.factory.annotation.Value; 305 | import org.springframework.boot.SpringBootConfiguration; 306 | import org.springframework.context.annotation.Bean; 307 | 308 | /** 309 | * Created by eggyer on 2017/3/25. 310 | */ 311 | @SpringBootConfiguration 312 | public class RocketMQProducerConfiguration { 313 | public static final Logger LOGGER = LoggerFactory.getLogger(RocketMQProducerConfiguration.class); 314 | @Value("${rocketmq.producer.groupName}") 315 | private String groupName; 316 | @Value("${rocketmq.producer.namesrvAddr}") 317 | private String namesrvAddr; 318 | @Value("${rocketmq.producer.instanceName}") 319 | private String instanceName; 320 | @Value("${rocketmq.producer.maxMessageSize}") 321 | private int maxMessageSize ; //4M 322 | @Value("${rocketmq.producer.sendMsgTimeout}") 323 | private int sendMsgTimeout ; 324 | 325 | @Bean 326 | public DefaultMQProducer getRocketMQProducer() throws RocketMQException { 327 | if (StringUtils.isBlank(this.groupName)) { 328 | throw new RocketMQException("groupName is blank"); 329 | } 330 | if (StringUtils.isBlank(this.namesrvAddr)) { 331 | throw new RocketMQException("nameServerAddr is blank"); 332 | } 333 | if (StringUtils.isBlank(this.instanceName)){ 334 | throw new RocketMQException("instanceName is blank"); 335 | } 336 | DefaultMQProducer producer; 337 | producer = new DefaultMQProducer(this.groupName); 338 | producer.setNamesrvAddr(this.namesrvAddr); 339 | producer.setInstanceName(instanceName); 340 | producer.setMaxMessageSize(this.maxMessageSize); 341 | producer.setSendMsgTimeout(this.sendMsgTimeout); 342 | try { 343 | producer.start(); 344 | LOGGER.info(String.format("producer is start ! groupName:[%s],namesrvAddr:[%s]" 345 | , this.groupName, this.namesrvAddr)); 346 | } catch (MQClientException e) { 347 | LOGGER.error(String.format("producer is error {}" 348 | , e.getMessage(),e)); 349 | throw new RocketMQException(e); 350 | } 351 | return producer; 352 | } 353 | } 354 | 355 | 356 | 357 | ``` 358 | 359 | ## 10.9 实现消费者 360 | 361 | ``` 362 | package com.clsaa.edu.trade.common.rocketmq.conf; 363 | 364 | import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer; 365 | import com.alibaba.rocketmq.client.exception.MQClientException; 366 | import com.clsaa.edu.trade.common.rocketmq.exception.RocketMQException; 367 | import org.apache.commons.lang.StringUtils; 368 | import org.slf4j.Logger; 369 | import org.slf4j.LoggerFactory; 370 | import org.springframework.beans.factory.annotation.Autowired; 371 | import org.springframework.beans.factory.annotation.Qualifier; 372 | import org.springframework.beans.factory.annotation.Value; 373 | import org.springframework.boot.SpringBootConfiguration; 374 | import org.springframework.context.annotation.Bean; 375 | 376 | 377 | /** 378 | * Created by eggyer on 2017/3/25. 379 | */ 380 | @SpringBootConfiguration 381 | public class RocketMQConsumerConfiguration { 382 | public static final Logger LOGGER = LoggerFactory.getLogger(RocketMQConsumerConfiguration.class); 383 | @Value("${rocketmq.consumer.namesrvAddr}") 384 | private String namesrvAddr; 385 | @Value("${rocketmq.consumer.groupName}") 386 | private String groupName; 387 | @Value("${rocketmq.consumer.topic}") 388 | private String topic; 389 | @Value("${rocketmq.consumer.tag}") 390 | private String tag; 391 | @Value("${rocketmq.consumer.consumeThreadMin}") 392 | private int consumeThreadMin; 393 | @Value("${rocketmq.consumer.consumeThreadMax}") 394 | private int consumeThreadMax; 395 | 396 | @Autowired 397 | @Qualifier("messageProcessorImplTest") 398 | private MessageProcessor messageProcessor; 399 | 400 | @Bean 401 | public DefaultMQPushConsumer getRocketMQConsumer() throws RocketMQException { 402 | if (StringUtils.isBlank(groupName)){ 403 | throw new RocketMQException("groupName is null !!!"); 404 | } 405 | if (StringUtils.isBlank(namesrvAddr)){ 406 | throw new RocketMQException("namesrvAddr is null !!!"); 407 | } 408 | if (StringUtils.isBlank(topic)){ 409 | throw new RocketMQException("topic is null !!!"); 410 | } 411 | if (StringUtils.isBlank(tag)){ 412 | throw new RocketMQException("tag is null !!!"); 413 | } 414 | DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName); 415 | consumer.setNamesrvAddr(namesrvAddr); 416 | consumer.setConsumeThreadMin(consumeThreadMin); 417 | consumer.setConsumeThreadMax(consumeThreadMax); 418 | MessageListener messageListener = new MessageListener(); 419 | messageListener.setMessageProcessor(messageProcessor); 420 | consumer.registerMessageListener(messageListener); 421 | try { 422 | consumer.subscribe(topic,this.tag); 423 | consumer.start(); 424 | LOGGER.info("consumer is start !!! groupName:{},topic:{},namesrvAddr:{}",groupName,topic,namesrvAddr); 425 | }catch (MQClientException e){ 426 | LOGGER.error("consumer is start !!! groupName:{},topic:{},namesrvAddr:{}",groupName,topic,namesrvAddr,e); 427 | throw new RocketMQException(e); 428 | } 429 | return consumer; 430 | } 431 | 432 | } 433 | 434 | ``` -------------------------------------------------------------------------------- /2-RocketMQ实战-双master模式集群搭建.md: -------------------------------------------------------------------------------- 1 | 1. 添加hosts信息 2 | 3 | 4 | IP | Name 5 | ---|--- 6 | 123.206.175.47 | rocketmq-nameserver01 7 | 123.206.175.47 | rocketmq-master01 8 | 182.254.210.72 | rocketmq-nameserver02 9 | 182.254.210.72 | rocketmq-master02 10 | 11 | - 编辑主机名,master可有可可无但nameserver必须配置 12 | 13 | ``` 14 | [root@VM_1_209_centos java]# vim /etc/hosts 15 | ``` 16 | 17 | ``` 18 | [root@VM_201_98_centos java]# vim /etc/hosts 19 | 20 | 127.0.0.1 localhost localhost.localdomain VM_201_98_centos 21 | 22 | 123.206.175.47 rocketmq-nameserver01 23 | 182.254.210.72 rocketmq-nameserver02 24 | 25 | 26 | 123.206.175.47 eggyer-node001 27 | 182.254.210.72 eggyer-node002 28 | 29 | ``` 30 | 31 | 2. 上传解压 32 | 1. 安装java(不再赘述) 33 | 2. 安装rocketmq 34 | - 解压 35 | 36 | ``` 37 | [root@VM_1_209_centos java]# cd /usr/local/software/ 38 | [root@VM_1_209_centos software]# tar -zxvf alibaba-rocketmq-3.2.6.tar.gz -C /usr/local/ 39 | 40 | ``` 41 | - 重命名,增加版本标识 42 | 43 | ``` 44 | [root@VM_1_209_centos local]# mv alibaba-rocketmq/ alibaba-rocketmq-3.2.6 45 | ``` 46 | - 建立软连接 47 | ``` 48 | [root@VM_1_209_centos local]# ln -s alibaba-rocketmq-3.2.6 rocketmq 49 | 50 | ``` 51 | - 创建存储路径(rocketmq数据的存储位置),此处在软连接下简历store 52 | - 53 | ``` 54 | [root@VM_1_209_centos local]# mkdir /usr/local/rocketmq/store 55 | [root@VM_1_209_centos local]# mkdir /usr/local/rocketmq/store/commitlog 56 | [root@VM_1_209_centos local]# mkdir /usr/local/rocketmq/store/consumequeue 57 | [root@VM_1_209_centos local]# mkdir /usr/local/rocketmq/store/index 58 | [root@VM_1_209_centos local]# cd alibaba-rocketmq-3.2.6/ 59 | [root@VM_1_209_centos alibaba-rocketmq-3.2.6]# ls 60 | LICENSE.txt benchmark bin conf issues lib store test wiki 61 | 62 | ``` 63 | - 修改rocketmq配置文件(双master模式) 64 | - 进入conf目录下后会发现2m-2s-async,2m-2s-sync,2m-noslave等模式,我们需要需要配置2m-noslave文件 65 | ``` 66 | [root@VM_1_209_centos local]# cd rocketmq/ 67 | [root@VM_1_209_centos rocketmq]# cd conf/ 68 | [root@VM_1_209_centos conf]# ll 69 | total 32 70 | drwxr-xr-x 2 52583 users 4096 Mar 28 2015 2m-2s-async 71 | drwxr-xr-x 2 52583 users 4096 Mar 28 2015 2m-2s-sync 72 | drwxr-xr-x 2 52583 users 4096 Mar 28 2015 2m-noslave 73 | -rw-r--r-- 1 52583 users 7786 Mar 28 2015 logback_broker.xml 74 | -rw-r--r-- 1 52583 users 2331 Mar 28 2015 logback_filtersrv.xml 75 | -rw-r--r-- 1 52583 users 2313 Mar 28 2015 logback_namesrv.xml 76 | -rw-r--r-- 1 52583 users 2435 Mar 28 2015 logback_tools.xml 77 | [root@VM_1_209_centos conf]# 78 | 79 | ``` 80 | 81 | - 进入2m-noslave文件 82 | - 这两个配置文件就代表了两个主节点的一些信息,因为有两个master主节点,所以主节点1启动依赖broker-a.properties,主节点2启动依赖broker-b.properties,如果是三个Master,那么还会有一个broker-c.properties,以此类推。 83 | ``` 84 | [root@VM_1_209_centos conf]# cd 2m-noslave/ 85 | [root@VM_1_209_centos 2m-noslave]# ls 86 | broker-a.properties broker-b.properties 87 | 88 | ``` 89 | 90 | - 修改配置文件broker-a.properties,broker-b.properties 91 | - Linux系统的编码问题 92 | - 执行locale命令查看系统语言 93 | - 设置系统环境变量LANG为en_US.UTF-8:export LANG=en_US.UTF-8或者编辑文件:vim /etc/sysconfig/i18n 94 | ``` 95 | brokerClusterName=rocketmq-cluster 96 | #broker名字,注意此处不同的配置文件填写的不一样 97 | brokerName=broker-a 98 | #0 表示 Master, >0 表示 Slave 99 | brokerId=0 100 | #nameServer地址,分号分割 101 | namesrvAddr=rocketmq-nameserver01:9876;rocketmq-nameserver02:9876 102 | #在发送消息时,自动创建服务器不存在的topic,默认创建的队列数 103 | defaultTopicQueueNums=4 104 | #是否允许 Broker 自动创建Topic,建议线下开启,线上关闭 105 | autoCreateTopicEnable=true 106 | #是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭 107 | autoCreateSubscriptionGroup=true 108 | #Broker 对外服务的监听端口 109 | listenPort=10911 110 | #删除文件时间点,默认凌晨 0点 111 | deleteWhen=00 112 | #文件保留时间,默认 48 小时 113 | fileReservedTime=120 114 | #commitLog每个文件的大小默认1G 115 | mapedFileSizeCommitLog=1073741824 116 | #ConsumeQueue每个文件默认存30W条,根据业务情况调整 117 | mapedFileSizeConsumeQueue=300000 118 | #destroyMapedFileIntervalForcibly=120000 119 | #redeleteHangedFileInterval=120000 120 | #检测物理文件磁盘空间 121 | diskMaxUsedSpaceRatio=88 122 | #存储路径 123 | storePathRootDir=/usr/local/rocketmq/store 124 | #commitLog 存储路径 125 | storePathCommitLog=/usr/local/rocketmq/storecommitlog 126 | #消费队列存储路径存储路径 127 | storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue 128 | #消息索引存储路径 129 | storePathIndex=/usr/local/rocketmq/store/index 130 | #checkpoint 文件存储路径 131 | storeCheckpoint=/usr/local/rocketmq/store/checkpoint 132 | #abort 文件存储路径 133 | abortFile=/usr/local/rocketmq/store/abort 134 | #限制的消息大小 135 | maxMessageSize=65536 136 | #flushCommitLogLeastPages=4 137 | #flushConsumeQueueLeastPages=2 138 | #flushCommitLogThoroughInterval=10000 139 | #flushConsumeQueueThoroughInterval=60000 140 | #Broker 的角色 141 | #- ASYNC_MASTER 异步复制Master 142 | #- SYNC_MASTER 同步双写Master 143 | #- SLAVE 144 | brokerRole=ASYNC_MASTER 145 | #刷盘方式 146 | #- ASYNC_FLUSH 异步刷盘 147 | #- SYNC_FLUSH 同步刷盘 148 | flushDiskType=ASYNC_FLUSH 149 | #checkTransactionMessageEnable=false 150 | #发消息线程池数量 151 | #sendMessageThreadPoolNums=128 152 | #拉消息线程池数量 153 | #pullMessageThreadPoolNums=128 154 | 155 | ``` 156 | - 修改日志配置文件 157 | - 进行日志文件的替换,sed是linux的替换命令 158 | ``` 159 | [root@VM_1_209_centos ~]# mkdir -p /usr/local/rocketmq/logs 160 | [root@VM_1_209_centos ~]# cd /usr/local/rocketmq/conf && sed -i 's#${user.home}#/usr/local/rocketmq#g' *.xml 161 | [root@VM_1_209_centos conf]# 162 | 163 | ``` 164 | 165 | - 修改JVM启动参数 166 | - 应该先启动nameserver集群,再启动broker集群 167 | - jvm启动大小 168 | - 进入rocketmq的bin文件夹 169 | ``` 170 | [root@VM_1_209_centos rocketmq]# cd bin 171 | [root@VM_1_209_centos bin]# ls 172 | mqadmin mqbroker.exe mqbroker.numanode3 mqfiltersrv.xml mqshutdown runbroker.sh 173 | mqadmin.exe mqbroker.numanode0 mqbroker.xml mqnamesrv os.sh runserver.sh 174 | mqadmin.xml mqbroker.numanode1 mqfiltersrv mqnamesrv.exe play.sh startfsrv.sh 175 | mqbroker mqbroker.numanode2 mqfiltersrv.exe mqnamesrv.xml README.md tools.sh 176 | 177 | ``` 178 | - 打开runbroker会发现一些JVM配置参数 179 | - 调整-Xms4g -Xmx4g -Xmn2g 180 | 181 | ``` 182 | #=========================================================================================== 183 | # JVM Configuration 184 | #=========================================================================================== 185 | JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:PermSize=128m -XX:MaxPermSize=320m" 186 | JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:+DisableExplicitGC" 187 | JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${HOME}/rmq_bk_gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps" 188 | JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow" 189 | JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${BASE_DIR}/lib" 190 | #JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n" 191 | JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}" 192 | 193 | ``` 194 | 195 | - 启动nameserver,使用mqnamesrv脚本(nohup sh mqnamesrv &) 196 | - 修改java路径 197 | ``` 198 | [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java 199 | [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/opt/taobao/java 200 | [ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!" 201 | 改为 202 | [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/local/java/jdk1.8.0_121 203 | #[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/opt/taobao/java 204 | #[ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!" 205 | 206 | 207 | ``` 208 | 209 | - 启动broker 210 | 211 | ``` 212 | [root@VM_1_209_centos bin]# nohup sh mqbroker -c /usr/local/rocketmq/conf/2m-noslave/broker-a.properties > /dev/null 2>&1 & 213 | [2] 21821 214 | [root@VM_1_209_centos bin]# jps 215 | 21828 BrokerStartup 216 | 20734 NamesrvStartup 217 | 21871 Jps 218 | [root@VM_1_209_centos bin]# 219 | [root@VM_1_209_centos bin]# nohup sh mqbroker -c /usr/local/rocketmq/conf/2m-noslave/broker-b.properties > /dev/null 2>&1 & 220 | [2] 21821 221 | ``` 222 | 223 | ## 坑与问题 224 | 225 | 启动nameserver出现:ERROR: Please set the JAVA_HOME variable in your environment, We need Java(x64)! !! 226 | 227 | ``` 228 | 打开启动脚本runserver.sh以及runbroker.sh文件,发现有如下三行: 229 | 230 | [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java 231 | [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/opt/taobao/java 232 | [ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!" 233 | 234 | ``` 235 | - 其中第二行是阿里巴巴集团内部服务器上的java目录,将这一行注释掉。然后第一行的JAVA_HOME的值改为自己机器的Java安装目录。然后再次起送mqnameserver以及mqbroker,观察日志发现启动成功. -------------------------------------------------------------------------------- /3-RocketMQ实战-控制台.md: -------------------------------------------------------------------------------- 1 | # 3-RocketMQ控制台 2 | 3 | [TOC] 4 | 5 | ## 3.1 搭建Tomcat环境 6 | 7 | 8 | ## 3.2 RocketMQ-console 9 | 10 | 1. 使用XFTP上传war文件 11 | 2. 解压war文件 12 | ``` 13 | [root@VM_1_209_centos webapps]# unzip rocketmq-console-3.2.6.war -d rocketmq-console 14 | 15 | ``` 16 | 3. 编辑properties文件 17 | ``` 18 | [root@VM_1_209_centos webapps]# rm -rf rocketmq-console-3.2.6.war 19 | [root@VM_1_209_centos webapps]# ls 20 | ROOT docs examples host-manager manager rocketmq-console 21 | [root@VM_1_209_centos webapps]# cd rocketmq-console/ 22 | [root@VM_1_209_centos rocketmq-console]# ls 23 | META-INF WEB-INF css index.html js 24 | [root@VM_1_209_centos rocketmq-console]# cd WEB-INF/ 25 | [root@VM_1_209_centos WEB-INF]# ll 26 | total 28 27 | drwxr-xr-x 4 root root 4096 Jun 14 2016 classes 28 | -rw-r--r-- 1 root root 59 Jun 14 2016 index.html 29 | drwxr-xr-x 2 root root 4096 Jun 14 2016 lib 30 | -rw-r--r-- 1 root root 2382 Jun 14 2016 springmvc-servlet.xml 31 | -rw-r--r-- 1 root root 676 Jun 14 2016 velocity-toolbox.xml 32 | drwxr-xr-x 10 root root 4096 Jun 14 2016 vm 33 | -rw-r--r-- 1 root root 1073 Jun 14 2016 web.xml 34 | [root@VM_1_209_centos WEB-INF]# cd classes/ 35 | [root@VM_1_209_centos classes]# ls 36 | com config.properties logback.xml spring 37 | [root@VM_1_209_centos classes]# vim config.properties 38 | 39 | ``` 40 | 4. 启动tomcat 41 | 42 | ``` 43 | [root@VM_1_209_centos bin]# ./startup.sh 44 | Using CATALINA_BASE: /usr/local/rocketmq-console-tomcat 45 | Using CATALINA_HOME: /usr/local/rocketmq-console-tomcat 46 | Using CATALINA_TMPDIR: /usr/local/rocketmq-console-tomcat/temp 47 | Using JRE_HOME: /usr/local/java/jdk1.8.0_121 48 | Using CLASSPATH: /usr/local/rocketmq-console-tomcat/bin/bootstrap.jar:/usr/local/rocketmq-console-tomcat/bin/tomcat-juli.jar 49 | Tomcat started. 50 | 51 | ``` 52 | 53 | ## 坑与问题 54 | 55 | - 启动后发现控制台brokerAddr和实际不一致问题 56 | ``` 57 | 在broker配置文件中添加属性 58 | 59 | brokerIP1=实际ip地址 60 | 61 | 注意属性名为brokerIP1 确实有个1 62 | ``` -------------------------------------------------------------------------------- /4-RocketMQ实战-HelloWorld.md: -------------------------------------------------------------------------------- 1 | # 4-RocketMQ-HelloWorld 2 | 3 | [TOC] 4 | 5 | ## 4.1 HelloWorld基本模型 6 | 7 | - 我们要使用rockerMQ主要非为以下步骤 8 | - 生产者 9 | 1. 创建DefaultMQProducer类并设定生产者名称,设置serNamesrvAddr,集群模式使用";"进行分割,调用start方法启动即可 10 | 2. 使用message类进行实例化消息,参数分别为:主题/标签/内容 11 | 3. 调用send方法发送消息,并且关闭生产者即可 12 | - 消费者 13 | 1. 创建deaultMQPushConsumer类并设定消费者名称,设置setNamesrvAddr,集群模式用";"进行分割. 14 | 2. 设置defaultMQPushConsumer实例的订阅主题,一个消费者对象可以订阅多个主题,使用subscribe方法订阅(参数1为主题名,参数2为标签内容,可以使用"||"对标签内容进行合并获取) 15 | 3. 消费者实例进行注册监听:设置registerMessageListener方法 16 | 4. 监听类实现MessageListenerConcurrently接口即可,重写conbsumeMessage方法接受数据.ConsumeConcurrently接口即可,重写consumeMessage方法接受数据.(ConsumeConcurrentlyStatus.RECONSUME_LATER;ConsumeConcurrentlyStatus.CONSUME_SUCCESS) 17 | 5. 启动消费者实例对象,调用start方法即可 18 | 19 | 20 | ## 4.2 RocketMQ-HelloWorld代码 21 | 22 | - 引入pom文件 23 | 24 | ```xml 25 | 27 | 28 | com.alibaba.rocketmq 29 | rocketmq-all 30 | 3.2.6 31 | 32 | 33 | 4.0.0 34 | jar 35 | rocketmq-example 36 | rocketmq-example ${project.version} 37 | 38 | 39 | 40 | junit 41 | junit 42 | test 43 | 44 | 45 | ${project.groupId} 46 | rocketmq-client 47 | 48 | 49 | ${project.groupId} 50 | rocketmq-srvutil 51 | 52 | 53 | ch.qos.logback 54 | logback-classic 55 | 56 | 57 | ch.qos.logback 58 | logback-core 59 | 60 | 61 | jboss 62 | javassist 63 | 64 | 65 | 66 | 67 | ``` 68 | 69 | - producer 70 | 71 | ```java 72 | /** 73 | * Copyright (C) 2010-2013 Alibaba Group Holding Limited 74 | * 75 | * Licensed under the Apache License, Version 2.0 (the "License"); 76 | * you may not use this file except in compliance with the License. 77 | * You may obtain a copy of the License at 78 | * 79 | * http://www.apache.org/licenses/LICENSE-2.0 80 | * 81 | * Unless required by applicable law or agreed to in writing, software 82 | * distributed under the License is distributed on an "AS IS" BASIS, 83 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 84 | * See the License for the specific language governing permissions and 85 | * limitations under the License. 86 | */ 87 | package com.clsaa.edu.rocketmq.quickstart; 88 | 89 | import com.alibaba.rocketmq.client.exception.MQClientException; 90 | import com.alibaba.rocketmq.client.producer.DefaultMQProducer; 91 | import com.alibaba.rocketmq.client.producer.SendResult; 92 | import com.alibaba.rocketmq.common.message.Message; 93 | 94 | 95 | /** 96 | * Producer,发送消息 97 | * 98 | */ 99 | public class Producer { 100 | public static void main(String[] args) throws MQClientException, InterruptedException { 101 | DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); 102 | 103 | /** 104 | * producer配置项 105 | * producerGroup DEFAULT_PRODUCER (一个线程下只能有一个组,但是一个组下面可以有多个实例,生产者组) 106 | * Producer组名,多个producer如果属于一个应用,发送同样的消息,则应该将他们视为同一组 107 | * createTopicKey WBW102 在发送消息时,自动创建服务器不存在的topic,需制定key 108 | * defaultTopicQueueNums4 发送消息时,自动创建服务器不存在的topic,默认创建的队列数 109 | * sendMsgTimeout 10000 发送消息超时时间,单位毫秒 110 | * compressMsgBodyOverHowmuch 4096 消息body超过多大开始压缩(Consumer收到消息会自动解压缩),单位:字节 111 | * retryTimesWhenSendFailed 重试次数 (可以配置) 112 | * retryAnotherBrokerWhenNotStoreOK FALSE 如果发送消息返回sendResult,但是sendStatus!=SEND_OK,是否重发 113 | * maxMessageSize 131072 客户端限制消息的大小,超过报错,同时服务端也会限制(默认128k) 114 | * transactionCheckListener 事务消息回查监听器,如果发送事务消息,必须设置 115 | * checkThreadPoolMinSize 1 Broker回查Producer事务状态时,线程池大小 116 | * checkThreadPoolMaxSize 1 Broker回查Producer事务状态时,线程池大小 117 | * checkRequestHoldMax 2000 Broker回查Producer事务状态,Producer本地缓冲请求队列大小 118 | */ 119 | 120 | producer.setNamesrvAddr("123.206.175.47:9876;182.254.210.72:9876"); 121 | producer.start(); 122 | 123 | for (int i = 1; i <= 10; i++) { 124 | try { 125 | Message msg = new Message("TopicQuickStart",// topic 126 | "TagA",// tag 127 | "KKK",//key用于标识业务的唯一性 128 | ("Hello RocketMQ " + i).getBytes()// body 二进制字节数组 129 | ); 130 | SendResult sendResult = producer.send(msg); //ACK确认反馈,通过result判断消息发送成功还是失败 131 | System.out.println(sendResult); //msgID会在msg经过msgQueue逻辑结构之后才会有ID 132 | } 133 | catch (Exception e) { 134 | e.printStackTrace(); 135 | Thread.sleep(1000); 136 | } 137 | } 138 | 139 | producer.shutdown(); 140 | } 141 | } 142 | 143 | ``` 144 | 145 | - consumer 146 | 147 | ```java 148 | /** 149 | * Copyright (C) 2010-2013 Alibaba Group Holding Limited 150 | * 151 | * Licensed under the Apache License, Version 2.0 (the "License"); 152 | * you may not use this file except in compliance with the License. 153 | * You may obtain a copy of the License at 154 | * 155 | * http://www.apache.org/licenses/LICENSE-2.0 156 | * 157 | * Unless required by applicable law or agreed to in writing, software 158 | * distributed under the License is distributed on an "AS IS" BASIS, 159 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 160 | * See the License for the specific language governing permissions and 161 | * limitations under the License. 162 | */ 163 | package com.clsaa.edu.rocketmq.quickstart; 164 | 165 | import java.util.List; 166 | 167 | import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer; 168 | import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; 169 | import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; 170 | import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently; 171 | import com.alibaba.rocketmq.client.exception.MQClientException; 172 | import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere; 173 | import com.alibaba.rocketmq.common.message.MessageExt; 174 | 175 | 176 | /** 177 | * Consumer,订阅消息 178 | */ 179 | public class Consumer { 180 | 181 | public static void main(String[] args) throws InterruptedException, MQClientException { 182 | DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); 183 | /** 184 | * Push Consumer设置 185 | * messageModel CLUSTERING 消息模型,支持以下两种1.集群消费2.广播消费 186 | * consumeFromWhere CONSUME_FROM_LAST_OFFSET Consumer启动后,默认从什么位置开始消费 187 | * allocateMessageQueueStrategy 188 | * allocateMessageQueueAveragely Rebalance 算法实现策略 189 | * Subsription{} 订阅关系 190 | * messageListener 消息监听器 191 | * offsetStore 消费进度存储 192 | * consumeThreadMin 10 消费线程池数量 193 | * consumeThreadMax 20 消费线程池数量 194 | * pullThresholdForQueue 1000 拉去消息本地队列缓存消息最大数 195 | * pullInterval 拉消息间隔,由于是轮训,所以为0,但是如果用了流控,也可以设置大于0的值,单位毫秒 196 | * consumeMessageBatchMaxSize 1 批量消费,一次消费杜少条消息 197 | * pullBatchSize 32 批量拉消息,一次最多拉多少条 198 | * 199 | */ 200 | 201 | /** 202 | * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
203 | * 如果非第一次启动,那么按照上次消费的位置继续消费 204 | */ 205 | 206 | consumer.setNamesrvAddr("123.206.175.47:9876;182.254.210.72:9876"); 207 | consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); 208 | 209 | consumer.subscribe("TopicQuickStart", "TagA"); 210 | 211 | consumer.registerMessageListener(new MessageListenerConcurrently() { 212 | @Override 213 | public ConsumeConcurrentlyStatus consumeMessage(List msgs, 214 | ConsumeConcurrentlyContext context) { 215 | //System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs); 216 | MessageExt msg = msgs.get(0); 217 | try { 218 | String topic = msg.getTopic(); 219 | String msgBody = new String(msg.getBody(),"utf-8"); 220 | String tags = msg.getTags(); 221 | System.out.println("get massage : " + " topic : " + topic + " tags : " + tags + " msg : " +msgBody); 222 | }catch (Exception e){ 223 | e.printStackTrace(); 224 | return ConsumeConcurrentlyStatus.RECONSUME_LATER; //requeue 一会再消费 225 | } 226 | return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; // response broker ack 227 | } 228 | }); 229 | 230 | consumer.start(); 231 | 232 | System.out.println("Consumer Started."); 233 | } 234 | } 235 | 236 | ``` 237 | 238 | ## 4.2 较为重要的配置项说明 239 | 240 | * producer配置项 241 | * producerGroup DEFAULT_PRODUCER (一个线程下只能有一个组,但是一个组下面可以有多个实例,生产者组) 242 | * Producer组名,多个producer如果属于一个应用,发送同样的消息,则应该将他们视为同一组 243 | * sendMsgTimeout 10000 发送消息超时时间,单位毫秒 244 | * compressMsgBodyOverHowmuch 4096 消息body超过多大开始压缩(Consumer收到消息会自动解压缩),单位:字节 245 | * retryTimesWhenSendFailed 重试次数 (可以配置) 246 | * retryAnotherBrokerWhenNotStoreOK FALSE 如果发送消息返回sendResult,但是sendStatus!=SEND_OK,是否重发 247 | * maxMessageSize 131072 客户端限制消息的大小,超过报错,同时服务端也会限制(默认128k) 248 | 249 | * Push Consumer设置 250 | * messageModel CLUSTERING 消息模型,支持以下两种1.集群消费2.广播消费 251 | * consumeFromWhere CONSUME_FROM_LAST_OFFSET Consumer启动后,默认从什么位置开始消费 252 | * allocateMessageQueueStrategy 253 | * allocateMessageQueueAveragely Rebalance 算法实现策略 254 | * Subsription 订阅关系 255 | * messageListener 消息监听器 256 | * offsetStore 消费进度存储 257 | 258 | ## 4.3 坑与问题 259 | 260 | - com.alibaba.rocketmq.client.exception.MQClientException: Send [3] times, still failed, cost [49]ms 261 | 1. 检查nameserver和broker是否启动成功(查看两个日志) 262 | 2. 查看broker的IP地址是否正确,若不正确参照RockerMQ控制台一节的问题与坑对配置文件进行修改 -------------------------------------------------------------------------------- /5-RocketMQ实战-顺序消息.md: -------------------------------------------------------------------------------- 1 | # 5-RocketMQ-顺序消息 2 | 3 | [TOC] 4 | 5 | - RockerMQ可以严格保证消息的顺序消费 6 | - 遵循全局顺序消费的时候使用一个queue,局部顺序的时候可以使用多个queue并行消费 7 | 8 | ## 5.1 顺序消息代码实例 9 | 10 | - producer 11 | 12 | ```java 13 | /** 14 | * Copyright (C) 2010-2013 Alibaba Group Holding Limited 15 | * 16 | * Licensed under the Apache License, Version 2.0 (the "License"); 17 | * you may not use this file except in compliance with the License. 18 | * You may obtain a copy of the License at 19 | * 20 | * http://www.apache.org/licenses/LICENSE-2.0 21 | * 22 | * Unless required by applicable law or agreed to in writing, software 23 | * distributed under the License is distributed on an "AS IS" BASIS, 24 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | * See the License for the specific language governing permissions and 26 | * limitations under the License. 27 | */ 28 | package com.clsaa.edu.rocketmq.ordermessage; 29 | 30 | import java.text.SimpleDateFormat; 31 | import java.util.Date; 32 | import java.util.List; 33 | import java.util.logging.SimpleFormatter; 34 | 35 | import com.alibaba.rocketmq.client.exception.MQBrokerException; 36 | import com.alibaba.rocketmq.client.exception.MQClientException; 37 | import com.alibaba.rocketmq.client.producer.DefaultMQProducer; 38 | import com.alibaba.rocketmq.client.producer.MQProducer; 39 | import com.alibaba.rocketmq.client.producer.MessageQueueSelector; 40 | import com.alibaba.rocketmq.client.producer.SendResult; 41 | import com.alibaba.rocketmq.common.message.Message; 42 | import com.alibaba.rocketmq.common.message.MessageQueue; 43 | import com.alibaba.rocketmq.remoting.exception.RemotingException; 44 | 45 | 46 | /** 47 | * Producer,发送顺序消息 48 | * 顺序消费的注意点: 49 | * 1. producer保证发送消息有序,并且发送到同一个队列 50 | * 2. consumer确保消费同一个队列 51 | */ 52 | public class Producer { 53 | public static void main(String[] args) { 54 | try { 55 | DefaultMQProducer producer = new DefaultMQProducer("order_producer"); 56 | 57 | producer.setNamesrvAddr("123.206.175.47:9876;182.254.210.72:9876"); 58 | 59 | producer.start(); 60 | 61 | Date date = new Date(); 62 | 63 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 64 | 65 | String dateStr = sdf.format(date); 66 | 67 | for (int i = 0; i < 5; i++) { 68 | String body = dateStr + " hello rocketMQ " + i; 69 | Message msg = new Message("TopicOrder2", "TagA", "KEY" + i, body.getBytes()); 70 | //发送数据:如果使用顺序消息,则必须自己实现MessageQueueSelector,保证消息进入同一个队列中去. 71 | SendResult sendResult = producer.send(msg, new MessageQueueSelector() { 72 | @Override 73 | public MessageQueue select(List mqs, Message msg, Object arg) { 74 | 75 | Integer id = (Integer) arg; 76 | System.out.println("id : " + id); 77 | return mqs.get(id); 78 | } 79 | }, 1);//队列下标 //orderID是选定的topic中队列的下标 80 | 81 | System.out.println(sendResult + " , body : " + body); 82 | } 83 | for (int i = 0; i < 5; i++) { 84 | String body = dateStr + " hello rocketMQ " + i; 85 | Message msg = new Message("TopicOrder2", "TagA", "KEY" + i, body.getBytes()); 86 | //发送数据:如果使用顺序消息,则必须自己实现MessageQueueSelector,保证消息进入同一个队列中去. 87 | SendResult sendResult = producer.send(msg, new MessageQueueSelector() { 88 | @Override 89 | public MessageQueue select(List mqs, Message msg, Object arg) { 90 | 91 | Integer id = (Integer) arg; 92 | System.out.println("id : " + id); 93 | return mqs.get(id); 94 | } 95 | }, 2);//队列下标 //orderID是选定的topic中队列的下标 96 | 97 | System.out.println(sendResult + " , body : " + body); 98 | } 99 | }catch (Exception e){ 100 | e.printStackTrace(); 101 | } 102 | } 103 | 104 | } 105 | 106 | 107 | ``` 108 | 109 | - consumer 110 | 111 | ``` 112 | /** 113 | * Copyright (C) 2010-2013 Alibaba Group Holding Limited 114 | * 115 | * Licensed under the Apache License, Version 2.0 (the "License"); 116 | * you may not use this file except in compliance with the License. 117 | * You may obtain a copy of the License at 118 | * 119 | * http://www.apache.org/licenses/LICENSE-2.0 120 | * 121 | * Unless required by applicable law or agreed to in writing, software 122 | * distributed under the License is distributed on an "AS IS" BASIS, 123 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 124 | * See the License for the specific language governing permissions and 125 | * limitations under the License. 126 | */ 127 | package com.clsaa.edu.rocketmq.ordermessage; 128 | 129 | import java.util.List; 130 | import java.util.Random; 131 | import java.util.concurrent.TimeUnit; 132 | 133 | import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer; 134 | import com.alibaba.rocketmq.client.consumer.listener.ConsumeOrderlyContext; 135 | import com.alibaba.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; 136 | import com.alibaba.rocketmq.client.consumer.listener.MessageListenerOrderly; 137 | import com.alibaba.rocketmq.client.exception.MQClientException; 138 | import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere; 139 | import com.alibaba.rocketmq.common.message.MessageExt; 140 | 141 | 142 | /** 143 | * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交) 144 | */ 145 | public class Consumer { 146 | 147 | public static void main(String[] args) throws MQClientException { 148 | DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_producer"); 149 | consumer.setNamesrvAddr("123.206.175.47:9876;182.254.210.72:9876"); 150 | /** 151 | * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
152 | * 如果非第一次启动,那么按照上次消费的位置继续消费 153 | */ 154 | consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); 155 | 156 | consumer.subscribe("TopicOrder2", "TagA"); 157 | 158 | consumer.registerMessageListener(new MessageListenerOrderly() { 159 | private Random random = new Random(); 160 | @Override 161 | public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { 162 | //设置自动提交 163 | context.setAutoCommit(true); 164 | 165 | for (MessageExt msg:msgs){ 166 | System.out.println(msg+ " , content : "+ new String(msg.getBody())); 167 | } 168 | try { 169 | //模拟业务处理 170 | TimeUnit.SECONDS.sleep(random.nextInt(5)); 171 | }catch (Exception e){ 172 | e.printStackTrace(); 173 | return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; 174 | } 175 | return ConsumeOrderlyStatus.SUCCESS; 176 | } 177 | }); 178 | 179 | consumer.start(); 180 | System.out.println("consume ! "); 181 | } 182 | } 183 | ``` 184 | 185 | 186 | - 输出结果 187 | 188 | ``` 189 | 190 | MessageExt [queueId=1, storeSize=168, queueOffset=20, sysFlag=0, bornTimestamp=1490337644787, bornHost=/112.28.174.121:25028, storeTimestamp=1490337647075, storeHost=/123.206.175.47:10911, msgId=7BCEAF2F00002A9F0000000000008083, commitLogOffset=32899, bodyCRC=505147113, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicOrder2, flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=21, KEYS=KEY0, WAIT=true, TAGS=TagA}, body=36]] , content : 2017-03-24 14:40:44 hello rocketMQ 0 191 | MessageExt [queueId=2, storeSize=168, queueOffset=0, sysFlag=0, bornTimestamp=1490337644908, bornHost=/112.28.174.121:25028, storeTimestamp=1490337647173, storeHost=/123.206.175.47:10911, msgId=7BCEAF2F00002A9F00000000000083CB, commitLogOffset=33739, bodyCRC=505147113, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicOrder2, flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=1, KEYS=KEY0, WAIT=true, TAGS=TagA}, body=36]] , content : 2017-03-24 14:40:44 hello rocketMQ 0 192 | MessageExt [queueId=2, storeSize=168, queueOffset=1, sysFlag=0, bornTimestamp=1490337644927, bornHost=/112.28.174.121:25028, storeTimestamp=1490337647192, storeHost=/123.206.175.47:10911, msgId=7BCEAF2F00002A9F0000000000008473, commitLogOffset=33907, bodyCRC=1763499647, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicOrder2, flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=2, KEYS=KEY1, WAIT=true, TAGS=TagA}, body=36]] , content : 2017-03-24 14:40:44 hello rocketMQ 1 193 | MessageExt [queueId=1, storeSize=168, queueOffset=21, sysFlag=0, bornTimestamp=1490337644831, bornHost=/112.28.174.121:25028, storeTimestamp=1490337647098, storeHost=/123.206.175.47:10911, msgId=7BCEAF2F00002A9F000000000000812B, commitLogOffset=33067, bodyCRC=1763499647, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicOrder2, flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=22, KEYS=KEY1, WAIT=true, TAGS=TagA}, body=36]] , content : 2017-03-24 14:40:44 hello rocketMQ 1 194 | MessageExt [queueId=2, storeSize=168, queueOffset=2, sysFlag=0, bornTimestamp=1490337644944, bornHost=/112.28.174.121:25028, storeTimestamp=1490337647210, storeHost=/123.206.175.47:10911, msgId=7BCEAF2F00002A9F000000000000851B, commitLogOffset=34075, bodyCRC=1880461253, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicOrder2, flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=3, KEYS=KEY2, WAIT=true, TAGS=TagA}, body=36]] , content : 2017-03-24 14:40:44 hello rocketMQ 2 195 | MessageExt [queueId=2, storeSize=168, queueOffset=3, sysFlag=0, bornTimestamp=1490337644964, bornHost=/112.28.174.121:25028, storeTimestamp=1490337647229, storeHost=/123.206.175.47:10911, msgId=7BCEAF2F00002A9F00000000000085C3, commitLogOffset=34243, bodyCRC=118669139, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicOrder2, flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=4, KEYS=KEY3, WAIT=true, TAGS=TagA}, body=36]] , content : 2017-03-24 14:40:44 hello rocketMQ 3 196 | MessageExt [queueId=1, storeSize=168, queueOffset=22, sysFlag=0, bornTimestamp=1490337644851, bornHost=/112.28.174.121:25028, storeTimestamp=1490337647117, storeHost=/123.206.175.47:10911, msgId=7BCEAF2F00002A9F00000000000081D3, commitLogOffset=33235, bodyCRC=1880461253, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicOrder2, flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=23, KEYS=KEY2, WAIT=true, TAGS=TagA}, body=36]] , content : 2017-03-24 14:40:44 hello rocketMQ 2 197 | MessageExt [queueId=2, storeSize=168, queueOffset=4, sysFlag=0, bornTimestamp=1490337644982, bornHost=/112.28.174.121:25028, storeTimestamp=1490337647247, storeHost=/123.206.175.47:10911, msgId=7BCEAF2F00002A9F000000000000866B, commitLogOffset=34411, bodyCRC=427174640, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicOrder2, flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=5, KEYS=KEY4, WAIT=true, TAGS=TagA}, body=36]] , content : 2017-03-24 14:40:44 hello rocketMQ 4 198 | MessageExt [queueId=1, storeSize=168, queueOffset=23, sysFlag=0, bornTimestamp=1490337644870, bornHost=/112.28.174.121:25028, storeTimestamp=1490337647135, storeHost=/123.206.175.47:10911, msgId=7BCEAF2F00002A9F000000000000827B, commitLogOffset=33403, bodyCRC=118669139, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicOrder2, flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=24, KEYS=KEY3, WAIT=true, TAGS=TagA}, body=36]] , content : 2017-03-24 14:40:44 hello rocketMQ 3 199 | MessageExt [queueId=1, storeSize=168, queueOffset=24, sysFlag=0, bornTimestamp=1490337644889, bornHost=/112.28.174.121:25028, storeTimestamp=1490337647154, storeHost=/123.206.175.47:10911, msgId=7BCEAF2F00002A9F0000000000008323, commitLogOffset=33571, bodyCRC=427174640, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicOrder2, flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=25, KEYS=KEY4, WAIT=true, TAGS=TagA}, body=36]] , content : 2017-03-24 14:40:44 hello rocketMQ 4 200 | ``` -------------------------------------------------------------------------------- /6-RocketMQ实战-事务消息.md: -------------------------------------------------------------------------------- 1 | # 6-RocketMQ实战-事务消息 2 | 3 | [TOC] 4 | 5 | ## 6.1 简介 6 | 7 | [http://www.jianshu.com/p/453c6e7ff81c](http://www.jianshu.com/p/453c6e7ff81c) 8 | 9 | - 事务模式:支持事务方式对消息进行提交处理,在rocket里事务非为两个阶段 10 | - 第一个阶段为把消息传递给MQ只不过消费端不可见,但是数据其实已经发送到broker上 11 | - 第二个阶段为本地消息回调处理,如果成功的话返回commitmessage,则在broker上的数据对消费端可见,失败则为rollbackmessage,消费端不可见. 12 | - 如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事物消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认. 13 | - 如果消费失败怎么办?阿里提供给我们的解决方法是:人工解决。大家可以考虑一下,按照事务的流程,因为某种原因Smith加款失败,那么需要回滚整个流程。如果消息系统要实现这个回滚流程的话,系统复杂度将大大提升,且很容易出现Bug,估计出现Bug的概率会比消费失败的概率大很多。这也是RocketMQ目前暂时没有解决这个问题的原因,在设计实现消息系统时,我们需要衡量是否值得花这么大的代价来解决这样一个出现概率非常小的问题,这也是大家在解决疑难问题时需要多多思考的地方。 14 | - 传统方式:两阶段提交协议(2PC)经常被用来处理分布式事务,一般氛围协调器C和若干事务执行者Si两种角色,哲理的事务执行者就是具体的数据库,协调器可以和事务执行在一台机器上. 15 | - 两阶段提交涉及多次节点间的网络通信,通信时间太长 16 | - 事务相对时间变长,对预定资源的占用时间变长 17 | ![image](http://roclsaarocketrrimgbed-10042610.cos.myqcloud.com/1338803383_5864.jpg) 18 | ![image](http://roclsaarocketrrimgbed-10042610.cos.myqcloud.com/1338803474_8977.jpg) 19 | 20 | ## 6.2 代码示例 21 | 22 | - producer 23 | 24 | ``` 25 | package com.clsaa.edu.rocketmq.transaction; 26 | 27 | import com.alibaba.rocketmq.client.producer.LocalTransactionExecuter; 28 | import com.alibaba.rocketmq.client.producer.LocalTransactionState; 29 | import com.alibaba.rocketmq.common.message.Message; 30 | 31 | 32 | /** 33 | * 执行本地事务 34 | */ 35 | public class TransactionExecuterImpl implements LocalTransactionExecuter { 36 | 37 | 38 | @Override 39 | public LocalTransactionState executeLocalTransactionBranch(final Message msg, final Object arg) { 40 | System.out.println(msg.toString()); 41 | System.out.println("msg = " + new String(msg.getBody())); 42 | System.out.println("arg = " + arg); 43 | String tag = msg.getTags(); 44 | System.out.println("这里执行入库操作....入库成功"); 45 | 46 | return LocalTransactionState.COMMIT_MESSAGE; 47 | } 48 | } 49 | 50 | ``` 51 | 52 | -TransactionExecuterImpl 53 | 54 | ``` 55 | 56 | package com.clsaa.edu.rocketmq.transaction; 57 | 58 | import com.alibaba.rocketmq.client.producer.LocalTransactionExecuter; 59 | import com.alibaba.rocketmq.client.producer.LocalTransactionState; 60 | import com.alibaba.rocketmq.common.message.Message; 61 | 62 | 63 | /** 64 | * 执行本地事务 65 | */ 66 | public class TransactionExecuterImpl implements LocalTransactionExecuter { 67 | 68 | 69 | @Override 70 | public LocalTransactionState executeLocalTransactionBranch(final Message msg, final Object arg) { 71 | System.out.println(msg.toString()); 72 | System.out.println("msg = " + new String(msg.getBody())); 73 | System.out.println("arg = " + arg); 74 | String tag = msg.getTags(); 75 | System.out.println("这里执行入库操作....入库成功"); 76 | 77 | return LocalTransactionState.COMMIT_MESSAGE; 78 | } 79 | } 80 | 81 | 82 | ``` 83 | 84 | ## 6.3参考文档 85 | 86 | - 分布式开放消息系统(RocketMQ)的原理与实践 - 简书[http://www.jianshu.com/p/453c6e7ff81c](http://www.jianshu.com/p/453c6e7ff81c) -------------------------------------------------------------------------------- /7-RocketMQ实战-消息重试机制.md: -------------------------------------------------------------------------------- 1 | # 7-RocketMQ实战-消息重试机制 2 | 3 | [TOC] 4 | 5 | ## 7.1 消息重试机制概述 6 | 7 | - RocketMQ提供了消息重试机制 8 | - 生产者:消息重投重试(保证数据的高可靠) 9 | - 消费者:消息处理异常(broker端到consumer端各种问题,比如网络原因闪断,消费端处理失败,ACK返回失败等) 10 | 11 | ## 7.2 consumer如何消息重试 12 | 13 | ### 7.2.1 需要重试的情况 14 | 15 | - Consumer 消费消息失败后,要提供一种重试机制,令消息再消费一次。 Consumer 消费消息失败通常可以讣为 16 | 有以下几种情冴 17 | 1. 由亍消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。返种错诨通常需要跳过返条消息,再消费其他消息,而返条失败的消息即使立刻重试消费,99%也丌成功,所以最好提供一种定时重试机制,即过 10s 秒后再重试。 18 | 2. 由亍依赖的下游应用服务丌可用,例如 db 连接丌可用,外系统网络丌可达等。遇到返种错诨,即使跳过当前失败的消息,消费其他消息同样也会报错。返种情冴建议应用 sleep 30s,再消费下一条消息,返样可以减轻 Broker 重试消息的压力。 19 | 20 | ### 7.2.2 触发与配置重试 21 | 22 | - 捕获异常后 return ConsumeConcurrentlyStatus.RECONSUME_LATER; //requeue 一会再消费,会启动broker的重试机制 23 | - consumer网络异常时,这时候不会返回东西,broker会选择同一个主题下同一个组下的另一个consumer把消息消费掉 24 | 25 | 26 | - 在服务器端(rocketmq-broker端)的属性配置文件中加入以下行(延时级别): 27 | ``` 28 | messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 29 | 描述了各级别与延时时间的对应映射关系。 30 | ``` 31 | 1. 这个配置项配置了从1级开始,各级延时的时间,可以修改这个指定级别的延时时间; 32 | 2. 时间单位支持:s、m、h、d,分别表示秒、分、时、天; 33 | 3. 默认值就是上面声明的,可手工调整; 34 | 4. 默认值已够用,不建议修改这个值。 35 | 36 | ### 7.2.3 处理重试次数过多的问题 37 | 38 | - 通过重试次数进行逻辑处理(补偿机制) 39 | - 重试之后会获取OrignMsgId 40 | 41 | ``` 42 | catch(exception e){ 43 | if(msg.getReconsumeTime()==3){ 44 | return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; 45 | } 46 | } 47 | ``` -------------------------------------------------------------------------------- /8-RocketMQ实战-幂等与去重.md: -------------------------------------------------------------------------------- 1 | # 8-RocketMQ实战-幂等与去重 2 | 3 | [TOC] 4 | 5 | ## 8.1 基本概念 6 | 7 | ### 幂等性 8 | 9 | - 一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。 10 | - 在MQ中broker不可避免的会发送重复的数据,到我们的consumer,consumer必须要保证处理的消息是唯一的. 11 | 12 | ## 8.2 如何去重 13 | 14 | - 去重原则 15 | 1. 幂等性 16 | 2. 业务去重 17 | - 去重策略 18 | 1. 去重表机制 19 | 2. 业务拼接去重(指纹码,唯一流水号) 20 | - 高并发去重 21 | 1. redis去重(需要有补偿策略) -------------------------------------------------------------------------------- /9-RockerMQ实战-消息模式.md: -------------------------------------------------------------------------------- 1 | # 9-RockerMQ实战-消息模式 2 | 3 | [TOC] 4 | 5 | - RockerMQ没有P2P模式 6 | 7 | ## 9.1 集群模式 8 | 9 | 设置消费端属性:MessageModel.CLUSTERING,这种方式就可以达到类似于ActiveMQ水平扩展负责负载均衡消息的实现.比较特殊的是这种消费方式可以支持先发送数据(也就是producer先发送数据到MQ),消费端订阅主题发生在生产端之后也可以收到数据,比较灵活. 10 | 11 | 12 | ## 9.2 广播模式 13 | 14 | 设置消费端对象属性:MessageModel.BROADCASTING,这种模式就是相当于生产端发送数据到MQ,多个消费端都可以获得数据. 15 | 16 | ## 9.3 重要概念 17 | 18 | - GroupName:无论是生产端还是消费端,都必须指定一个GroupName,这个组名称,是维护在应用系统级别上的,比如在一个生产端指定一个名称:ProducerGN,这个名称是需要由应用系统来保证唯一性的,一类Producer集合的名称,这类Producer通常发一类消息,且逻辑一致.同理消费端也是如此. 19 | 20 | - Topic:每个主题标识一个逻辑上存储的概念,而在其MQ上,会有着与之对应的多个Queue队列,这个(QUEUE)是物理存储的概念. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RocketMQ-Notes 2 | 3 | * RocketMQ基础概念 4 | * RocketMQ使用教程 5 | * SpringBoot整合RocketMQ 6 | -------------------------------------------------------------------------------- /RocketMQ运维指令整理.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clsaa/RocketMQ-Notes/acab0b3cf001c4fec608c39229439274c3b7493a/RocketMQ运维指令整理.docx -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [1-RocketMQ实战-核心概念详解](1-RocketMQ实战-核心概念详解.md) 4 | * [2-RocketMQ实战-双master模式集群搭建](2-RocketMQ实战-双master模式集群搭建.md) 5 | * [3-RocketMQ实战-控制台](3-RocketMQ实战-控制台.md) 6 | * [4-RocketMQ实战-HelloWorld](4-RocketMQ实战-HelloWorld.md) 7 | * [5-RocketMQ实战-顺序消息](5-RocketMQ实战-顺序消息.md) 8 | * [6-RocketMQ实战-事务消息](6-RocketMQ实战-事务消息.md) 9 | * [7-RocketMQ实战-消息重试机制](7-RocketMQ实战-消息重试机制.md) 10 | * [8-RocketMQ实战-幂等与去重](8-RocketMQ实战-幂等与去重.md) 11 | * [9-RockerMQ实战-消息模式](9-RockerMQ实战-消息模式.md) 12 | * [10-SpringBoot整合RocketMQ](10-SpringBoot整合RocketMQ.md) -------------------------------------------------------------------------------- /源码阅读/0.RocketMQ核心.md: -------------------------------------------------------------------------------- 1 | # 1.RocketMQ的概念模型 2 | 3 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-140251.png) 4 | 5 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-144316.png) 6 | 7 | 8 | 9 | ## 角色: 10 | 11 | ### Producer 12 | 13 | 生产者。发送消息的客户端角色。发送消息的时候需要指定Topic。 14 | 15 | Producer:消息生产者,也称为消息发布者,负责生产并发送消息。 16 | 17 | Producer 实例:Producer 的一个对象实例,不同的 Producer 实例可以运行在不同进程内或者不同机器上。Producer 实例线程安全,可在同一进程内多线程之间共享。 18 | 19 | ### Consumer 20 | 21 | 消费者。消费消息的客户端角色。通常是后台处理异步消费的系统。 RocketMQ中Consumer有两种实现:PushConsumer和PullConsumer。 22 | 23 | Consumer:消息消费者,也称为消息订阅者,负责接收并消费消息。 24 | 25 | Consumer 实例:Consumer 的一个对象实例,不同的 Consumer 实例可以运行在不同进程内或者不同机器上。一个 Consumer 实例内配置线程池消费消息。 26 | 27 | #### PushConsumer 28 | 29 | 推送模式(虽然RocketMQ使用的是长轮询)的消费者。消息的能及时被消费。使用非常简单,内部已处理如线程池消费、流控、负载均衡、异常处理等等的各种场景。 30 | 31 | #### PullConsumer 32 | 33 | 拉取模式的消费者。应用主动控制拉取的时机,怎么拉取,怎么消费等。主动权更高。但要自己处理各种场景。 34 | 35 | ## 概念术语 36 | 37 | ### Producer Group 38 | 39 | 标识发送同一类消息的Producer,通常发送逻辑一致。发送普通消息的时候,仅标识使用,并无特别用处。若事务消息,如果某条发送某条消息的producer-A宕机,使得事务消息一直处于PREPARED状态并超时,则broker会回查同一个group的其 他producer,确认这条消息应该commit还是rollback。但开源版本并不支持事务消息。 40 | 41 | ### Consumer Group 42 | 43 | 标识一类Consumer的集合名称,这类Consumer通常消费一类消息,且消费逻辑一致。同一个Consumer Group下的各个实例将共同消费topic的消息,起到负载均衡的作用。 44 | 45 | 消费进度以Consumer Group为粒度管理,不同Consumer Group之间消费进度彼此不受影响,即消息A被Consumer Group1消费过,也会再给Consumer Group2消费。 46 | 47 | 注: RocketMQ要求同一个Consumer Group的消费者必须要拥有相同的注册信息,即必须要听一样的topic(并且tag也一样)。 48 | 49 | ### Message 50 | 51 | Message:消息,消息队列中信息传递的载体。 52 | 53 | Message ID:消息的全局唯一标识,由消息队列 RocketMQ 系统自动生成,唯一标识某条消息。 54 | 55 | Message Key:消息的业务标识,由消息生产者(Producer)设置,唯一标识某个业务逻辑。 56 | 57 | ### Topic 58 | 59 | 标识一类消息的逻辑名字,消息的逻辑管理单位。无论消息生产还是消费,都需要指定Topic。 60 | 61 | ### Tag 62 | 63 | Tag:消息标签,二级消息类型,用来进一步区分某个 Topic 下的消息分类。 64 | 65 | RocketMQ支持给在发送的时候给topic打tag,同一个topic的消息虽然逻辑管理是一样的。但是消费topic1的时候,如果你订阅的时候指定的是tagA,那么tagB的消息将不会投递。 66 | 67 | ### Message Queue 68 | 69 | 简称Queue或Q。消息物理管理单位。一个Topic将有若干个Q。若Topic同时创建在不通的Broker,则不同的broker上都有若干Q,消息将物理地存储落在不同Broker结点上,具有水平扩展的能力。 70 | 71 | 无论生产者还是消费者,实际的生产和消费都是针对Q级别。例如Producer发送消息的时候,会预先选择(默认轮询)好该Topic下面的某一条Q地发送;Consumer消费的时候也会负载均衡地分配若干个Q,只拉取对应Q的消息。 72 | 73 | 每一条message queue均对应一个文件,这个文件存储了实际消息的索引信息。并且即使文件被删除,也能通过实际纯粹的消息文件(commit log)恢复回来。 74 | 75 | ### Offset 76 | 77 | RocketMQ中,有很多offset的概念。但通常我们只关心暴露到客户端的offset。一般我们不特指的话,就是指逻辑Message Queue下面的offset。 78 | 79 | 注: 逻辑offset的概念在RocketMQ中字面意思实际上和真正的意思有一定差别,这点在设计上显得有点混乱。祥见下面的解释。 80 | 81 | 可以认为一条逻辑的message queue是无限长的数组。一条消息进来下标就会涨1,而这个数组的下标就是offset。 82 | 83 | #### max offset 84 | 85 | 字面上可以理解为这是标识message queue中的max offset表示消息的最大offset。但是从源码上看,这个offset实际上是最新消息的offset+1,即:下一条消息的offset。 86 | 87 | #### min offset: 88 | 89 | 标识现存在的最小offset。而由于消息存储一段时间后,消费会被物理地从磁盘删除,message queue的min offset也就对应增长。这意味着比min offset要小的那些消息已经不在broker上了,无法被消费。 90 | 91 | #### consumer offset 92 | 93 | 字面上,可以理解为标记Consumer Group在一条逻辑Message Queue上,消息消费到哪里即消费进度。但从源码上看,这个数值是消费过的最新消费的消息offset+1,即实际上表示的是**下次拉取的offset位置**。 94 | 95 | 消费者拉取消息的时候需要指定offset,broker不主动推送消息, offset的消息返回给客户端。 96 | 97 | consumer刚启动的时候会获取持久化的consumer offset,用以决定从哪里开始消费,consumer以此发起第一次请求。 98 | 99 | 每次消息消费成功后,这个offset在会先更新到内存,而后定时持久化。在集群消费模式下,会同步持久化到broker,而在广播模式下,则会持久化到本地文件。 100 | 101 | ### 集群消费 102 | 103 | 消费者的一种消费模式。一个Consumer Group中的各个Consumer实例分摊去消费消息,即一条消息只会投递到一个Consumer Group下面的一个实例。 104 | 105 | 实际上,每个Consumer是平均分摊Message Queue的去做拉取消费。例如某个Topic有3条Q,其中一个Consumer Group 有 3 个实例(可能是 3 个进程,或者 3 台机器),那么每个实例只消费其中的1条Q。 106 | 107 | 而由Producer发送消息的时候是轮询所有的Q,所以消息会平均散落在不同的Q上,可以认为Q上的消息是平均的。那么实例也就平均地消费消息了。 108 | 109 | 这种模式下,消费进度的存储会持久化到Broker。 110 | 111 | ### 广播消费 112 | 113 | 消费者的一种消费模式。消息将对一个Consumer Group下的各个Consumer实例都投递一遍。即即使这些 Consumer 属于同一个Consumer Group,消息也会被Consumer Group 中的每个Consumer都消费一次。 114 | 115 | 实际上,是一个消费组下的每个消费者实例都获取到了topic下面的每个Message Queue去拉取消费。所以消息会投递到每个消费者实例。 116 | 117 | 这种模式下,消费进度会存储持久化到实例本地。 118 | 119 | ### 顺序消息 120 | 121 | 消费消息的顺序要同发送消息的顺序一致。由于Consumer消费消息的时候是针对Message Queue顺序拉取并开始消费,且一条Message Queue只会给一个消费者(集群模式下),所以能够保证同一个消费者实例对于Q上消息的消费是顺序地开始消费(不一定顺序消费完成,因为消费可能并行)。 122 | 123 | 在RocketMQ中,顺序消费主要指的是都是Queue级别的局部顺序。这一类消息为满足顺序性,必须Producer单线程顺序发送,且发送到同一个队列,这样Consumer就可以按照Producer发送的顺序去消费消息。 124 | 125 | 生产者发送的时候可以用MessageQueueSelector为某一批消息(通常是有相同的唯一标示id)选择同一个Queue,则这一批消息的消费将是顺序消息(并由同一个consumer完成消息)。或者Message Queue的数量只有1,但这样消费的实例只能有一个,多出来的实例都会空跑。 126 | 127 | ### 普通顺序消息 128 | 129 | 顺序消息的一种,正常情况下可以保证完全的顺序消息,但是一旦发生异常,Broker宕机或重启,由于队列总数发生发化,消费者会触发负载均衡,而默认地负载均衡算法采取哈希取模平均,这样负载均衡分配到定位的队列会发化,使得队列可能分配到别的实例上,则会短暂地出现消息顺序不一致。 130 | 131 | 如果业务能容忍在集群异常情况(如某个 Broker 宕机或者重启)下,消息短暂的乱序,使用普通顺序方式比较合适。 132 | 133 | ### 严格顺序消息 134 | 135 | 顺序消息的一种,无论正常异常情况都能保证顺序,但是牺牲了分布式 Failover 特性,即 Broker集群中只要有一台机器不可用,则整个集群都不可用,服务可用性大大降低。 136 | 137 | 如果服务器部署为同步双写模式,此缺陷可通过备机自动切换为主避免,不过仍然会存在几分钟的服务不可用。(依赖同步双写,主备自动切换,自动切换功能目前并未实现) 138 | 139 | 目前已知的应用只有数据库 binlog 同步强依赖严格顺序消息,其他应用绝大部分都可以容忍短暂乱序,推荐使用普通的顺序消息 140 | 141 | ### 定时消费 142 | 143 | 定时消息Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息。 144 | 145 | ### 延时消息 146 | 147 | Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。详情请见定时和延时消息。 148 | 149 | ### 事务消息 150 | 151 | 消息队列 RocketMQ 提供类似 X/Open XA 的分布事务功能,通过消息队列 RocketMQ 的事务消息能达到分布式事务的最终一致。 152 | 153 | ### 消息堆积 154 | 155 | 消息堆积:Producer 已经将消息发送到消息队列 RocketMQ 的服务端,但由于 Consumer 消费能力有限,未能在短时间内将所有消息正确消费掉,此时在消息队列 RocketMQ 的服务端保存着未被消费的消息,该状态即消息堆积。 156 | 157 | ### 消息过滤 158 | 159 | 消息过滤:消费者可以根据消息标签(Tag)对消息进行过滤,确保消费者最终只接收被过滤后的消息类型。消息过滤在消息队列 RocketMQ 的服务端完成。 160 | 161 | ### 消息轨迹 162 | 163 | 消息轨迹:在一条消息从生产者发出到订阅者消费处理过程中,由各个相关节点的时间、地点等数据汇聚而成的完整链路信息。通过消息轨迹,您能清晰定位消息从生产者发出,经由消息队列 RocketMQ 服务端,投递给消息消费者的完整链路,方便定位排查问题。 164 | 165 | ### 重置消费位点 166 | 167 | 重置消费位点:以时间轴为坐标,在消息持久化存储的时间范围内(默认 3 天),重新设置消息消费者对其订阅 Topic 的消费进度,设置完成后订阅者将接收设定时间点之后由消息生产者发送到消息队列 RocketMQ 服务端的消息。 168 | 169 | ### 私信队列 170 | 171 | 死信队列:死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。 RocketMQ 将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。 172 | 173 | ### Exactly-Once 投递语义 174 | 175 | Exactly-Once 投递语义:Exactly-Once 投递语义是指发送到消息系统的消息只能被消费端处理且仅处理一次,即使生产端重试消息发送导致某消息重复投递,该消息也在消费端也只被消费一次。详情请见Exactly-Once 投递语义。 176 | 177 | # 2.RocketMQ的部署模型 178 | 179 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-140552.png) 180 | 181 | 总共有四大部分:NameServer,Broker,Producer,Consumer。 182 | 183 | Broker 消息服务器在启动时向所有Name Server 注册,消息生产者(Producer)在发送消息之前先从Name Server 获取Broker 服务器地址列表,然后根据负载算法从列表中选择一台消息服务器进行消息发送。NameServer 与每台Broker 服务器保持长连接,并间隔30s 检测Broker 是否存活,如果检测到Broker 右机, 则从路由注册表中将其移除。但是路由变化不会马上通知消息生产者,为什么要这样设计呢?这是为了降低NameServer 实现的复杂性,在消息发送端提供容错机制来保证消息发送的高可用性 184 | 185 | NameServer 本身的高可用可通过部署多台Names 巳rver 服务器来实现,但彼此之间互不通信,也就是NameServer 服务器之间在某一时刻的数据并不会完全相同,但这对消息发送不会造成任何影响,这也是RocketMQ NameServer 设计的一个亮点, RocketMQNameServer 设计追求简单高效。 186 | 187 | ## 2.1.NameServer 188 | 189 | NameServer主要包括两个主要功能: 190 | 191 | 1. 管理brokers:broker服务器启动时会注册到NameServer上,并且两者之间保持心跳监测机制,以此来保证NameServer知道broker的存活状态; 192 | 2. 路由信息管理:每一台NameServer都存有全部的broker集群信息和生产者/消费者客户端的请求信息; 193 | 194 | 一些特点: 195 | 196 | **Namesrv用于存储Topic、Broker关系信息,功能简单,稳定性高。多个Namesrv之间相互没有通信**,单台Namesrv宕机不影响其他Namesrv与集群;即使整个Namesrv集群宕机,已经正常工作的Producer,Consumer,Broker仍然能正常工作,**但新起的Producer, Consumer,Broker就无法工作**。 197 | 198 | >Namesrv压力不会太大,平时主要开销是在维持心跳和提供Topic-Broker的关系数据。但有一点需要注意,Broker向Namesr发心跳时,会带上当前自己所负责的所有Topic信息,**如果Topic个数太多(万级别),会导致一次心跳中,就Topic的数据就几十M,网络情况差的话,网络传输失败,心跳失败,导致Namesrv误认为Broker心跳失败**。 199 | 200 | ## 2.2.Broker 201 | 202 | Broker的四大作用: 203 | 204 | 1. 请求分发:是client的入口,接收来自生产者消费者的请求 205 | 2. client管理:管理客户(产品/消费者)并维护消费者的主题订阅。 206 | 3. 数据存储:提供简单的api来查询磁盘上的临时数据 207 | 4. 高可用:主从节点间同步数据保证高可用 208 | 209 | 一些特点: 210 | 211 | 1. 负载均衡:Broker上存Topic信息,Topic由多个队列组成,队列会平均分散在多个Broker上,而Producer的发送机制保证消息尽量平均分布到所有队列中,最终效果就是所有消息都平均落在每个Broker上。 212 | 2. 动态伸缩能力(非顺序消息):Broker的伸缩性体现在两个维度:Topic, Broker。 213 | 1. Topic维度:假如一个Topic的消息量特别大,但集群水位压力还是很低,就可以扩大该Topic的队列数,Topic的队列数跟发送、消费速度成正比。 214 | 2. Broker维度:如果集群水位很高了,需要扩容,直接加机器部署Broker就可以。Broker起来后想Namesrv注册,Producer、Consumer通过Namesrv发现新Broker,立即跟该Broker直连,收发消息。 215 | 3. 高可用&高可靠 216 | 1. 高可用:集群部署时一般都为主备,备机实时从主机同步消息,如果其中一个主机宕机,备机提供消费服务,但不提供写服务。 217 | 2. 高可靠:所有发往broker的消息,有同步刷盘和异步刷盘机制;同步刷盘时,消息写入物理文件才会返回成功,异步刷盘时,只有机器宕机,才会产生消息丢失,broker挂掉可能会发生,但是机器宕机崩溃是很少发生的,除非突然断电 218 | 219 | ## 2.3.Producer 220 | 221 | Producer启动时,需要指定Namesrv的地址,从Namesrv集群中选一台建立长连接。如果该Namesrv宕机,会自动连其他Namesrv。直到有可用的Namesrv为止。生产者每30秒从Namesrv获取Topic跟Broker的映射关系,更新到本地内存中。再跟Topic涉及的所有Broker建立长连接,每隔30秒发一次心跳。 222 | 223 | 224 | ### 2.3.1.Producer三种发送方式 225 | 226 | * 同步:在广泛的场景中使用可靠的同步传输,如重要的通知信息、短信通知、短信营销系统等。 227 | * 异步:异步发送通常用于响应时间敏感的业务场景,发送出去即刻返回,利用回调做后续处理。 228 | * 一次性:一次性发送用于需要中等可靠性的情况,如日志收集,发送出去即完成,不用等待发送结果,回调等等。 229 | 230 | #### 生产者端的负载均衡 231 | 232 | 生产者发送时,会自动轮询当前所有可发送的broker,一条消息发送成功,下次换另外一个broker发送,以达到消息平均落到所有的broker上。 233 | 234 | 235 | ## 2.4.Consumer 236 | 237 | 238 | 消费客户端的连接方式和生产者类似。 239 | 240 | 消费者端的负载均衡 241 | 242 | 先讨论消费者的消费模式,消费者有两种模式消费:**集群消费,广播消费**。 243 | 244 | 集群:使用相同 Group ID 的订阅者属于同一个集群。同一个集群下的订阅者消费逻辑必须完全一致(包括 Tag 的使用),这些订阅者在逻辑上可以认为是一个消费节点。 245 | 246 | ### 2.4.1.广播消费 247 | 248 | 广播消费:每个消费者消费Topic下的所有队列。当使用广播消费模式时,消息队列 RocketMQ 会将每条消息推送给集群内所有注册过的客户端,保证消息至少被每台机器消费一次。 249 | 250 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-143311.png) 251 | 252 | 适用场景&注意事项 253 | 254 | * 广播消费模式下不支持顺序消息。 255 | * 每条消息都需要被相同逻辑的多台机器处理。 256 | * 消费进度在客户端维护,出现重复的概率稍大于集群模式。 257 | * 广播模式下,消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次,但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消费失败的情况。 258 | * 广播模式下,客户端第一次启动时默认从最新消息消费。客户端的消费进度是被持久化在客户端本地的隐藏文件中,因此不建议删除该隐藏文件,否则会丢失部分消息。 259 | * 广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。 260 | * 目前仅 Java 客户端支持广播模式。 261 | * 广播模式下服务端不维护消费进度,所以消息队列 RocketMQ 控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。 262 | 263 | ### 2.4.2.集群消费 264 | 265 | 集群消费:一个topic可以由同一个ID下所有消费者分担消费。当使用集群消费模式时,消息队列 RocketMQ 认为任意一条消息只需要被集群内的任意一个消费者处理即可。 266 | 267 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-143249.png) 268 | 269 | 适用场景&注意事项 270 | 271 | * 消费端集群化部署,每条消息只需要被处理一次。 272 | * 由于消费进度在服务端维护,可靠性更高。 273 | * 集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。 274 | * 集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上,因此处理消息时不应该做任何确定性假设。 275 | 276 | ### 2.4.3.使用集群模式模拟广播 277 | 278 | 如果业务需要使用广播模式,也可以创建多个 Group ID,用于订阅同一个 Topic 279 | 280 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-143842.png) 281 | 282 | 适用场景&注意事项 283 | 284 | * 每条消息都需要被多台机器处理,每台机器的逻辑可以相同也可以不一样。 285 | * 消费进度在服务端维护,可靠性高于广播模式。 286 | * 对于一个 Group ID 来说,可以部署一个消费端实例,也可以部署多个消费端实例。 当部署多个消费端实例时,实例之间又组成了集群模式(共同分担消费消息)。 假设 Group ID 1 部署了三个消费者实例 C1、C2、C3,那么这三个实例将共同分担服务器发送给 Group ID 1 的消息。 同时,实例之间订阅关系必须保持一致。 287 | 288 | 消费者端的负载均衡,就是集群消费模式下,同一个ID的所有消费者实例平均消费该Topic的所有队列。 289 | 290 | 消费者从用户角度来看有两种类型: 291 | 292 | * PullConsumer:主动从brokers处拉取消息。一旦拉取到批量的数据,用户应用的消费进程初始化。 293 | * PushConsumer:封装消息拉取、消费进程和内部其他工作维护,留下一个回调接口让用户实现,当消息到达时即可执行用户实现逻辑。 294 | 295 | 296 | # 3.消息存储 297 | 298 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-160709.png) 299 | 300 | RocketMQ 主要存储的文件包括Comitlog 文件、ConsumeQueue 文件、IndexFile 文件。RocketMQ 将所有主题的消息存储在同-个文件中,确保消息发送时顺序写文件,尽仅最大的能力确保消息发送的高性能与高吞吐量。但由于消息中间件一般是基于消息主题的订阅机制,这样便给按照消息主题检索消息带来了极大的不便。为了提高消息消费的效率, RocketMQ 引入了ConsumeQueue 消息队列文件,每个消息主题包含多个消息消费队列,每一个消息队列有一个消息文件。Ind 巳xFile 索引文件,其主要设计理念就是为了加速消息的检索性能,根据消息的属性快速从Commitlog 文件中检索消息。RocketMQ 是一款高性能的消息中间件,存储部分的设计是核心,存储的核心是IO 访问性能,本章也会重点剖析RocketMQ 是如何提高IO 访问性能的。进入RocketMQ 存储剖析之前,先看一下RocketMQ 数据流向,如图4-1 所示。 301 | 302 | * CommitLog:消息存储文件,所有消息主题的消息都存储在CommitLog 文件中。 303 | * ConsumeQueue:消息消费队列,消息到达CommitLog 文件后,将异步转发到消息消费队列,供消息消费者消费。 304 | * IndexFile:消息索引文件,主要存储消息Key 与Offset 的对应关系。 305 | * 事务状态服务: 存储每条消息的事务状态。 306 | * 定时消息服务:每一个延迟级别对应一个消息消费队列,存储延迟队列的消息拉取进度。 307 | 308 | * commitlog :消息存储目录。 309 | * config :运行期间一些配置信息,主要包括下列信息。 310 | * consumerFilter.json : 主题消息过滤信息。 311 | * consumerOffset.json : 集群消费模式消息消费进度。 312 | * delayOffset.json :延时消息队列拉取进度。 313 | * subscriptionGroup.json : 消息消费组配置信息。 314 | * topics.json: topic 配置属性。 315 | 316 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-19-171817.png) 317 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-161249.png) 318 | 319 | * consumequeue :消息消费队列存储目录。 320 | * index :消息索引文件存储目录。 321 | * abort :如果存在abort 文件说明Broker 非正常关闭,该文件默认启动时创建,正常退出之前删除。 322 | * checkpoint :文件检测点,存储commitlog 文件最后一次刷盘时间戳、consumequeue最后一次刷盘时间、index 索引文件最后一次刷盘时间戳。 323 | 324 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-161316.png) 325 | 326 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-161345.png) 327 | 328 | 329 | 330 | ## 3.1.PageCache 331 | 332 | MappedByteBuffer 333 | 334 | RocketMQ中的文件读写主要就是通过MappedByteBuffer进行操作,来进行文件映射。利用了nio中的FileChannel模型,可以直接将物理文件映射到缓冲区,提高读写速度。 335 | 336 | 了解了每个文件都在什么位置存放什么内容,那接下来就正式开始讨论这种存储方案为什么在性能带来的提升。 337 | 338 | 通常文件读写比较慢,如果对文件进行顺序读写,速度几乎是接近于内存的随机读写,为什么会这么快,原因就是Page Cache。 339 | 340 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-161555.png) 341 | 342 | 先来个直观的感受,整个OS有3.7G的物理内存,用掉了2.7G,应当还剩下1G空闲的内存,但OS给出的却是175M。当然这个数学题肯定不能这么算。 343 | 344 | OS发现系统的物理内存有大量剩余时,为了提高IO的性能,就会使用多余的内存当做文件缓存,也就是图上的buff / cache,广义我们说的Page Cache就是这些内存的子集。 345 | 346 | OS在读磁盘时会将当前区域的内容全部读到Cache中,以便下次读时能命中Cache,写磁盘时直接写到Cache中就写返回,由OS的pdflush以某些策略将Cache的数据Flush回磁盘。 347 | 348 | 但是系统上文件非常多,即使是多余的Page Cache也是非常宝贵的资源,OS不可能将Page Cache随机分配给任何文件,Linux底层就提供了mmap将一个程序指定的文件映射进虚拟内存(Virtual Memory),对文件的读写就变成了对内存的读写,能充分利用Page Cache。不过,文件IO仅仅用到了Page Cache还是不够的,如果对文件进行随机读写,会使虚拟内存产生很多缺页(Page Fault)中断。 349 | 350 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-161811.png) 351 | 352 | 每个用户空间的进程都有自己的虚拟内存,每个进程都认为自己所有的物理内存,但虚拟内存只是逻辑上的内存,要想访问内存的数据,还得通过内存管理单元(MMU)查找页表,将虚拟内存映射成物理内存。如果映射的文件非常大,程序访问局部映射不到物理内存的虚拟内存时,产生缺页中断,OS需要读写磁盘文件的真实数据再加载到内存。如同我们的应用程序没有Cache住某块数据,直接访问数据库要数据再把结果写到Cache一样,这个过程相对而言是非常慢的。 353 | 354 | 但是顺序IO时,读和写的区域都是被OS智能Cache过的热点区域,不会产生大量缺页中断,文件的IO几乎等同于内存的IO,性能当然就上去了。 355 | 356 | 说了这么多Page Cache的优点,也得稍微提一下它的缺点,内核把可用的内存分配给Page Cache后,free的内存相对就会变少,如果程序有新的内存分配需求或者缺页中断,恰好free的内存不够,内核还需要花费一点时间将热度低的Page Cache的内存回收掉,对性能非常苛刻的系统会产生毛刺。 357 | 358 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-164636.png) 359 | 360 | ## 3.2.刷盘 361 | 362 | 刷盘一般分成:同步刷盘和异步刷盘 363 | 364 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-161939.png) 365 | 366 | ### 3.2.1.同步刷盘 367 | 368 | 在消息真正落盘后,才返回成功给Producer,只要磁盘没有损坏,消息就不会丢。 369 | 370 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-162018.png) 371 | 372 | 一般只用于金融场景,这种方式不是本文讨论的重点,因为没有利用Page Cache的特点,RMQ采用GroupCommit的方式对同步刷盘进行了优化。 373 | 374 | ### 3.2.2.异步刷盘 375 | 376 | 读写文件充分利用了Page Cache,即写入Page Cache就返回成功给Producer,RMQ中有两种方式进行异步刷盘,整体原理是一样的。 377 | 378 | 刷盘由程序和OS共同控制 379 | 380 | 先谈谈OS,当程序顺序写文件时,首先写到Cache中,这部分被修改过,但却没有被刷进磁盘,产生了不一致,这些不一致的内存叫做脏页(Dirty Page)。 381 | 382 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-162116.png) 383 | 384 | 脏页设置太小,Flush磁盘的次数就会增加,性能会下降;脏页设置太大,性能会提高,但万一OS宕机,脏页来不及刷盘,消息就丢了。 385 | 386 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-162233.png) 387 | 388 | 一般不是高配玩家,用OS的默认值就好,如上图。 389 | 390 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-162254.png) 391 | 392 | RMQ想要性能高,那发送消息时,消息要写进Page Cache而不是直接写磁盘,接收消息时,消息要从Page Cache直接获取而不是缺页从磁盘读取。 393 | 394 | 好了,原理回顾完,从消息发送和消息接收来看RMQ中被mmap后的Commit Log和Consume Queue的IO情况。 395 | 396 | ### 3.3.commitLog(RMQ发送逻辑) 397 | 398 | 发送时,Producer不直接与Consume Queue打交道。上文提到过,RMQ所有的消息都会存放在Commit Log中,为了使消息存储不发生混乱,对Commit Log进行写之前就会上锁。 399 | 400 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-162412.png) 401 | 402 | 消息持久被锁串行化后,对Commit Log就是顺序写,也就是常说的Append操作。配合上Page Cache,RMQ在写Commit Log时效率会非常高。 403 | 404 | ### 3.3.Consume Queue(RMQ消费逻辑) 405 | 406 | CommitLog持久后,会将里面的数据Dispatch到对应的Consume Queue上。 407 | 408 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-164557.png) 409 | 410 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-162529.png) 411 | 412 | 消费时,Consumer不直接与Commit Log打交道,而是从Consume Queue中去拉取数据 413 | 414 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-162615.png) 415 | 416 | 拉取的顺序从旧到新,在文件表示每一个Consume Queue都是顺序读,充分利用了Page Cache。 417 | 418 | 上述的消息存储只是把消息主体存储到了物理文件中,但是并没有把消息处理到consumeQueue文件中,那么到底是哪里存入的? 419 | 420 | 任务处理一般都分为两种: 421 | 422 | 一种是同步,把消息主体存入到commitLog的同时把消息存入consumeQueue,rocketMQ的早期版本就是这样处理的。 423 | 424 | 另一种是异步处理,起一个线程,不停的轮询,将当前的consumeQueue中的offSet和commitLog中的offSet进行对比,将多出来的offSet进行解析,然后put到consumeQueue中的MapedFile中。 425 | 426 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-163348.png) 427 | 428 | ### 3.4.commitLog随机读(RMQ消费逻辑) 429 | 430 | 光拉取Consume Queue是没有数据的,里面只有一个对Commit Log的引用,所以再次拉取Commit Log。 431 | 432 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-162734.png) 433 | 434 | Commit Log会进行随机读 435 | 436 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-162755.png) 437 | 438 | 但整个RMQ只有一个Commit Log,虽然是随机读,但整体还是有序地读,只要那整块区域还在Page Cache的范围内,还是可以充分利用Page Cache。 439 | 440 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-162821.png) 441 | 442 | 在一台真实的MQ上查看网络和磁盘,即使消息端一直从MQ读取消息,也几乎看不到进程从磁盘拉数据,数据直接从Page Cache经由Socket发送给了Consumer。 443 | 444 | ## 3.5.Index 445 | 446 | ### 3.5.1.消息索引的作用 447 | 448 | 这里的消息索引主要是提供根据起始时间、topic和key来查询消息的接口。 449 | 450 | 首先根据给的topic、key以及起始时间查询到一个list,然后将offset拉到commitLog中查询,再反序列化成消息实体。 451 | 452 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-163505.png) 453 | 454 | 构建consumeQueue的同时会buildIndex构建索引 455 | 456 | * 根据查询的 key 的 hashcode%slotNum 得到具体的槽的位置(slotNum 是一个索引文件里面包含的最大槽的数目, 例如图中所示 slotNum=5000000) 。 457 | * 根据 slotValue(slot 位置对应的值)查找到索引项列表的最后一项(倒序排列,slotValue 总是挃吐最新的一个项目开源主页:https://github.com/alibaba/RocketMQ 458 | * 遍历索引项列表迒回查询时间范围内的结果集(默讣一次最大迒回的 32 条记彔) 459 | * Hash 冲突: 寻找 key 的 slot 位置时相当亍执行了两次散列函数,一次 key 的 hash,一次 key 的 hash 值取模, 因此返里存在两次冲突的情冴;第一种,key 的 hash 值丌同但模数相同,此时查询的时候会在比较一次 key 的 hash 值(每个索引项保存了 key 的 hash 值),过滤掉 hash 值丌相等的项。第二种,hash 值相等但 key 相等, 出亍性能的考虑冲突的检测放到客户端处理(key 的原始值是存储在消息文件中的,避免对数据文件的解析), 客户端比较一次消息体的 key 是否相同。 460 | * 存储: 为了节省空间索引项中存储的时间是时间差值(存储时间-开始时间,开始时间存储在索引文件头中), 整个索引文件是定长的,结构也是固定的。 461 | 462 | # 4.事务消息 463 | 464 | ## 4.1.业务逻辑 465 | 466 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-164304.png) 467 | 468 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-164425.png) 469 | 470 | 1. Producer发送Half(prepare)消息到broker; 471 | 2. half消息发送成功之后执行本地事务; 472 | 3. (由用户实现)本地事务执行如果成功则返回commit,如果执行失败则返回roll_back。 473 | 4. Producer发送确认消息到broker(也就是将步骤3执行的结果发送给broker),这里可能broker未收到确认消息,下面分两种情况分析: 474 | 475 | 1. 如果broker收到了确认消息: 476 | 1. 如果收到的结果是commit,则broker视为整个事务过程执行成功,将消息下发给Conusmer端消费; 477 | 2. 如果收到的结果是rollback,则broker视为本地事务执行失败,broker删除Half消息,不下发给consumer。 478 | 2. 如果broker未收到了确认消息: 479 | 1. broker定时回查本地事务的执行结果; 480 | 2. (由用户实现)如果本地事务已经执行则返回commit;如果未执行,则返回rollback; 481 | 3. Producer端回查的结果发送给broker; 482 | 4. broker接收到的如果是commit,则broker视为整个事务过程执行成功,将消息下发给Conusmer端消费;如果是rollback,则broker视为本地事务执行失败,broker删除Half消息,不下发给consumer。如果broker未接收到回查的结果(或者查到的是unknow),则broker会定时进行重复回查,以确保查到最终的事务结果。 483 | 484 | ## 4.2.实现细节 485 | 486 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-164809.png) 487 | 488 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-164831.png) 489 | 490 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-165004.png) 491 | 492 | ## 4.3.使用细节 493 | 494 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-165020.png) 495 | 496 | ## 4.4.多分支事务 497 | 498 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-165104.png) 499 | 500 | # 5.顺序消息 501 | 502 | ## 5.1.引入 503 | 504 | 世界上解决一个计算机问题最简单的方法:“恰好”不需要解决它!——沈询 505 | 506 | >有些问题,看起来很重要,但实际上我们可以通过合理的设计或者将问题分解来规避。如果硬要把时间花在解决问题本身,实际上不仅效率低下,而且也是一种浪费。从这个角度来看消息的顺序问题,我们可以得出两个结论: 507 | 508 | * 不关注乱序的应用实际大量存在 509 | * 队列无序并不意味着消息无序 510 | 511 | 所以从业务层面来保证消息的顺序而不仅仅是依赖于消息系统,是不是我们应该寻求的一种更合理的方式? 512 | 513 | RocketMQ通过轮询所有队列的方式来确定消息被发送到哪一个队列(负载均衡策略)。比如下面的示例中,订单号相同的消息会被先后发送到同一个队列中: 514 | 515 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-170622.png) 516 | 517 | 在获取到路由信息以后,会根据MessageQueueSelector实现的算法来选择一个队列,同一个OrderId获取到的肯定是同一个队列。 518 | 519 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-170641.png) 520 | 521 | RocketMQ可以严格的保证消息有序。但这个顺序,不是全局顺序,只是分区(queue)顺序。要全局顺序只能一个分区。 522 | 523 | 之所以出现你这个场景看起来不是顺序的,是因为发送消息的时候,消息发送默认是会采用轮询的方式发送到不通的queue(分区)。如图: 524 | 525 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-171324.png) 526 | 527 | 而消费端消费的时候,是会分配到多个queue的,多个queue是同时拉取提交消费。如图: 528 | 529 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-171340.png) 530 | 531 | 但是同一条queue里面,RocketMQ的确是能保证FIFO的。那么要做到顺序消息,应该怎么实现呢——把消息确保投递到同一条queue。 532 | 533 | ## 5.2.使用细节 534 | 535 | 下面用订单进行示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。 536 | 537 | ```java 538 | 539 | /** 540 | * Producer,发送顺序消息 541 | */ 542 | public class Producer { 543 | 544 | public static void main(String[] args) throws IOException { 545 | try { 546 | DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); 547 | 548 | producer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876"); 549 | 550 | producer.start(); 551 | 552 | String[] tags = new String[] { "TagA", "TagC", "TagD" }; 553 | 554 | // 订单列表 555 | List orderList = new Producer().buildOrders(); 556 | 557 | Date date = new Date(); 558 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 559 | String dateStr = sdf.format(date); 560 | for (int i = 0; i < 10; i++) { 561 | // 加个时间后缀 562 | String body = dateStr + " Hello RocketMQ " + orderList.get(i); 563 | Message msg = new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i, body.getBytes()); 564 | 565 | SendResult sendResult = producer.send(msg, new MessageQueueSelector() { 566 | @Override 567 | public MessageQueue select(List mqs, Message msg, Object arg) { 568 | Long id = (Long) arg; 569 | long index = id % mqs.size(); 570 | return mqs.get((int)index); 571 | } 572 | }, orderList.get(i).getOrderId());//订单id 573 | 574 | System.out.println(sendResult + ", body:" + body); 575 | } 576 | 577 | producer.shutdown(); 578 | 579 | } catch (MQClientException e) { 580 | e.printStackTrace(); 581 | } catch (RemotingException e) { 582 | e.printStackTrace(); 583 | } catch (MQBrokerException e) { 584 | e.printStackTrace(); 585 | } catch (InterruptedException e) { 586 | e.printStackTrace(); 587 | } 588 | System.in.read(); 589 | } 590 | 591 | /** 592 | * 生成模拟订单数据 593 | */ 594 | private List buildOrders() { 595 | List orderList = new ArrayList(); 596 | 597 | OrderDemo orderDemo = new OrderDemo(); 598 | orderDemo.setOrderId(15103111039L); 599 | orderDemo.setDesc("创建"); 600 | orderList.add(orderDemo); 601 | 602 | orderDemo = new OrderDemo(); 603 | orderDemo.setOrderId(15103111065L); 604 | orderDemo.setDesc("创建"); 605 | orderList.add(orderDemo); 606 | 607 | orderDemo = new OrderDemo(); 608 | orderDemo.setOrderId(15103111039L); 609 | orderDemo.setDesc("付款"); 610 | orderList.add(orderDemo); 611 | 612 | orderDemo = new OrderDemo(); 613 | orderDemo.setOrderId(15103117235L); 614 | orderDemo.setDesc("创建"); 615 | orderList.add(orderDemo); 616 | 617 | orderDemo = new OrderDemo(); 618 | orderDemo.setOrderId(15103111065L); 619 | orderDemo.setDesc("付款"); 620 | orderList.add(orderDemo); 621 | 622 | orderDemo = new OrderDemo(); 623 | orderDemo.setOrderId(15103117235L); 624 | orderDemo.setDesc("付款"); 625 | orderList.add(orderDemo); 626 | 627 | orderDemo = new OrderDemo(); 628 | orderDemo.setOrderId(15103111065L); 629 | orderDemo.setDesc("完成"); 630 | orderList.add(orderDemo); 631 | 632 | orderDemo = new OrderDemo(); 633 | orderDemo.setOrderId(15103111039L); 634 | orderDemo.setDesc("推送"); 635 | orderList.add(orderDemo); 636 | 637 | orderDemo = new OrderDemo(); 638 | orderDemo.setOrderId(15103117235L); 639 | orderDemo.setDesc("完成"); 640 | orderList.add(orderDemo); 641 | 642 | orderDemo = new OrderDemo(); 643 | orderDemo.setOrderId(15103111039L); 644 | orderDemo.setDesc("完成"); 645 | orderList.add(orderDemo); 646 | 647 | return orderList; 648 | 649 | ``` 650 | 651 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-171454.png) 652 | 653 | 从图中红色框可以看出,orderId等于15103111039的订单被顺序放入queueId等于7的队列。queueOffset同时在顺序增长。 654 | 655 | 发送时有序,接收(消费)时也要有序,才能保证顺序消费。如下这段代码演示了普通消费(非有序消费)的实现方式。 656 | 657 | ```java 658 | /** 659 | * 普通消息消费 660 | */ 661 | public class Consumer { 662 | 663 | public static void main(String[] args) throws MQClientException { 664 | DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3"); 665 | consumer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876"); 666 | /** 667 | * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
668 | * 如果非第一次启动,那么按照上次消费的位置继续消费 669 | */ 670 | consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); 671 | 672 | consumer.subscribe("TopicTestjjj", "TagA || TagC || TagD"); 673 | 674 | consumer.registerMessageListener(new MessageListenerConcurrently() { 675 | 676 | Random random = new Random(); 677 | 678 | @Override 679 | public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { 680 | System.out.print(Thread.currentThread().getName() + " Receive New Messages: " ); 681 | for (MessageExt msg: msgs) { 682 | System.out.println(msg + ", content:" + new String(msg.getBody())); 683 | } 684 | try { 685 | //模拟业务逻辑处理中... 686 | TimeUnit.SECONDS.sleep(random.nextInt(10)); 687 | } catch (Exception e) { 688 | e.printStackTrace(); 689 | } 690 | return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; 691 | } 692 | }); 693 | 694 | consumer.start(); 695 | 696 | System.out.println("Consumer Started."); 697 | } 698 | } 699 | ``` 700 | 701 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-171523.png) 702 | 703 | 可见,订单号为15103111039的订单被消费时顺序完成乱了。所以用MessageListenerConcurrently这种消费者是无法做到顺序消费的,采用下面这种方式就做到了顺序消费 704 | 705 | ```java 706 | /** 707 | * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交) 708 | */ 709 | public class ConsumerInOrder { 710 | 711 | public static void main(String[] args) throws MQClientException { 712 | DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3"); 713 | consumer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876"); 714 | /** 715 | * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
716 | * 如果非第一次启动,那么按照上次消费的位置继续消费 717 | */ 718 | consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); 719 | 720 | consumer.subscribe("TopicTestjjj", "TagA || TagC || TagD"); 721 | 722 | consumer.registerMessageListener(new MessageListenerOrderly() { 723 | 724 | Random random = new Random(); 725 | 726 | @Override 727 | public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { 728 | context.setAutoCommit(true); 729 | System.out.print(Thread.currentThread().getName() + " Receive New Messages: " ); 730 | for (MessageExt msg: msgs) { 731 | System.out.println(msg + ", content:" + new String(msg.getBody())); 732 | } 733 | try { 734 | //模拟业务逻辑处理中... 735 | TimeUnit.SECONDS.sleep(random.nextInt(10)); 736 | } catch (Exception e) { 737 | e.printStackTrace(); 738 | } 739 | return ConsumeOrderlyStatus.SUCCESS; 740 | } 741 | }); 742 | 743 | consumer.start(); 744 | 745 | System.out.println("Consumer Started."); 746 | } 747 | } 748 | ``` 749 | 750 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-171813.png) 751 | 752 | MessageListenerOrderly能够保证顺序消费,从图中我们也看到了期望的结果。图中的输出是只启动了一个消费者时的输出,看起来订单号还是混在一起,但是每组订单号之间是有序的。因为消息发送时被分配到了三个队列(参见前面生产者输出日志),那么这三个队列的消息被这唯一消费者消费。 753 | 754 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-21-171854.png) -------------------------------------------------------------------------------- /源码阅读/1.环境准备.md: -------------------------------------------------------------------------------- 1 | # 1.环境准备 2 | 3 | ## 1.1.依赖工具 4 | 5 | * JDK :1.8+ 6 | * Maven 7 | * IntelliJ IDEA 8 | 9 | ## 1.2.源码拉取 10 | 11 | 从官方仓库 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 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-17-142203.png) 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 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-17-151448.png) 315 | 316 | RocketMQ2 主2 从部署图如图2-3 所示。 317 | 318 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-17-151640.png) 319 | 320 | 对应运行时数据结构如图2-4 和图2-5 所示。 321 | 322 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-17-151808.png) 323 | 324 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-17-151824.png) 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 | ![image](https://clsaa-markdown-imgbed-1252032169.cos.ap-shanghai.myqcloud.com/very-java/2019-04-17-170537.png) 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 | ![](http://markdown-img-bed-common.oss-cn-hangzhou.aliyuncs.com/2019-08-03-121649.png) 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 | ![](http://markdown-img-bed-common.oss-cn-hangzhou.aliyuncs.com/2019-08-03-125305.png) -------------------------------------------------------------------------------- /源码阅读/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 | ![](http://markdown-img-bed-common.oss-cn-hangzhou.aliyuncs.com/2019-08-03-174453.png) 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 | ![](http://markdown-img-bed-common.oss-cn-hangzhou.aliyuncs.com/2019-08-03-174622.png) 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 | ![](http://markdown-img-bed-common.oss-cn-hangzhou.aliyuncs.com/2019-08-03-174835.jpg) 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 | ![](http://markdown-img-bed-common.oss-cn-hangzhou.aliyuncs.com/2019-08-03-175152.jpg) 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 | ![](http://markdown-img-bed-common.oss-cn-hangzhou.aliyuncs.com/2019-08-03-175600.jpg) 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 | ![](http://markdown-img-bed-common.oss-cn-hangzhou.aliyuncs.com/2019-08-03-175810.jpg) 222 | 223 | RocketMQ 在内存中还维护了一个索引文件列表,对于每一个索引文件,前一个文件的最大存储时间是下一个文件的最小存储时间,前一个文件的最大偏移量是下一个文件的最大偏移量。每一个索引文件都索引了在某个时间段内、某个偏移量段内的所有消息,当文件满了,就会用前一个文件的最大偏移量和最大存储时间作为起始值,创建下一个索引文件: 224 | 225 | ![](http://markdown-img-bed-common.oss-cn-hangzhou.aliyuncs.com/2019-08-03-175858.jpg) 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 | ![](http://markdown-img-bed-common.oss-cn-hangzhou.aliyuncs.com/2019-08-03-180216.jpg) 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 | ![](http://markdown-img-bed-common.oss-cn-hangzhou.aliyuncs.com/2019-08-03-180334.jpg) 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 --------------------------------------------------------------------------------