如果想要了解RocketMQ的历史,则需了解阿里巴巴中间件团队中的历史
2011年,Linkin(领英:全球知名的职场社交平台)推出Kafka消息引擎,阿里巴巴中间件团队在研究了Kafka的整体机制和架构设计之后,基于Kafka(Scala语言编写)的设计使用Java进行了完全重写并推出了MetaQ1.0版本,主要是用于解决顺序消息和海量堆积的问题,由开源社区killme2008维护。本书重点不在此版本,具体见:https://github.com/killme2008/Metamorphosis
2012年,阿里巴巴发现MetaQ原本基于Kafka的架构在阿里巴巴如此庞大的体系下很难进行水平扩展,于是对MetaQ进行了架构重组升级,开发出了MetaQ2.0,同年阿里把Meta2.0从阿里内部开源出来,取名RocketMQ,为了命名上的规范以及版本上的延续,对外称为RocketMQ3.0。因为RocketMQ3只是RocketMQ的一个过渡版本,本书重点也不在此。
2016年11月28日,阿里巴巴宣布将开源分布式消息中间件RocketMQ捐赠给Apache,成为Apache孵化项目。在孵化期间,RocketMQ完成编码规约、分支模型、持续交付、发布规约等方面的产品规范化,同时RocketMQ3也升级为RocketMQ4。现在RocketMQ主要维护的是4.x的版本,也是大家使用得最多的版本,所以本书重点将围绕此版本进行详细的讲解,项目地址:https://github.com/apache/rocketmq/
2015年,阿里基于RocketMQ开发了阿里云上的Aliware MQ,Aliware MQ(MessageQueue)是RocketMQ的商业版本,是阿里云商用的专业消息中间件,是企业级互联网架构的核心产品,基于高可用分布式集群技术,搭建了包括发布订阅、消息轨迹、资源统计、定时(延时)、监控报警等一套完整的消息云服务。因为Aliware
MQ是商业版本,本书也不对此产品进行讲述,产品地址:https://www.aliyun.com/product/rocketmq
2021年,伴随众多企业全面上云以及云原生的兴起,RocketMQ也在github上发布5.0版本。目前来说还只是一个预览版,不RocketMQ5的改动非常大,同时也明确了版本定位,RocketMQ5.0定义为云原生的消息、事件、流的超融合平台。本书也将会根据目前所发布的版本进行针对性的讲述。
RocketMQ可以从官网下载,也可以从Github上获取,推荐从官网中获取
官网:http://rocketmq.apache.org/dowloading/releases/
Github:https://github.com/apache/rocketmq/
本书中将使用4.8.0的版本,从官网上获取的页面如下。
环境要求如下:
Windows/Linux 64位系统
JDK1.8(64位)
源码安装需要安装Maven 3.2.x
下载链接:https://archive.apache.org/dist/rocketmq/4.8.0/rocketmq-all-4.8.0-bin-release.zip
解压运行版本(Binary),确保已经安装好了JDK1.8
解压后的目录如下:
变量名:ROCKETMQ_HOME
变量值:MQ解压路径\MQ文件夹名
在RocketMQ的架构中,都是需要先启动NameServer再启动Broker的。所以先启动NameServer。
使用cmd命令框执行进入至’MQ文件夹\bin’下,然后执行’start mqnamesrv.cmd’,启动NameServer。成功后会弹出提示框,此框勿关闭。
使用cmd命令框执行进入至’MQ文件夹\bin’下,然后执行’start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true’,启动Broker。成功后会弹出提示框,此框勿关闭。
打开’MQ文件夹\bin’下的runbroker.cmd,然后将’%CLASSPATH%'加上英文双引号。保存并重新执行start语句。
再次启动
RocketMQ默认的虚拟机内存较大,启动Broker如果因为内存不足失败,需要编辑如下两个配置文件,修改JVM内存大小。编辑’MQ文件夹\bin’下的runbroker.cmd和runserver.cmd修改默认JVM大小(Linux上对应同名sh文件)
runbroker.cmd --broker的配置
runserver. cmd --nameServer的配置
例如:配置以下参数将RocketMQ的启动JVM的堆空间内存控制在512m,新生代控制 在256m。元空间初始128m,最大320m。
rocketmq取的默认路径是user.home路径,也就是用户的根目录,一般存储放在跟路径下的/store目录。
源码中可以得到验证,如下图:
所以这里会有一个问题,RocketMQ很容易导致C盘空间不够,在使用过程中,创建一个主题默认就是要创建1G的文件,很可能会导致出问题。
所以在windows上容易导致C盘空间吃满。
解决方式有两种:
1、修改源码,比如:全局替换user.home参数为mq.store,然后重新打包
2、使用源码方式启动,源码启动时通过参数设置指定存储位置
运行前确保:已经有jdk1.8,Maven(打包需要安装Maven 3.2.x)
老版本地址下载:https://codeload.github.com/apache/rocketmq-externals/zip/master
新版本地址:https://github.com/apache/rocketmq-dashboard
解压后如图(以下使用的是老版本,新版本参考老版本即可)
后端管理界面是:rocketmq-console
下载完成之后,进入’\rocketmq-console\src\main\resources’文件夹,打开’application.properties’进行配置。
进入’\rocketmq-externals\rocketmq-console’文件夹,执行’mvn clean package -Dmaven.test.skip=true’,编译生成。
编译成功之后,cmd命令进入’target’文件夹,执行’java -jar rocketmq-console-ng-2.0.0.jar’,启动’rocketmq-console-ng-2.0.0.jar’。
浏览器中输入’127.0.0.1:8089’,成功后即可进行管理端查看。
运维页面
驾驶舱
集群
主题页面
展示所有的主题,可以通过搜索框进行过滤
筛选 普通/重试/死信 主题
添加/更新主题
clusterName 创建在哪几个cluster上
brokerName 创建在哪几个broker上
topicName 主题名
writeQueueNums 写队列数量
readQueueNums 读队列数量
perm //2是写 4是读 6是读写
状态 查询消息投递状态(投递到哪些broker/哪些queue/多少量等)
路由查看消息的路由(现在你发这个主题的消息会发往哪些broker,对应broker的queue信息)
CONSUMER管理(这个topic都被哪些group消费了,消费情况何如)
topic配置(查看变更当前的配置)
发送消息(向这个主题发送一个测试消息)
重置消费位点(分为在线和不在线两种情况,不过都需要检查重置是否成功)
删除主题 (会删除掉所有broker以及namesrv上的主题配置和路由信息)
消费者页面
展示所有的消费组,可以通过搜索框进行过滤
刷新页面/每隔五秒定时刷新页面
按照订阅组/数量/TPS/延迟 进行排序
添加/更新消费组
clusterName 创建在哪几个集群上
brokerName 创建在哪几个broker上
groupName 消费组名字
consumeEnable //是否可以消费 FALSE的话将无法进行消费
consumeBroadcastEnable //是否可以广播消费
retryQueueNums //重试队列的大小
brokerId //正常情况从哪消费
whichBrokerWhenConsumeSlowly//出问题了从哪消费
终端 在线的消费客户端查看,包括版本订阅信息和消费模式
消费详情
对应消费组的消费明细查看,这个消费组订阅的所有Topic的消费情况,每个queue对应的消费client查看(包括Retry消息)
配置 查看变更消费组的配置
删除 在指定的broker上删除消费组
生产者页面
通过Topic和Group查询在线的消息生产者客户端信息包含客户端主机 版本
消息查询页面
64bit OS、64bit JDK 1.8+、4g+ free disk for Broker server
在RocketMQ的架构中,都是需要先启动NameServer再启动Broker的。所以先启动NameServer。
进入至’MQ文件夹\bin’下,然后执行’nohup sh mqnamesrv &',启动NAMESERVER。
查看日志的命令:tail -f ~/logs/rocketmqlogs/namesrv.log
进入至’MQ文件夹\bin’下,启动BROKER。
修改配置文件增加外网地址(你启动加载哪个配置文件就修改哪个,比如修改broker.conf)
brokerIP1=192.168.56.101
启动命令如下:
nohup sh mqbroker -c …/conf/broker.conf -n 192.168.56.101:9876 autoCreateTopicEnable=true &
这样启动的服务器客户端可以自动创建主题。
查看日志的命令:tail -f ~/logs/rocketmqlogs/broker.log
RocketMQ需要开通的端口:
rocketMQ自身占用有9876
非vip通道端口:10911
vip通道端口:10909 (只针对producer 而且4.5以后已经默认不开启了)
VIP通道其实就是多监听一个端口用于接受处理消息,因为默认端口通道可能很多在用,为了防止某些很重要的业务堵塞,就再开一个端口处理。这对于老版本的RocketMQ 有消息接收队列的时候,作用可能大一点,对于目前的 RocketMQ的设计,作用没那么大了。所以,这个默认就不开启了,留着只是为了兼容老版本。
记得Linux上修改文件权限:命令如下:chmod -R 777 /home/linux
RocketMQ默认的虚拟机内存较大,启动Broker如果因为内存不足失败,需要编辑如下两个配置文件,修改JVM内存大小。(但是这个也仅仅是在测试环境中,RocketMQ在生产上最低要求至少8G内存<官方推荐>才能确保RocketMQ的效果)
编辑runbroker.sh和runserver.sh修改默认JVM大小(windows上对应cmd文件)
vi runbroker.sh --broker的配置
vi runserver.sh --nameServer的配置
JAVA_OPT=“${JAVA_OPT} -server -Xms1024m -Xmx1024m -Xmn512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m”
因为可视化插件是基于java打包的,所以启动过程和windows上是一样,修改端口号和打包可参考之前控制台插件
执行’nohup java -jar rocketmq-console-ng-1.0.1.jar &‘,启动’rocketmq-console-ng-1.0.1.jar’。
64位系统
JDK1.8(64位)
Maven 3.2.x
导入后执行Maven命令install
mvn install -Dmaven.test.skip=true
验证下没问题
如上图,中Value值是一个Rocket运行主目录(一般这个目录新建)
在Rocket运行主目录中创建conf、logs、store三个文件夹
然后从源码目录中distribution目录下的中将broker.conf、logback_broker.xml、logback_namesrv.xml复制到conf目录中
在broker模块找到broker模块,同时找到启动类BrokerStartup.java
需要修改配置文件broker.conf
配置如下:
#nameServer
namesrvAddr=127.0.0.1:9876
autoCreateTopicEnable = true
storePathRootDir = F:\\RocketMQ\\store
#commitLog存储路径
storePathCommitLog = F:\\RocketMQ\\store\\commitlog
#消费队列存储路径
storePathConsumeQueue =F:\\RocketMQ\\store\\consumequeue
#消息索引存储路径
storePathindex = F:\\RocketMQ\\store\\index
#checkpoint文件存储路径
storeCheckpoint = F:\\RocketMQ\\store\\checkpoint
#abort文件存储路径
abortFile = F:\\RocketMQ\\store\\abort
启动过程中任何的日志信息已经写入
消息中间件,英文MessageQueue,简称MQ。它没有标准定义,一般认为:消息中间件属于分布式系统中一个子系统,关注于数据的发送和接收,利用高效可靠的异步消息传递机制对分布式系统中的其余各个子系统进行集成。
**高效:**对于消息的处理处理速度快,RocketMQ可以达到单机10万+的并发。
**可靠:**一般消息中间件都会有消息持久化机制和其他的机制确保消息不丢失。
**异步:**指发送完一个请求,不需要等待返回,随时可以再发送下一个请求,既不需要等待。
一句话总结:消息中间件不生产消息,只是消息的搬运工。
系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验。
使用消息中间件,系统的耦合性就会提高了。比如物流系统发生故障,需要几分钟才能来修复,在这段时间内,物流系统要处理的数据被缓存到消息队列中,用户的下单操作正常完成。当物流系统恢复后,继续处理存放在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障。
应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提到系统的稳定性和用户体验。
互联网公司的大促场景(双十一、店庆活动、秒杀活动)都会使用到MQ。
通过消息队列可以让数据在多个系统更加之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可。
接口调用的弊端,无论是新增系统,还是移除系统,代码改造工作量都很大。
使用MQ做数据分发好处,无论是新增系统,还是移除系统,代码改造工作量较小。所以使用MQ做数据的分发,可以提高团队开发的效率。
NameServer是整个RocketMQ的"大脑",它是RocketMQ的服务注册中心,所以RocketMQ需要先启动NameServer再启动Rocket中的Broker。
Broker在启动时向所有NameServer注册(主要是服务器地址等),生产者在发送消息之前先从NameServer获取Broker服务器地址列表(消费者一样),然后根据负载均衡算法从列表中选择一台服务器进行消息发送。
RocketMQ的核心,用于暂存和传输消息。
生产者:也称为消息发布者,负责生产并发送消息至RocketMQ。
消费者:也称为消息订阅者,负责从RocketMQ接收并消费消息。
消息:生产或消费的数据,对于RocketMQ来说,消息就是字节数组。
标识RocketMQ中一类消息的逻辑名字,消息的逻辑管理单位。无论消息生产还是消费,都需要指定Topic。主题主要用于区分消息的种类:一个生产者可以发送消息给一个或者多个Topic,消息的消费者也可以订阅一个或者多个Topic消息。
简称Queue或Q。消息物理管理单位。一个Topic将有若干个Q。
无论生产者还是消费者,实际的生产和消费都是针对Q级别。例如Producer发送消息的时候,会预先选择(默认轮询)好该Topic下面的某一条Q发送;Consumer消费的时候也会负载均衡地分配若干个Q,只拉取对应Q的消息。
若一个Topic创建在不同的Broker,则不同的broker上都有若干Q,消息将物理地存储落在不同Broker结点上,具有水平扩展的能力。
**生产者:**标识发送同一类消息的Producer,通常发送逻辑一致。发送普通消息的时候,仅标识使用,并无特别用处。主要作用用于事务消息:
**消费者:**标识一类Consumer的集合名称,这类Consumer通常消费一类消息(也称为Consumer Group),且消费逻辑一致。同一个Consumer Group下的各个实例将共同消费topic的消息,起到负载均衡的作用。
RocketMQ支持给在发送的时候给消息打tag,同一个topic的消息虽然逻辑管理是一样的。但是消费同一个topic时,如果你消费订阅的时候指定的是tagA,那么tagB的消息将不会投递。
RocketMQ中,有很多offset的概念。一般我们只关心暴露到客户端的offset。不指定的话,就是指MessageQueue下面的offset。
Messagequeue是无限长的数组。一条消息进来下标就会涨1,而这个数组的下标就是offset,MessageQueue中的max offset表示消息的最大offset
Consumer offset可以理解为标记Consumer Group在一条逻辑MessageQueue上,消息消费到哪里即消费进度。但从源码上看,这个数值是消费过的最新消费的消息offset+1,即实际上表示的是下次拉取的offset位置。
本章节先会使用RocketMQ提供的原生客户端的API,当然除了原生客户端外,SpringBoot、SpringCloudStream也进行了集成,但本质上这些也是基于原生API的封装,所以只需掌握原生API,其他的也会水到渠成。
Java代码中使用普通消息的整体流程如下
org.apache.rocketmq
rocketmq-client
4.8.0
1.创建消息生产者producer,并指定生产者组名
2.指定Nameserver地址
3.启动producer
4.创建消息对象,指定Topic、Tag和消息体
5.发送消息
6.关闭生产者producer
1.创建消费者Consumer,指定消费者组名
2.指定Nameserver地址
3.订阅主题Topic和Tag
4.设置回调函数,处理消息
5.启动消费者consumer
同步发送是指消息发送方发出数据后,同步等待,直到收到接收方发回响应之后才发下一个请求。这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。
代码演示
发送结果分析
消息的全局唯一标识(RocketMQ的ID生成是使用机器IP和消息偏移量的组成),由消息队列MQ 系统自动生成,唯一标识某条消息。
发送的标识:成功,失败等
queueId是Topic的分区;Producer发送具体一条消息的时,对应选择的该Topic下的某一个Queue的标识ID。
MessageQueue是无限长的数组。一条消息进来下标就会涨1,而这个数组的下标就是queueOffset,queueOffset是从0开始递增。
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。消息发送方在发送了一条消息后,不等接收方发回响应,接着进行第二条消息发送。发送方通过回调接口的方式接收服务器响应,并对响应结果进行处理。
代码演示
发送结果分析跟发送同步消息相同。
这种方式主要用在不特别关心发送结果的场景,例如日志发送。单向(Oneway)发送特点为发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。
代码演示
消费者采用负载均衡方式消费消息,一个分组(Group)下的多个消费者共同消费队列消息,每个消费者处理的消息不同。一个Consumer
Group中的各个Consumer实例分摊去消费消息,即一条消息只会投递到一个ConsumerGroup下面的一个实例。例如某个Topic有3个队列,其中一个Consumer Group 有 3个实例,那么每个实例只消费其中的1个队列。集群消费模式是消费者默认的消费方式。
代码演示
广播消费模式中消息将对一个ConsumerGroup下的各个Consumer实例都投递一遍。即使这些 Consumer属于同一个Consumer
Group,消息也会被Consumer Group中的每个Consumer都消费一次。实际上,是一个消费组下的每个消费者实例都获取到了topic下面的每个MessageQueue去拉取消费。所以消息会投递到每个消费者实例。
代码演示
负载均衡模式:适用场景&注意事项
消费端集群化部署,每条消息只需要被处理一次。
由于消费进度在服务端维护,可靠性更高。
集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。
集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上,因此处理消息时不应该做任何确定性假设。
广播模式:适用场景&注意事项
每条消息都需要被相同逻辑的多台机器处理。
消费进度在客户端维护,出现重复的概率稍大于集群模式。
广播模式下,消息队列 RocketMQ保证每条消息至少被每台客户端消费一次,但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消费失败的情况。
广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。
广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。
目前仅 Java 客户端支持广播模式。
广播消费模式下不支持顺序消息。
广播消费模式下不支持重置消费位点。
广播模式下服务端不维护消费进度,所以消息队列 RocketMQ控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。
消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。区别如下:
生产消息时在默认的情况下消息发送会采取RoundRobin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。
全局有序比较简单,主要控制在于创建Topic指定只有一个队列,同步确保生产者与消费者都只有一个实例进行即可。
在电商业务场景中,一个订单的流程是:创建、付款、推送、完成。在加入RocketMQ后,一个订单会分别产生对于这个订单的创建、付款、推送、完成等消息,如果我们把所有消息全部送入到RocketMQ中的一个主题中,这里该如何实现针对一个订单的消息顺序性呢!如下图:
要完成分区有序性,在生产者环节使用自定义的消息队列选择策略,确保订单号尾数相同的消息会被先后发送到同一个队列中(案例中主题有3个队列,生产环境中可设定成10个满足全部尾数的需求),然后再消费端开启负载均衡模式,最终确保一个消费者拿到的消息对于一个订单来说是有序的。
发送日志
消费时,同一个OrderId获取到的肯定是同一个队列。从而确保一个订单中处理的顺序。
使用顺序消息:首先要保证消息是有序进入MQ的,消息放入MQ之前,对id等关键字进行取模,放入指定messageQueue,同时consume消费消息失败时,不能返回reconsume------later,这样会导致乱序,所以应该返回suspend_current_queue_a_moment,意思是先等一会,一会儿再处理这批消息,而不是放到重试队列里。
**延时消息:**Producer 将消息发送到消息队列 RocketMQ服务端,但并不期望这条消息立马投递(被消费者消费),而是延迟一定时间后才投递到Consumer 进行消费,该消息即延时消息。
消息生产和消费有时间窗口要求:比如在电商交易中超时未支付关闭订单的场景,在订单创建时向RocketMQ发送一条延时消息。这条消息将会在30分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。如支付未完成,则关闭订单。如已完成支付则忽略。
Apache RocketMQ目前只支持固定精度的定时消息,因为如果要支持任意的时间精度,在Broker层面,必须要做消息排序,如果再涉及到持久化,那么消息排序要不可避免的产生巨大性能开销。(RocketMQ的商业版本AliwareMQ提供了任意时刻的定时消息功能,Apache的RocketMQ并没有,阿里并没有开源)
ApacheRocketMQ发送延时消息是设置在每一个消息体上的,在创建消息时设定一个延时时间长度,消息将从当前发送时间点开始延迟固定时间之后才开始投递。
延迟消息的level,区分18个等级:level为1,表示延迟1秒后消费;level为2表示延迟5秒后消费;level为3表示延迟10秒后消费;以此类推;最大level为18表示延迟2个小时消费。具体标识如下:
level | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
延迟 | 1s | 5s | 10s | 30s | 1m | 2m | 3m | 4m | 5m |
level | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
延迟 | 6m | 7m | 8m | 9m | 10m | 20m | 30m | 1h | 2h |
是这生产消息跟普通的生产消息类似,只需要在消息上设置延迟队列的level即可。消费消息跟普通的消费消息一致。
查看消费者消息信息,打印消费延迟与生产时设定符合。
在高并发场景中,批量发送消息能显著提高传递消息发送时的性能(减少网络连接及IO的开销)。使用批量消息时的限制是这些批量消息应该有相同的topic,相同的waitStoreMsgOK(集群时会细讲),且不能是延时消息。
在发送批量消息时先构建一个消息对象集合,然后调用send(Collection msg)系列的方法即可。由于批量消息的4MB限制,所以一般情况下在集合中添加消息需要先计算当前集合中消息对象的大小是否超过限制,如果超过限制也可以使用分割消息的方式进行多次批量发送。
因为批量消息是一个Collection,所以送入消息可以是List,也可以使Set,这里为方便起见,使用List进行批量组装发送。
如果消息的总长度可能大于4MB时,这时候最好把消息进行分割,案例中以1M大小进行消息分割。
我们需要发送10万元素的数组,这个量很大,怎么快速发送完。使用批量发送,同时每一批控制在1M左右确保不超过消息大小限制。
在实际的开发应用中,对于一类消息尽可能使用一个Topic进行存储,但在消费时需要选择您想要的消息,这时可以使用RocketMQ的消息过滤功能,具体实现是利用消息的Tag和Key。
Key一般用于消息在业务层面的唯一标识。对发送的消息设置好Key,以后可以根据这个 Key来查找消息。比如消息异常,消息丢失,进行查找会很方便。RocketMQ会创建专门的索引文件,用来存储 Key与消息的映射,由于底层实现是 Hash索引,应尽量使 Key唯一,避免潜在的哈希冲突。
Tag可以理解为是二级分类。以淘宝交易平台为例,订单消息和支付消息属于不同业务类型的消息,分别创建OrderTopic和PayTopic,其中订单消息根据不同的商品品类以不同的 Tag再进行细分,如手机类、家电类、男装类、女装类、化妆品类,最后它们都被各个不同的系统所接收。通过合理的使用Topic 和 Tag,可以让业务结构清晰,更可以提高效率。
Key和Tag的主要差别是使用场景不同,Key主要用于通过命令行命令查询消息,而Tag用于在消息端的代码中,用来进行服务端消息过滤。
使用Key一般使用mqadmin管理工具,具体位置在RocketMQ/bin目录下。具体文档见:https://github.com/apache/rocketmq/blob/master/docs/cn/operation.md
使用Tag过滤的方式是在消息生产时传入感兴趣的Tag标签,然后在消费端就可以根据Tag来选择您想要的消息。具体的操作是在创建Message的时候添加,一个Message只能有一个Tag。
生产者发送60条消息,分别打上三种tag标签。
消费者消费时只选择TagA和TagB的消息。
Tag过滤的形式非常简单,||代表或、*代表所有,所以使用Tag过滤这对于复杂的场景可能不起作用。在这种情况下,可以使用SQL表达式筛选消息。
SQL特性可以通过发送消息时的属性来进行消息的过滤计算。具体的操作是使用SQL92标准的sql语句,前提是只有使用push模式的消费者才能用(消费的模式就是push)
**数值比较:**比如:>,>=,<,<=,BETWEEN,=;
**字符比较:**比如:=,<>,IN;
IS NULL 或者 IS NOT NULL;
**逻辑符号:**AND,OR,NOT;
常量支持类型为:
数值,比如:123,3.1415;
字符,比如:‘abc’,必须用单引号包裹起来;
NULL,特殊的常量
布尔值,TRUE 或 FALSE
Sql过滤需要Broker开启这项功能(如果消费时使用SQL过滤抛出异常错误,说明Sql92功能没有开启),需要修改Broker.conf配置文件。加入enablePropertyFilter=true然后重启Broker服务。
消息生产者,发送消息时加入消息属性,你能通过putUserProperty来设置消息的属性,以下案例中生产者发送10条消息,除了设置Tag之外,另外设置属性a的值。
用MessageSelector.bySql来使用sql筛选消息
消费结果:按照Tag和SQL过滤消费3条消息。
org.apache.rocketmq.example.details. ProducerDetails类中
producerGroup:生产者所属组
defaultTopicQueueNums:默认主题在每一个Broker队列数量
sendMsgTimeout:发送消息默认超时时间,默认3s
compressMsgBodyOverHowmuch:消息体超过该值则启用压缩,默认4k
retryTimesWhenSendFailed:同步方式发送消息重试次数,默认为2,总共执行3次
retryTimesWhenSendAsyncFailed:异步方法发送消息重试次数,默认为2
retryAnotherBrokerWhenNotStoreOK:消息重试时选择另外一个Broker时,是否不等待存储结果就返回,默认为false
maxMessageSize:允许发送的最大消息长度,默认为4M
org.apache.rocketmq.example.details. ProducerDetails类中
//启动
void start() throws MQClientException;
//关闭
void shutdown();
//查找该主题下所有消息队列
List
//发送单向消息
void sendOneway(final Message msg) throws MQClientException,RemotingException,InterruptedException;
//选择指定队列单向发送消息
void sendOneway(final Message msg, final MessageQueue mq) throws MQClientException,RemotingException, InterruptedException;
//同步发送消息
SendResult send(final Message msg) throws MQClientException,RemotingException, MQBrokerException,InterruptedException;
//同步超时发送消息
SendResult send(final Message msg, final long timeout) throws MQClientException,RemotingException, MQBrokerException, InterruptedException;
//选择指定队列同步发送消息
SendResult send(final Message msg, final MessageQueue mq) throws MQClientException,RemotingException, MQBrokerException, InterruptedException;
//异步发送消息
void send(final Message msg, final SendCallback sendCallback) throws MQClientException,RemotingException, InterruptedException;
//异步超时发送消息
void send(final Message msg, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, InterruptedException;
//选择指定队列异步发送消息
void send(final Message msg, final MessageQueue mq, final SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException;
org.apache.rocketmq.example.details. ComuserDetails类中
//消费者组
private String consumerGroup;
//消息消费模式
private MessageModel messageModel = MessageModel.CLUSTERING;
//指定消费开始偏移量(最大偏移量、最小偏移量、启动时间戳)开始消费
private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET;
ConsumeFromTimestamp模式下只会在订阅组(消费者群组)第一次启动的时候,过滤掉小于当前系统时间戳的消息,后续如果进程停掉或者崩溃,但是又生产了新消息。下次启动消费者时,会继续消费停掉期间新生产的消息。后续行为和ConsumeFromLastOffset类似
//消费者最小线程数量
private int consumeThreadMin = 20;
//消费者最大线程数量
private int consumeThreadMax = 20;
//推模式下任务间隔时间
private long pullInterval = 0;
//推模式下任务拉取的条数,默认32条
private int pullBatchSize = 32;
//消息重试次数,-1代表16次
private int maxReconsumeTimes = -1;
//消息消费超时时间
private long consumeTimeout = 15;
void subscribe(final String topic, final MessageSelector selector) :订阅消息,并指定队列选择器
void unsubscribe(final String topic):取消消息订阅
Set
void registerMessageListener(final MessageListenerConcurrently messageListener):注册并发事件监听器
void registerMessageListener(final MessageListenerOrderly messageListener):注册顺序消息事件监听器
业务实现消费回调的时候,当且仅当此回调函数返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS,RocketMQ才会认为这批消息(默认是1条)是消费完成的中途断电,抛出异常等都不会认为成功------即都会重新投递。
返回ConsumeConcurrentlyStatus.RECONSUME_LATER,RocketMQ就会认为这批消息消费失败了。
如果业务的回调没有处理好而抛出异常,会认为是消费失败ConsumeConcurrentlyStatus.RECONSUME_LATER处理。
为了保证消息是肯定被至少消费成功一次,RocketMQ会把这批消息重发回Broker(topic不是原topic而是这个消费组的RETRY topic),在延迟的某个时间点(默认是10秒,业务可设置)后,再次投递到这个ConsumerGroup。而如果一直这样重复消费都持续失败到一定次数(默认16次),就会投递到DLQ死信队列。应用可以监控死信队列来做人工干预。
另外如果使用顺序消费的回调MessageListenerOrderly时,由于顺序消费是要前者消费成功才能继续消费,所以没有RECONSUME_LATER的这个状态,只有SUSPEND_CURRENT_QUEUE_A_MOMENT来暂停队列的其余消费,直到原消息不断重试成功为止才能继续消费
业务场景:用户A转账100元给用户B,这个业务比较简单,具体的步骤:
1、用户A的账户先扣除100元
2、再把用户B的账户加100元
如果在同一个数据库中进行,事务可以保证这两步操作,要么同时成功,要么同时不成功。这样就保证了转账的数据一致性。
但是在微服务架构中因为各个服务都是独立的模块,都是远程调用,没法在同一个事务中,所以就会遇到分布式事务问题。
RocketMQ分布式事务方式:把扣款业务和加钱业务异步化,扣款成功后,发送"扣款成功消息"到RocketMQ,加钱业务订阅"扣款成功消息",再对用户B加钱(扣款消息中包含了源账户和目标账户ID,以及钱数)
对于扣款业务来说,需规定是先扣款还是先向MQ发消息
场景一:先扣款后向MQ发消息
先扣款再发送消息,万一发送消息失败了,那用户B就没法加钱
场景二:先向MQ发像消息,后扣款
扣款成功消息发送成功,但用户A扣款失败,可加钱业务订阅到了消息,用户B加了钱
问题所在,也就是没法保证扣款和发送消息,同时成功,或同时失败;导致数据不一致。
为了解决以上问题,RocketMq把消息分为两个阶段:半事务阶段和确认阶段
半事务阶段:
该阶段主要发一个消息到rocketmq,但该消息只储存在commitlog中,但consumeQueue中不可见,也就是消费端(订阅端)无法看到此消息
确认阶段(commit/rollback):
该阶段主要是把半事务消息保存到consumeQueue中,即让消费端可以看到此消息,也就是可以消费此消息。如果是rollback就不保存。
整个流程:
1、A在扣款之前,先发送半事务消息
2、发送预备消息成功后,执行本地扣款事务
3、扣款成功后,再发送确认消息
4、B消息端(加钱业务)可以看到确认消息,消费此消息,进行加钱
注意:上面的确认消息可以为commit消息,可以被订阅者消费;也可以是Rollback消息,即执行本地扣款事务失败后,提交rollback消息,即删除那个半事务消息,订阅者无法消费。这样就可以解决以下异常问题:
异常1:如果发送半事务消息失败,下面的流程不会走下去,这个是正常的。
异常2:如果发送半事务消息成功,但执行本地事务失败。这个也没有问题,因为此半事务消息不会被消费端订阅到,消费端不会执行业务。
异常3:如果发送半事务消息成功,执行本地事务成功,但发送确认消息失败;这个就有问题了,因为用户A扣款成功了,但加钱业务没有订阅到确认消息,无法加钱。这里出现了数据不一致。
RocketMq如何解决上面的问题,核心思路就是【事务回查】,也就是RocketMq会定时遍历commitlog中的半事务消息。
对于异常3,发送半事务消息成功,本地扣款事务成功,但发送确认消息失败;因为RocketMq会进行回查半事务消息,在回查后发现业务已经扣款成功了,就补发"发送commit确认消息";这样加钱业务就可以订阅此消息了。
这个思路其实把异常2也解决了,如果本地事务没有执行成功,RocketMQ回查业务,发现没有执行成功,就会发送RollBack确认消息,把消息进行删除。
同时还要注意的点是,RocketMQ不能保障消息的重复,所以在消费端一定要做幂等性处理。
除此之外,如果消费端发生消费失败,同时也需要做重试,如果重试多次,消息会进入死信队列,这个时候也需要进行特殊的处理。(一般就是把A已经处理完的业务进行回退)
如果本地事务执行了很多张表,那是不是我们要把那些表都要进行判断是否执行成功呢?这样是不是太麻烦了,而且和业务很耦合。
好的方案是设计一张Transaction表,将业务表和Transaction绑定在同一个本地事务中,如果扣款本地事务成功时,Transaction中应当已经记录该TransactionId的状态为「已完成」。当RocketMq事务回查时,只需要检查对应的TransactionId的状态是否是「已完成」就好,而不用关心具体的业务数据。
如果是银行业务,对数据要求性极高,一般A与B需要进行手动对账,手动补偿。
本案例简化整体流程,使用A系统向B系统转100块钱为例进行讲解。
案例中消息发送方是A系统,消费订阅方是B系统。
生产者案例代码:
代码解释如下:
事务回查案例代码:
消费者案例代码:
事务消息不支持延时消息和批量消息。
事务回查的间隔时间:BrokerConfig. transactionCheckInterval 通过Broker的配置文件设置好。
为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为15 次,但是用户可以通过 Broker 配置文件的transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写AbstractTransactionCheckListener 类来修改这个行为。
事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于transactionMsgTimeout 参数。
事务性消息可能不止一次被检查或消费。
事务性消息中用到了生产者群组,这种就是一种高可用机制,用来确保事务消息的可靠性。
提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过RocketMQ本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。
RocketMQ 中"Request-Reply"模式允许Producer发出消息后,以同步或异步的形式等Consumer消费并返回一个响应消息,达到类似RPC的调用过程。
RocketMQ从4.6.0版本开始支持这种模式。这种模式的流程如下图:
RocketMQ的这种调用方式跟dubbo之类的RPC调用非常类似,那为什么不使用dubbo?而要使用RocketMQ的这种RPC调用呢?原因如下:
基于RocketMQ来实现RPC可以快速搭建服务的消息总线,实现自己的RPC框架。
基于RocketMQ来实现RPC可以方便的收集调用的相关信息,能够实现调用链路追踪和分析。
基于RocketMQ来实现RPC既可以解耦两个系统之间的依赖,也可以实现跨网络区域实现系统间的同步调用,这里RocketMQ扮演的是一个类似于网关的角色。
在以上图中,可以看到,使用RPC模式还是三方:Producer、Broker、Consumer。
在Producer中进行消息的发送时,可以随便指定Topic,但是需要送入reply_to_client、correlation_id两个关键信息,reply_to_client记录着请求方的clientlD(用于Broker响应时确定client端)。而correlation_id是标识每次请求的,用于响应消息与请求的配对。而在进行发送消息时,也有两种模式,一种同步阻塞,另外一种异步非阻塞,这些跟之前普通消息的三种发送方式类似。
Broker端除了Producer发送时指定的Topic之外,还有一个Reply_Topic,这个以集群名_REPLY_TOPIC命名(不管RPC生产者主题有多少,这个在一个集群中只有一个),主要用于Consumer响应RPC消息的路由发现。
Consumer端除了消费监听之外,还需要加入一个消息的生产(用于RPC的响应消息),必须使用客户端提供的MessageUtil进行消息的包装,防止关键信息丢失从而导致Producer不能收到RPC消息响应。
生产者向RequestTopic主题发送RPC消息,使用同步阻塞方式。发送方法也不是send方法,而是request方法(该方法会封装reply_to_client、correlation_id等关键信息),同时方法也提供了Message的返回值。
消费者接受主题消息发送RPC响应。收到响应后需要再做一次生产,使用工具类MessageUtil封装消息后进行响应消息发送。
RPC案例消息:
生产者打印如下,对比普通消息多了reply_to_client、correlation_id两个关键信息,reply_to_client记录着请求方的clientlD(用于Broker响应时确定client端)。而correlation_id是标识每次请求的,用于响应消息与请求的配对。
消费打如下,消费者同时需要响应RPC,对应的主题是DefaultCluster_REPLY_TOPIC。
在PRC方式中,生产者也可以使用异步方式发起,代码如下:
领域模型(Domain Model)是对领域内的概念类或现实世界中对象的可视化表示。又称概念模型、领域对象模型、分析对象模型。它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。
Message是RocketMQ消息引擎中的主体。messageId是全局唯一的。MessageKey是业务系统(生产者)生成的,所以如果要结合业务,可以使用MessageKey作为业务系统的唯一索引。
另外Message中的equals方法和hashCode主要是为了完成消息只处理一次(Exactly-Once)。
Exactly-Once是指发送到消息系统的消息只能被消费端处理且仅处理一次,即使生产端重试消息发送导致某消息重复投递,该消息在消费端也只被消费一次。
Tags是在同一Topic中对消息进行分类
subTopics==Message
Queue,其实在内存逻辑中,subTopics是对Topics的一个拓展,尤其是在MQTT这种协议下,在Topic底下会有很多subTopics。
Queue是消息物理管理单位,比如在RocketMQ的控制台中,就可以看到每一个queue中的情况(比如消息的堆积情况、消息的TPS、QPS)
对于每一个Queue来说都有Offset,这个是消费位点。
业务场景中,如果有一堆发送者,一堆消费者,所以这里使用Group的概念进行管理。
Message与 Topic是多对一的关系,一个Topic可以有多个Message.
Topic到Queue是一对多的关系,这个也是方便横向拓展,也就是消费的时候,这里可以有很多很多的Queue.
一个Queue只有一个消费位点(Offset),所以Topic和Offset也是一对多的关系
Topic和Group也是多对多的关系。
从上面模型可以看出,要解决消费并发,就是要利用Queue,一个Topic可以分出更多的queue,每一个queue可以存放在不同的硬件上来提高并发。
前面讲过要确保消息的顺序,生产者、队列、消费者最好都是一对一的关系。但是这样设计,并发度就会成为消息系统的瓶颈(并发度不够)
RocketMQ不解决这个矛盾的问题。理由如下:
乱序的应用实际大量存在
队列无序并不意味着消息无序
另外还有消息重复,造成消息重复的根本原因是:网络不可达(网络波动)。所以如果消费者收到两条一样的消息,应该是怎么处理?
RocketMQ不保证消息不重复,如果你的业务要严格确保消息不重复,需要在自己的业务端进行去重。
消费端处理消息的业务逻辑保持幂等性
确保每一条消息都有唯一的编号且保证消息处理成功与去重表的日志同时出现
RocketMQ因为有高可靠性的要求(宕机不丢失数据),所以数据要进行持久化存储。所以RocketMQ
采用文件进行存储。
commitLog:消息存储目录
config:运行期间一些配置信息
consumerqueue:消息消费队列存储目录
index:消息索引文件存储目录
abort:如果存在改文件则Broker非正常关闭
checkpoint:文件检查点,存储CommitLog文件最后一次刷盘时间戳、consumerqueue最后一次刷盘时间,index索引文件最后一次刷盘时间戳。
RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。
**CommitLog:**存储消息的元数据
**ConsumerQueue:**存储消息在CommitLog的索引
**IndexFile:**为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程
CommitLog 以物理文件的方式存放,每台 Broker 上的 CommitLog 被本机器所有ConsumeQueue 共享,文件地址:$ {user.home} \store\$ { commitlog} \$ { fileName}。在CommitLog 中,一个消息的存储长度是不固定的,RocketMQ采取一些机制,尽量向CommitLog 中顺序写 ,但是随机读。commitlog文件默认大小为lG ,可通过在 broker 置文件中设置mappedFileSizeCommitLog属性来改变默认大小。
Commitlog文件存储的逻辑视图如下,每条消息的前面4个字节存储该条消息的总长度。但是一个消息的存储长度是不固定的。
每个 CommitLog 文件的大小为 1G,一般情况下第一个 CommitLog 的起始偏移量为 0,第二个 CommitLog 的起始偏移量为 1073741824 (1G = 1073741824byte)。
每台Rocket只会往一个commitlog文件中写,写完一个接着写下一个。
indexFile 和 ComsumerQueue 中都有消息对应的物理偏移量,通过物理偏移量就可以计算出该消息位于哪个 CommitLog 文件上。
ConsumeQueue 是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件, 文件地址在${$storeRoot} \consumequeue\$ {topicName} \$ { queueld} \${fileName}。
ConsumeQueue中存储的是消息条目,为了加速 ConsumeQueue 消息条目的检索速度与节省磁盘空间,每一个 Consumequeue条目不会存储消息的全量信息,消息条目如下:
ConsumeQueue 即为Commitlog 文件的索引文件, 其构建机制是 当消息到达 Commitlog 文件后由专门的线程产生消息转发任务,从而构建消息消费队列文件(ConsumeQueue)与下文提到的索引文件。
存储机制这样设计有以下几个好处:
1 ) CommitLog 顺序写 ,可以大大提高写入效率。
(实际上,磁盘有时候会比你想象的快很多,有时候也比你想象的慢很多,关键在如何使用,使用得当,磁盘的速度完全可以匹配上网络的数据传输速度。目前的高性能磁盘,顺序写速度可以达到600MB/s,超过了一般网卡的传输速度,这是磁盘比想象的快的地方但是磁盘随机写的速度只有大概lOOKB/s,和顺序写的性能相差 6000 倍!)
2 )虽然是随机读,但是利用操作系统的 pagecache机制,可以批量地从磁盘读取,作为 cache 存到内存中,加速后续的读取速度。
3 )为了保证完全的顺序写,需要 ConsumeQueue 这个中间结构,因为ConsumeQueue里只存偏移量信息,所以尺寸是有限的,在实际情况中,大部分的 ConsumeQueue能够被全部读入内存,所以这个中间结构的操作速度很快,可以认为是内存读取的速度。此外为了保证CommitLog和ConsumeQueue 的一致性, CommitLog 里存储了 Consume Queues、Message Key、 Tag 等所有信息,即使 ConsumeQueue 丢失,也可以通过commitLog 完全恢复出来。
RocketMQ还支持通过MessageID或者MessageKey来查询消息;使用ID查询时,因为ID就是用broker+offset生成的(这里msgId指的是服务端的),所以很容易就找到对应的commitLog文件来读取消息。但是对于用MessageKey来查询消息,RocketMQ则通过构建一个index来提高读取速度。
index 存的是索引文件,这个文件用来加快消息查询的速度。消息消费队列RocketMQ 专门为消息订阅构建的索引文件 ,提高根据主题与消息检索消息的速度,使用Hash索引机制,具体是Hash槽与Hash冲突的链表结构。(这里不做过多解释)
config 文件夹中
存储着Topic和Consumer等相关信息。主题和消费者群组相关的信息就存在在此。
topics.json : topic 配置属性
subscriptionGroup.json :消息消费组配置信息。
delayOffset.json :延时消息队列拉取进度。
consumerOffset.json :集群消费模式消息消进度。
consumerFilter.json :主题消息过滤信息。
abort :如果存在 abort 文件说明 Broker
非正常闭,该文件默认启动时创建,正常退出之前删除
checkpoint :文件检测点,存储 commitlog 文件最后一次刷盘时间戳、
consumequeue最后一次刷盘时间、 index 索引文件最后一次刷盘时间戳。
由于 RocketMQ 操作 CommitLog,ConsumeQueue文件是基于内存映射机制并在启动的时候会加载
commitlog,ConsumeQueue 目录下的所有文件,为了避免内存与磁盘的浪费,不可能将消息永久存储在消息服务器上,所以需要引入一种机制来删除己过期的文件。
删除过程分别执行清理消息存储文件( Commitlog )与消息消费 队列文件(ConsumeQueue 文件), 消息消费队列文件与消息存储文件( Commitlog)共用一套过期文件机制。
RocketMQ 清除过期文件的方法是:如果非当前写文件在一定时间间隔内没有再次被更新,则认为是过期文件,可以被删除,RocketMQ 不会关注这个文件上的消息是否全部被消费。默认每个文件的过期时间为42小时(不同版本的默认值不同,这里以4.4.0为例) ,通过在 Broker配置文件中设置 fileReservedTime 来改变过期时间,单位为小时。
触发文件清除操作的是一个定时任务,而且只有定时任务,文件过期删除定时任务的周期由该删除决定,默认每10s执行一次。
文件删除主要是由这个配置属性:fileReservedTime:文件保留时间。也就是从最后一次更新时间到现在,如果超过了该时间,则认为是过期文件,可以删除。
另外还有其他两个配置参数:
deletePhysicFilesInterval:删除物理文件的时间间隔(默认是100MS),在一次定时任务触发时,可能会有多个物理文件超过过期时间可被删除,因此删除一个文件后需要间隔deletePhysicFilesInterval这个时间再删除另外一个文件,由于删除文件是一个非常耗费IO的操作,会引起消息插入消费的延迟(相比于正常情况下),所以不建议直接删除所有过期文件。
destroyMapedFileIntervalForcibly:在删除文件时,如果该文件还被线程引用,此时会阻止此次删除操作,同时将该文件标记不可用并且纪录当前时间戳destroyMapedFileIntervalForcibly这个表示文件在第一次删除拒绝后,文件保存的最大时间,在此时间内一直会被拒绝删除,当超过这个时间时,会将引用每次减少1000,直到引用
小于等于 0为止,即可删除该文件.
1)指定删除文件的时间点, RocketMQ 通过 deleteWhen
设置一天的固定时间执行一次。删除过期文件操作, 默认为凌晨4点。
2)磁盘空间是否充足,如果磁盘空间不充足(DiskSpaceCleanForciblyRatio。磁盘空间强制删除文件水位。默认是85),会触发过期文件删除操作。
另外还有RocketMQ的磁盘配置参数:
1:物理使用率大于diskSpaceWarningLevelRatio(默认90%可通过参数设置),则会阻止新消息的插入。
2:物理磁盘使用率小于diskMaxUsedSpaceRatio(默认75%) 表示磁盘使用正常。
零拷贝(英语: Zero-copy)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
➢零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
➢零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上:下文切换而带来的开销
可以看出没有说不需要拷贝,只是说减少冗余[不必要]的拷贝。
下面这些组件、框架中均使用了零拷贝技术:Kafka、Netty、Rocketmq、Nginx、Apache。
比如:读取文件,再用socket发送出去,实际经过四次copy。
伪码实现如下:
buffer = File.read()
Socket.send(buffer)
1、第一次:将磁盘文件,读取到操作系统内核缓冲区;
2、第二次:将内核缓冲区的数据,copy到应用程序的buffer;
3、第三步:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);
4、第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。
分析上述的过程,虽然引入DMA来接管CPU的中断请求,但四次copy是存在"不必要的拷贝"的。实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。
显然,第二次和第三次数据copy其实在这种场景下没有什么帮助反而带来开销(DMA拷贝速度一般比CPU拷贝速度快一个数量级),这也正是零拷贝出现的背景和意义。
打个比喻:200M的数据,读取文件,再用socket发送出去,实际经过四次copy(2次cpu拷贝每次100ms
,2次DMS拷贝每次10ms)传统网络传输的话:合计耗时将有220ms
同时,read和send都属于系统调用,每次调用都牵涉到两次上下文切换:
总结下,传统的数据传送所消耗的成本:4次拷贝,4次上下文切换。
4次拷贝,其中两次是DMA copy,两次是CPU copy。
硬盘上文件的位置和应用程序缓冲区(application buffers)进行映射(建立一种一一对应关系),由于mmap()将文件直接映射到用户空间,所以实际文件读取时根据这个映射关系,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝,不再有文件内容从硬盘拷贝到内核空间的一个缓冲区。
mmap内存映射将会经历:3次拷贝: 1次cpu copy,2次DMA copy;
打个比喻:200M的数据,读取文件,再用socket发送出去,如果是使用MMAP实际经过三次copy(1次cpu拷贝每次100ms
,2次DMS拷贝每次10ms)合计只需要120ms
从数据拷贝的角度上来看,就比传统的网络传输,性能提升了近一倍。
以及4次上下文切换
mmap()是在
Windows操作系统上也有虚拟机内存,如下图:
如果按照传统的方式进行数据传送,那肯定性能上不去,作为MQ也是这样,尤其是RocketMQ,要满足一个高并发的消息中间件,一定要进行优化。所以RocketMQ使用的是MMAP。
RocketMQ一个映射文件大概是,commitlog 文件默认大小为lG。
这里需要注意的是,采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G
的文件至用户态的虚拟内存,这也是为何RocketMQ默认设置单个CommitLog日志数据文件为1G的原因了。
RocketMQ源码中,使用MappedFile这个类类进行MMAP的映射
Producer端发送消息最终写入的是CommitLog(消息存储的日志数据文件),Consumer端先从ConsumeQueue(消息逻辑队列)读取持久化消息的起始物理位置偏移量offset、大小size和消息Tag的HashCode值,随后再从CommitLog中进行读取待拉取消费消息的真正实体内容部分;
所有的Topic下的消息队列共用同一个CommitLog的日志数据文件,并通过建立类似索引文件—ConsumeQueue的方式来区分不同Topic下面的不同MessageQueue的消息,同时为消费消息起到一定的缓冲作用(异步服务线生成了ConsumeQueue队列的信息后,Consumer端才能进行消费)。这样,只要消息写入并刷盘至CommitLog文件后,消息就不会丢失,即使ConsumeQueue中的数据丢失,也可以通过CommitLog来恢复。
发送消息时,生产者端的消息确实是顺序写入CommitLog;订阅消息时,消费者端也是顺序读取ConsumeQueue,然而根据其中的起始物理位置偏移量offset读取消息真实内容却是随机读取CommitLog。所以在RocketMQ集群整体的吞吐量、并发量非常高的情况下,随机读取文件带来的性能开销影响还是比较大的,RocketMQ怎么优化的,源码解读部分进行讲解。
RocketMQ分布式集群是通过Master和Slave的配合达到高可用性的。
Master和Slave的区别:在Broker的配置文件中,参数 brokerId的值为0表明这个Broker是Master,大于0表明这个Broker是
Slave,同时brokerRole参数也会说明这个Broker是Master还是Slave。
Master角色的Broker支持读和写,Slave角色的Broker仅支持读,也就是 Producer只能和Master角色的Broker连接写入消息;Consumer可以连接Master角色的Broker,也可以连接Slave角色的Broker来读取消息。
也就是只有一个 master 节点,称不上是集群,一旦这个 master 节点宕机,那么整个服务就不可用。
多个 master 节点组成集群,单个 master 节点宕机或者重启对应用没有影响。
优点:所有模式中性能最高(一个Topic的可以分布在不同的master,进行横向拓展)在多主多从的架构体系下,无论使用客户端还是管理界面创建主题,一个主题都会创建多份队列在多主中(默认是4个的话,双主就会有8个队列,每台主4个队列,所以双主可以提高性能,一个Topic的分布在不同的master,方便进行横向拓展。
缺点:单个 master 节点宕机期间,未被消费的消息在节点恢复之前不可用,消息的实时性就受到影响。
而从节点(Slave)就是复制主节点的数据,对于生产者完全感知不到,对于消费者正常情况下也感知不到。(只有当Master不可用或者繁忙的时候,Consumer会被自动切换到从Slave读。)
在多 master 模式的基础上,每个 master 节点都有至少一个对应的slave。master节点可读可写,但是 slave只能读不能写,类似于 mysql的主备模式。
优点: 一般情况下都是master消费,在 master 宕机或超过负载时,消费者可以从 slave 读取消息,消息的实时性不会受影响,性能几乎 和多 master 一样。
缺点:使用异步复制的同步方式有可能会有消息丢失的问题。(Master宕机后,生产者发送的消息没有消费完,同时到Slave节点的数据也没有同步完)
**优点:**主从同步复制模式能保证数据不丢失。
**缺点:**发送单个消息响应时间会略长,性能相比异步复制低10%左右。
对数据要求较高的场景,主从同步复制方式,保存数据热备份,通过异步刷盘方式,保证rocketMQ高吞吐量。
在RocketMQ4.5版本之后推出了Dlegder模式,但是这种模式一直存在严重的BUG,同时性能有可能有问题,包括升级到了4.8的版本后也一样,所以目前不讲这种模式。(类似于Zookeeper的集群选举模式)
生产时首先将消息写入到MappedFile,内存映射文件,然后根据刷盘策略刷写到磁盘。
大致的步骤可以理解成使用MMAP中的MappedByteBuffer中实际用flip().
RocketMQ的刷盘是把消息存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式,同步刷盘和异步刷盘。
SYNC_FLUSH(同步刷盘):生产者发送的每一条消息都在保存到磁盘成功后才返回告诉生产者成功。这种方式不会存在消息丢失的问
题,但是有很大的磁盘IO开销,性能有一定影响。
ASYNC_FLUSH(异步刷盘):生产者发送的每一条消息并不是立即保存到磁盘,而是暂时缓存起来,然后就返回生产者成功。随后再异步的将缓存数据保存到磁盘,有两种情况:1是定期将缓存中更新的数据进行刷盘,2是当缓存中更新的数据条数达到某一设定值后进行刷盘。这种异步的方式会存在消息丢失(在还未来得及同步到磁盘的时候宕机),但是性能很好。默认是这种模式。
4.8.0版本中默认值下是异步刷盘,如下图:
集群环境下需要部署多个Broker,Broker分为两种角色:一种是master,即可以写也可以读,其brokerId=0,只能有一个;另外一种是slave,只允许读,其brokerId为非0。一个master与多个slave通过指定相同的brokerClusterName被归为一个broker set(broker集)。通常生产环境中,我们至少需要2个broker set。Slave是复制master的数据。一个Broker组有Master和Slave,消息需要从Master复制到Slave上,有同步和异步两种复制方式。
主从同步复制方式(Sync Broker):生产者发送的每一条消息都至少同步复制到一个slave后才返回告诉生产者成功,即"同步双写"
在同步复制方式下,如果Master出故障,Slave上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量。
主从异步复制方式(Async Broker):生产者发送的每一条消息只要写入master就返回告诉生产者成功。然后再"异步复制"到slave。
在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但是如果Master出了故障,有些数据因为没有被写
入Slave,有可能会丢失;
同步复制和异步复制是通过Broker配置文件里的brokerRole参数进行设置的,这个参数可以被设置成ASYNC_MASTER、
SYNC_MASTER、SLAVE三个值中的一个。
brokerId=0代表主
brokerId=1代表从(大于0都代表从)
brokerRole=SYNC_MASTER 同步复制(主从)
brokerRole=ASYNC_MASTER异步复制(主从)
flushDiskType=SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH 异步刷盘
106.55.246.66
94.191.83.120
106.55.246.66 ------MasterA
94.191.83.120 ------MasterB
106.53.195.121 ------SlaveA
106.55.248.74 ------SlaveB
注意,因为RocketMQ使用外网地址,所以配置文件(MQ文件夹/conf/2m-2s-sync/)需要修改(同时修改nameserver地址为集群地址):
注意如果机器内存不够,建议把启动时的堆内存改小,具体见《RocketMQ的安装.docx》中 — 3、RocketMQ在Linux下的安装/注意事项
106.55.246.66 ------主A
broker-a.properties 增加: brokerIP1=106.55.246.66
namesrvAddr=106.55.246.66:9876;94.191.83.120:9876
94.191.83.120 ------主B
broker-b.properties 增加: brokerIP1=94.191.83.120
namesrvAddr=106.55.246.66:9876;94.191.83.120:9876
106.53.195.121 ------从A
broker-a-s.properties 增加:brokerIP1=106.53.195.121
namesrvAddr=106.55.246.66:9876;94.191.83.120:9876
106.55.248.74 ------从B
broker-b-s.properties 增加:brokerIP1=106.55.248.74
namesrvAddr=106.55.246.66:9876;94.191.83.120:9876
不管是主还是从,如果属于一个集群,使用相同的brokerClusterName名称
1.启动NameServer集群,这里使用106.55.246.66和94.191.83.120两台作为集群即可。
1) 在机器A,启动第1台NameServer:102服务器进入至’MQ文件夹/bin’下:然后执行’nohup sh mqnamesrv &’
查看日志的命令:tail -f ~/logs/rocketmqlogs/namesrv.log
2) 在机器B,启动第2台NameServer: 103服务器进入至’MQ文件夹/bin’下:然后执行’nohup sh mqnamesrv &’
查看日志的命令:tail -f ~/logs/rocketmqlogs/namesrv.log
2.启动双主双从同步集群,顺序是先启动主,然后启动从。
3)启动主A:102服务器进入至’MQ文件夹/bin’下:执行以下命令(autoCreateTopicEnable=true
测试环境开启,生产环境建议关闭):
nohup sh mqbroker -c …/conf/2m-2s-sync/broker-a.properties autoCreateTopicEnable=true &
查看日志的命令:tail -f ~/logs/rocketmqlogs/broker.log
4)启动主B: 103服务器进入至’MQ文件夹\bin’下:执行以下命令:
nohup sh mqbroker -c …/conf/2m-2s-sync/broker-b.properties autoCreateTopicEnable=true &
查看日志的命令:tail -f ~/logs/rocketmqlogs/broker.log
5)启动从A: 104服务器进入至’MQ文件夹\bin’下:执行以下命令:
nohup sh mqbroker -c …/conf/2m-2s-sync/broker-a-s.properties autoCreateTopicEnable=true &
查看日志的命令:tail -f ~/logs/rocketmqlogs/broker.log
6)启动从B: 105服务器进入至’MQ文件夹\bin’下:执行以下命令:
nohup sh mqbroker -c …/conf/2m-2s-sync/broker-b-s.properties autoCreateTopicEnable=true &
查看日志的命令:tail -f ~/logs/rocketmqlogs/broker.log
每台服务器查看日志:tail -f ~/logs/rocketmqlogs/broker.log
如果是要启动控制台,则需要重新打包:
进入’\rocketmq-console\src\main\resources’文件夹,打开’application.properties’进行配置。(多个NameServer使用;分隔)
rocketmq.config.namesrvAddr=106.55.246.66:9876;94.191.83.120:9876
进入’\rocketmq-externals\rocketmq-console’文件夹,执行’mvn clean package -Dmaven.test.skip=true’,编译生成。
在把编译后的jar包丢上服务器:
nohup java -jar rocketmq-console-ng-2.0.0.jar &
进入控制台http://106.55.246.66:8089/#/cluster
集群搭建成功。
在创建Topic的时候,把Topic的多个MessageQueue创建在多个Broker组上(相同Broker名称,不同brokerId的机器组成一个Broker组),这样当一个Broker组的Master不可用后,其他组的Master仍然可用,Producer仍然可以发送消息。
RocketMQ目前不支持把Slave自动转成Master,如果机器资源不足,需要把Slave转成Master,则要手动停止Slave角色的Broker,更改配置文件,用新的配置文件启动Broker。
TopicA创建在双主中,BrokerA和BrokerB中,每一个Broker中有4个队列
选择队列是,默认是使用轮训的方式,比如发送一条消息A时,选择BrokerA中的Q4
如果发送成功,消息A发结束。
如果消息发送失败,默认会采用重试机制
retryTimesWhenSendFailed 同步模式下内部尝试发送消息的最大次数默认值是2
retryTimesWhenSendAsyncFailed 异步模式下内部尝试发送消息的最大次数默认值是2
5.1、默认不启用Broker故障延迟机制(规避策略):如果是BrokerA宕机,上一次路由选择的是BrokerA中的Q4,那么再次重发的队列选择是BrokerA中的Q1。但是这里的问题就是消息发送很大可能再次失败,引发再次重复失败,带来不必要的性能损耗。
注意,这里的规避仅仅只针对消息重试,例如在一次消息发送过程中如果遇到消息发送失败,规避broekr-a,但是在下一次消息发送时,即再次调用 DefaultMQProducer 的 send 方法发送消息时,还是会选择 broker-a 的消息进行发送,只有继续发送失败后,重试时再次规避 broker-a。
为什么会默认这么设计?
某一时间段,从NameServer中读到的路由中包含了不可用的主机
不正常的路由信息也是只是一个短暂的时间而已。
生产者每隔30s更新一次路由信息,而NameServer认为broker不可用需要经过120s。
所以生产者要发送时认为broker不正常(从NameServer拿到)和实际Broker不正常有延迟。
5.2、启用Broker故障延迟机制:代码如下
开启延迟规避机制,一旦消息发送失败(不是重试的)会将 broker-a "悲观"地认为在接下来的一段时间内该 Broker 不可用,在为未来某一段时间内所有的客户端不会向该 Broker 发送消息。这个延迟时间就是通过 notAvailableDuration、latencyMax 共同计算的,就首先先计算本次消息发送失败所耗的时延,然后对应 latencyMax 中哪个区间,即计算在 latencyMax 的下标,然后返 notAvailableDuration
同一个下标对应的延迟值。
这个里面涉及到一个算法,源码部分进行详细讲解。
比如**:**在发送失败后,在接下来的固定时间(比如5分钟)内,发生错误的BrokeA中的队列将不再参加队列负载,发送时只选择BrokerB服务器上的队列。
如果所有的 Broker 都触发了故障规避,并且 Broker 只是那一瞬间压力大,那岂不是明明存在可用的 Broker,但经过你这样规避,反倒是没有 Broker 可用来,那岂不是更糟糕了。所以RocketMQ默认不启用Broker故障延迟机制。
在Consumer的配置文件中,并不需要设置是从Master读还是从Slave 读,当Master不可用或者繁忙的时候,Consumer会被自动切换到从Slave 读。有了自动切换Consumer这种机制,当一个Master角色的机器出现故障后,Consumer仍然可以从Slave读取消息,不影响Consumer程序。这就达到了消费端的高可用性。
Master不可用这个很容易理解,那什么是Master繁忙呢?
这个繁忙其实是RocketMQ服务器的内存不够导致的。
源码分析:org.apache.rocketmq.store. DefaultMessageStore#getMessage方法
当前需要拉取的消息已经超过常驻内存的大小,表示主服务器繁忙,此时才建议从从服务器拉取。
消费端如果发生消息失败,没有提交成功,消息默认情况下会进入重试队列中。
注意重试队列的名字其实是跟消费群组有关,不是主题,因为一个主题可以有多个群组消费,所以要注意
对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。
所以玩顺序消息时。consume消费消息失败时,不能返回reconsume------later,这样会导致乱序,应该返回suspend_current_queue_a_moment,意思是先等一会,一会儿再处理这批消息,而不是放到重试队列里。
对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。
第几次重试 | 与上次重试的间隔时间 | 第几次重试 | 与上次重试的间隔时间 |
---|---|---|---|
1 | 10秒 | 9 | 7分钟 |
2 | 30秒 | 10 | 8分钟 |
3 | 1分钟 | 11 | 9分钟 |
4 | 2分钟 | 12 | 10分钟 |
5 | 3分钟 | 13 | 20分钟 |
6 | 4分钟 | 14 | 30分钟 |
7 | 5分钟 | 15 | 1小时 |
8 | 6分钟 | 16 | 2小时 |
如果消息重试 16次后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递。
注意: 一条消息无论重试多少次,这些重试消息的 Message ID 不会改变。
集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种):
返回 RECONSUME_LATER (推荐)
返回 Null
抛出异常
集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回CONSUME_SUCCESS,此后这条消息将不会再重试。
消息队列 RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略:
最大重试次数小于等于 16 次,则重试时间间隔同上表描述。
最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时。
消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。
如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。
配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置
当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。
死信消息具有以下特性:
不会再被消费者正常消费。
有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。
死信队列具有以下特性:
不会再被消费者正常消费。
一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。
在控制台查询出现死信队列的主题信息
在消息界面根据主题查询死信消息
选择重新发送消息
一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要您对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列RocketMQ 控制台重新发送该消息,让消费者重新消费一次。
Producer端,每个实例在发消息的时候,默认会轮询所有的message queue发送,以达到让消息平均落在不同的queue上。而由于queue可以散落在不同的broker,所以消息就发送到不同的broker下,如下图:
发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推。
在集群消费模式下,每条消息只需要投递到订阅这个topic的Consumer Group下的一个实例即可。RocketMQ采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message queue。
而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照queue的数量和实例的数量平均分配queue给每个实例。
默认的分配算法是AllocateMessageQueueAveragely 还有另外一种平均的算法是AllocateMessageQueueAveragelyByCircle,也是平均分摊每一条queue,只是以环状轮流分queue的形式
如下图:
需要注意的是,集群模式下,queue都是只允许分配只一个实例,这是由于如果多个实例同时消费一个queue的消息,由于拉取哪些消息是consumer主动控制的,那样会导致同一个消息在不同的实例下被消费多次,所以算法上都是一个queue只分给一个consumer实例,一个consumer实例可以允许同时分到不同的queue。
通过增加consumer实例去分摊queue的消费,可以起到水平扩展的消费能力的作用。而有实例下线的时候,会重新触发负载均衡,这时候原来分配到的queue将分配到其他实例上继续消费。
但是如果consumer实例的数量比message queue的总数量还多的话,多出来的consumer实例将无法分到queue,也就无法消费到消息,也就无法起到分摊负载的作用了。所以需要控制让queue的总数量大于等于consumer的数量。
由于广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以也就没有消息被分摊消费的说法。
在实现上,其中一个不同就是在consumer分配queue的时候,所有consumer都分到所有的queue。