rocketMQ学习之路
- 前言
- 1 RocketMQ概述
- 2 RocketMQ的安装与启动
- 3 RocketMQ工作原理
- 4 RocketMQ应用
- 总结
前言
本文主要是讲解RocketMQ的相关知识,本篇文章是借鉴了尚硅谷雷老师的笔记。
1 RocketMQ概述
1.1 MQ概述
1.1.1 MQ简介
MQ,Message Queue,是一种提供消息队列服务
的中间件,也称为消息中间件,是一套提供了消息生产、存储、消费全过程API的软件系统。消息即数据。一般消息的体量不会很大。
1.1.2 MQ用途
从网上可以查看到很多的关于MQ用途的叙述,但总结起来其实就以下三点。
数据收集
分布式系统会产生海量级数据流,如:业务日志、监控数据、用户行为等。针对这些数据流进行实时或批量采集汇总,然后对这些数据流进行大数据分析,这是当前互联网平台的必备技术。通过MQ完成此类数据收集是最好的选择。
1.1.3 常见MQ产品
ActiveMQ
ActiveMQ是使用Java语言开发一款MQ产品。早期很多公司与项目中都在使用。但现在的社区活跃度已经很低。现在的项目中已经很少使用了。
RabbitMQ
RabbitMQ是使用ErLang语言开发的一款MQ产品。其吞吐量较Kafka与RocketMQ要低,且由于其不是Java语言开发,所以公司内部对其实现定制化开发难度较大。
Kafka
Kafka是使用Scala/Java语言开发的一款MQ产品。其最大的特点就是高吞吐率,常用于大数据领域的实时计算、日志采集等场景。其没有遵循任何常见的MQ协议,而是使用自研协议。对于Spring CloudNetflix,其仅支持RabbitMQ与Kafka。
RocketMQ
RocketMQ是使用Java语言开发的一款MQ产品。经过数年阿里双11的考验,性能与稳定性非常高。其没有遵循任何常见的MQ协议,而是使用自研协议。对于Spring Cloud Alibaba,其支持RabbitMQ、Kafka,但提倡使用RocketMQ。
1.1.4 MQ常见协议
一般情况下MQ的实现是要遵循一些常规性协议的。常见的协议如下:
JMS
JMS,Java Messaging Service(Java消息服务)。是Java平台上有关MOM(Message OrientedMiddleware,面向消息的中间件 PO/OO/AO)的技术规范,它便于消息系统中的Java应用程序进行消息交换,并且通过提供标准的产生、发送、接收消息的接口,简化企业应用的开发。ActiveMQ是该协议的典型实现。
STOMP
STOMP,Streaming Text Orientated Message Protocol(面向流文本的消息协议),是一种MOM设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。ActiveMQ是该协议的典型实现,RabbitMQ通过插件可以支持该协议。
AMQP
AMQP,Advanced Message Queuing Protocol(高级消息队列协议),一个提供统一消息服务的应用层标准,是应用层协议的一个开放标准,是一种MOM设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。 RabbitMQ是该协议的典型实现。
MQTT
MQTT,Message Queuing Telemetry Transport(消息队列遥测传输),是IBM开发的一个即时通讯协议,是一种二进制协议,主要用于服务器和低功耗IoT(物联网)设备间的通信。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器的通信协议。 RabbitMQ通过插件可以支持该协议。
1.2 RocketMQ概述
1.2.1 RocketMQ简介
RocketMQ是一个统一消息引擎、轻量级数据处理平台。
RocketMQ是⼀款阿⾥巴巴开源的消息中间件。2016年11⽉28⽇,阿⾥巴巴向 Apache 软件基⾦会捐赠RocketMQ,成为 Apache 孵化项⽬。2017 年 9 ⽉ 25 ⽇,Apache 宣布 RocketMQ孵化成为 Apache 顶级项⽬(TLP ),成为国内⾸个互联⽹中间件在 Apache 上的顶级项⽬。
官⽹地址
:http://rocketmq.apache.org
1.2.2 RocketMQ发展历程
2 RocketMQ的安装与启动
2.1 基本概念
2.1.1 消息(Message)
消息是指,消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。
2.1.2 主题(Topic)
Topic表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。
topic:message 1:n
message:topic 1:1
一个生产者可以同时发送多种Topic的消息;而一个消费者只对某种特定的Topic感兴趣,即只可以订阅和消费一种Topic的消息。
producer:topic 1:n
consumer:topic 1:1
2.1.3 标签(Tag)
为消息设置的标签,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。
Topic是消息的一级分类,Tag是消息的二级分类。
Topic:货物
tag=上海
tag=江苏
tag=浙江
------- 消费者 -----
topic=货物 tag = 上海
topic=货物 tag = 上海|浙江
topic=货物 tag = *
2.1.4 队列(Queue)
存储消息的物理实体。一个Topic中可以包含多个Queue,每个Queue中存放的就是该Topic的消息。一个Topic的Queue也被称为一个Topic中消息的分区(Partition)。
一个Topic的Queue中的消息只能被一个消费者组中的一个消费者消费。一个Queue中的消息不允许同一个消费者组中的多个消费者同时消费。
在学习参考其它相关资料时,还会看到一个概念:分片(Sharding)。分片不同于分区。在RocketMQ中,分片指的是存放相应Topic的Broker。每个分片中会创建出相应数量的分区,即Queue,每个Queue的大小都是相同的。
2.1.5 消息标识(MessageId/Key)
RocketMQ中每个消息拥有唯一的MessageId,且可以携带具有业务标识的Key,以方便对消息的查询。
不过需要注意的是,MessageId有两个:在生产者send()消息时会自动生成一个MessageId(msgId),当消息到达Broker后,Broker也会自动生成一个MessageId(offsetMsgId)。msgId、offsetMsgId与key都称为消息标识。
msgId
:由producer端生成,其生成规则为:
producerIp + 进程pid + MessageClientIDSetter类的ClassLoader的hashCode +当前时间 + AutomicInteger自增计数器offsetMsgId
:由broker端生成,其生成规则为: brokerIp + 物理分区的offset(Queue中的偏移量)- key:由用户指定的业务相关的唯一标识
2.2 系统架构
RocketMQ架构上主要分为四部分构成:
2.2.1 Producer
消息生产者,负责生产消息。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
例如,业务系统产生的日志写入到MQ的过程,就是消息生产的过程
再如,电商平台中用户提交的秒杀请求写入到MQ的过程,就是消息生产的过程
RocketMQ中的消息生产者都是以生产者组(Producer Group)的形式出现的。生产者组是同一类生产者的集合,这类Producer发送相同Topic类型的消息。一个生产者组可以同时发送多个主题的消息。
2.2.2 Consumer
消息消费者,负责消费消息。一个消息消费者会从Broker服务器中获取到消息,并对消息进行相关业务处理。
例如,QoS系统从MQ中读取日志,并对日志进行解析处理的过程就是消息消费的过程。
再如,电商平台的业务系统从MQ中读取到秒杀请求,并对请求进行处理的过程就是消息消费的过程。
RocketMQ中的消息消费者都是以消费者组(Consumer Group)的形式出现的。消费者组是同一类消费者的集合,这类Consumer消费的是同一个Topic类型的消息。消费者组使得在消息消费方面,实现负载均衡
(将一个Topic中的不同的Queue平均分配给同一个Consumer Group的不同的Consumer,注意,并不是将消息负载均衡)和容错
(一个Consmer挂了,ConsumerGroup中的其它Consumer可以接着消费原Consumer消费的Queue)的目标变得非常容易。
消费者组中Consumer的数量应该小于等于订阅Topic的Queue数量。如果超出Queue数量,则多出的Consumer将不能消费消息。
不过,一个Topic类型的消息可以被多个消费者组同时消费。
注意,
1)消费者组只能消费一个Topic的消息,不能同时消费多个Topic消息
2)一个消费者组中的消费者必须订阅完全相同的Topic
2.2.3 Name Server
功能介绍
NameServer是一个Broker与Topic路由的注册中心,支持Broker的动态注册与发现。
RocketMQ的思想来自于Kafka,而Kafka是依赖了Zookeeper的。所以,在RocketMQ的早期版本,即在MetaQ v1.0与v2.0版本中,也是依赖Zookeeper的。从MetaQ v3.0,即RocketMQ开始去掉了Zookeeper依赖,使用了自己的NameServer。
主要包括两个功能:
- Broker管理:接受Broker集群的注册信息并且保存下来作为路由信息的基本数据;提供心跳检测机制,检查Broker是否还存活。
- 路由信息管理:每个NameServer中都保存着Broker集群的整个路由信息和用于客户端查询的队列信息。Producer和Conumser通过NameServer可以获取整个Broker集群的路由信息,从而进行消息的投递和消费。
路由注册
NameServer通常也是以集群的方式部署,不过,NameServer是无状态的,即NameServer集群中的各个节点间是无差异的,各节点间相互不进行信息通讯。那各节点中的数据是如何进行数据同步的呢?在Broker节点启动时,轮询NameServer列表,与每个NameServer节点建立长连接,发起注册请求。在NameServer内部维护着⼀个Broker列表,用来动态存储Broker的信息。
注意,这是与其它像zk、Eureka、Nacos等注册中心不同的地方。
这种NameServer的无状态方式,有什么优缺点:
优点:NameServer集群搭建简单,扩容简单。
缺点:对于Broker,必须明确指出所有NameServer地址。否则未指出的将不会去注册。也正因为如此,NameServer并不能随便扩容。因为,若Broker不重新配置,新增的NameServer对于Broker来说是不可见的,其不会向这个NameServer进行注册。
Broker节点为了证明自己是活着的,为了维护与NameServer间的长连接,会将最新的信息以心跳包
的方式上报给NameServer,每30秒发送一次心跳。心跳包中包含 BrokerId、Broker地址(IP+Port)、Broker名称、Broker所属集群名称等等。NameServer在接收到心跳包后,会更新心跳时间戳,记录这个Broker的最新存活时间。
路由剔除
由于Broker关机、宕机或网络抖动等原因,NameServer没有收到Broker的心跳,NameServer可能会将其从Broker列表中剔除。
NameServer中有⼀个定时任务,每隔10秒就会扫描⼀次Broker表,查看每一个Broker的最新心跳时间戳距离当前时间是否超过120秒,如果超过,则会判定Broker失效,然后将其从Broker列表中剔除。
扩展:对于RocketMQ日常运维工作,例如Broker升级,需要停掉Broker的工作。OP需要怎么做?
OP需要将Broker的读写权限禁掉。一旦client(Consumer或Producer)向broker发送请求,都会收到broker的NO_PERMISSION响应,然后client会进行对其它Broker的重试。
当OP观察到这个Broker没有流量后,再关闭它,实现Broker从NameServer的移除。
OP:运维工程师
SRE:Site Reliability Engineer,现场可靠性工程师
路由发现
RocketMQ的路由发现采用的是Pull模型。当Topic路由信息出现变化时,NameServer不会主动推送给客户端,而是客户端定时拉取主题最新的路由。默认客户端每30秒会拉取一次最新的路由。
扩展:
1)Push模型:推送模型。其实时性较好,是一个“发布-订阅”模型,需要维护一个长连接。而长连接的维护是需要资源成本的。该模型适合于的场景:
实时性要求较高
Client数量不多,Server数据变化较频繁
2)Pull模型:拉取模型。存在的问题是,实时性较差。
3)Long Polling模型:长轮询模型。其是对Push与Pull模型的整合,充分利用了这两种模型的优势,屏蔽了它们的劣势。
客户端NameServer选择策略
这里的客户端指的是Producer与Consumer
客户端在配置时必须要写上NameServer集群的地址,那么客户端到底连接的是哪个NameServer节点呢?客户端首先会生产一个随机数,然后再与NameServer节点数量取模,此时得到的就是所要连接的节点索引,然后就会进行连接。如果连接失败,则会采用round-robin策略,逐个尝试着去连接其它节点。
首先采用的是随机策略
进行的选择,失败后采用的是轮询策略
。
扩展:Zookeeper Client是如何选择Zookeeper Server的?
简单来说就是,经过两次Shuffle,然后选择第一台Zookeeper Server。
详细说就是,将配置文件中的zk server地址进行第一次shuffle,然后随机选择一个。这个选择出的一般都是一个hostname。然后获取到该hostname对应的所有ip,再对这些ip进行第二次shuffle,从shuffle过的结果中取第一个server地址进行连接。
2.2.4 Broker
功能介绍
Broker充当着消息中转角色,负责存储消息、转发消息。Broker在RocketMQ系统中负责接收并存储从生产者发送来的消息,同时为消费者的拉取请求作准备。Broker同时也存储着消息相关的元数据,包括消费者组消费进度偏移offset、主题、队列等。
Kafka 0.8版本之后,offset是存放在Broker中的,之前版本是存放在Zookeeper中的。
模块构成
下图为Broker Server的功能模块示意图。
Remoting Module
:整个Broker的实体,负责处理来自clients端的请求。而这个Broker实体则由以下模块构成。
Client Manager
:客户端管理器。负责接收、解析客户端(Producer/Consumer)请求,管理客户端。例如,维护Consumer的Topic订阅信息。
Store Service
:存储服务。提供方便简单的API接口,处理消息存储到物理硬盘和消息查询功能。
HA Service
:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
Index Service
:索引服务。根据特定的Message key,对投递到Broker的消息进行索引服务,同时也提供根据Message Key对消息进行快速查询的功能。
集群部署
为了增强Broker性能与吞吐量,Broker一般都是以集群形式出现的。各集群节点中可能存放着相同Topic的不同Queue。不过,这里有个问题,如果某Broker节点宕机,如何保证数据不丢失呢?其解决方案是,将每个Broker集群节点进行横向扩展,即将Broker节点再建为一个HA集群,解决单点问题。
Broker节点集群是一个主从集群,即集群中具有Master与Slave两种角色。Master负责处理读写操作请求,Slave负责对Master中的数据进行备份。当Master挂掉了,Slave则会自动切换为Master去工作。所以这个Broker集群是主备集群。一个Master可以包含多个Slave,但一个Slave只能隶属于一个Master。Master与Slave 的对应关系是通过指定相同的BrokerName、不同的BrokerId 来确定的。BrokerId为0表示Master,非0表示Slave。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。
2.2.5 工作流程
具体流程
1)启动NameServer,NameServer启动后开始监听端口,等待Broker、Producer、Consumer连接。
2)启动Broker时,Broker会与所有的NameServer建立并保持长连接,然后每30秒向NameServer定时发送心跳包。
3)发送消息前,可以先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,当然,在创建Topic时也会将Topic与Broker的关系写入到NameServer中。不过,这步是可选的,也可以在发送消息时自动创建Topic。
4)Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取路由信息,即当前发送的Topic消息的Queue与Broker的地址(IP+Port)的映射关系。然后根据算法策略从队选择一个Queue,与队列所在的Broker建立长连接从而向Broker发消息。当然,在获取到路由信息后,Producer会首先将路由信息缓存到本地,再每30秒从NameServer更新一次路由信息。
5)Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取其所订阅Topic的路由信息,然后根据算法策略从路由信息中获取到其所要消费的Queue,然后直接跟Broker建立长连接,开始消费其中的消息。Consumer在获取到路由信息后,同样也会每30秒从NameServer更新一次路由信息。不过不同于Producer的是,Consumer还会向Broker发送心跳,以确保Broker的存活状态。
Topic的创建模式
手动创建Topic时,有两种模式:
- 集群模式:该模式下创建的Topic在该集群中,所有Broker中的Queue数量是相同的。
- Broker模式:该模式下创建的Topic在该集群中,每个Broker中的Queue数量可以不同。
自动创建Topic时,默认采用的是Broker模式,会为每个Broker默认创建4个Queue。
读/写队列
从物理上来讲,读/写队列是同一个队列。所以,不存在读/写队列数据同步问题。读/写队列是逻辑上进行区分的概念。一般情况下,读/写队列数量是相同的。
例如,创建Topic时设置的写队列数量为8,读队列数量为4,此时系统会创建8个Queue,分别是0 1 2 3 4 5 6 7。Producer会将消息写入到这8个队列,但Consumer只会消费0 1 2 3这4个队列中的消息,4 5 6 7中的消息是不会被消费到的。
再如,创建Topic时设置的写队列数量为4,读队列数量为8,此时系统会创建8个Queue,分别是0 1 2 3 4 5 6 7。Producer会将消息写入到0 1 2 3 这4个队列,但Consumer只会消费0 1 2 3 4 5 6 7这8个队列中的消息,但是4 5 6 7中是没有消息的。此时假设Consumer Group中包含两个Consuer,Consumer1消费0 1 2 3,而Consumer2消费4 5 6 7。但实际情况是,Consumer2是没有消息可消费的。
也就是说,当读/写队列数量设置不同时,总是有问题的。那么,为什么要这样设计呢?
其这样设计的目的是为了,方便Topic的Queue的缩容。
例如,原来创建的Topic中包含16个Queue,如何能够使其Queue缩容为8个,还不会丢失消息?可以动态修改写队列数量为8,读队列数量不变。此时新的消息只能写入到前8个队列,而消费都消费的却是16个队列中的数据。当发现后8个Queue中的消息消费完毕后,就可以再将读队列数量动态设置为8。整个缩容过程,没有丢失任何消息。
perm用于设置对当前创建Topic的操作权限:2表示只写,4表示只读,6表示读写。
2.3 单机安装与启动
2.3.1 准备工作
软硬件需求
系统要求是64位的,JDK要求是1.8及其以上版本的。
下载RocketMQ安装包
这里选择和课程教学相同的版本 4.9.0.
将下载的安装包上传到Linux。
解压。
unzip 下载的文件即可
2.3.2 修改初始内存
修改runserver.sh
使用vim命令打开bin/runserver.sh文件。现将这些值修改为如下:
2.3.3 启动
启动server
nohup sh bin/mqnamesrv &
tail -f ~/logs/rocketmqlogs/namesrv.log
The Name Server boot success...
启动broker
nohup sh bin/mqbroker -n localhost:9876 &
tail -f ~/logs/rocketmqlogs/broker.log
The broker[%s, 172.30.30.233:10911] boot success...
2.3.4 发送/接收消息测试
发送消息
export NAMESRV_ADDR=localhost:9876
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
接收消息
sh bin/tools.sh org.apache.rocketmq.example.1 quickstart.Consumer
2.3.5 关闭server
无论是关闭name server还是broker,都是使用bin/mqshutdown
命令。
[root@mqOS rocketmq]# sh bin/mqshutdown broker
The mqbroker(1740) is running...
Send shutdown request to mqbroker(1740) OK
[root@mqOS rocketmq]# sh bin/mqshutdown namesrv
The mqnamesrv(1692) is running...
Send shutdown request to mqnamesrv(1692) OK
[2]+ 退出 143 nohup sh bin/mqbroker -n localhost:98
2.4 控制台的安装与启动
RocketMQ有一个可视化的dashboard,通过该控制台可以直观的查看到很多数据。
2.4.1 下载
https://github.com/apache/rocketmq-externals/tags。
2.4.2 修改配置
修改其src/main/resources中的application.properties配置文件。
- 原来的端口号为8080,修改为一个不常用的
- 指定RocketMQ的name server地址
2.4.3 添加依赖
在解压目录rocketmq-console的pom.xml中添加如下JAXB依赖。
JAXB,Java Architechture for Xml Binding,用于XML绑定的Java技术,是一个业界标准,是一项可以根据XML Schema生成Java类的技术。
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
2.4.4 打包
2.4.5 启动
2.4.6 访问
2.5 集群搭建理论
2.5.1 数据复制与刷盘策略
复制策略
复制策略是Broker的Master与Slave间的数据同步方式。分为同步复制与异步复制:
- 同步复制:消息写入master后,master会等待slave同步数据成功后才向producer返回成功ACK
- 异步复制:消息写入master后,master立即向producer返回成功ACK,无需等待slave同步数据成功
异步复制策略会降低系统的写入延迟,RT变小,提高了系统的吞吐量
刷盘策略
刷盘策略指的是broker中消息的落盘
方式,即消息发送到broker内存后消息持久化到磁盘的方式。分为
同步刷盘与异步刷盘:
- 同步刷盘:当消息持久化到broker的磁盘后才算是消息写入成功。
- 异步刷盘:当消息写入到broker的内存后即表示消息写入成功,无需等待消息持久化到磁盘。
1)异步刷盘策略会降低系统的写入延迟,RT变小,提高了系统的吞吐量
2)消息写入到Broker的内存,一般是写入到了PageCache
3)对于异步 刷盘策略,消息会写入到PageCache后立即返回成功ACK。但并不会立即做落盘操作,而是当PageCache到达一定量时会自动进行落盘。
2.5.2 Broker集群模式
根据Broker集群中各个节点间关系的不同,Broker集群可以分为以下几类:
单Master
只有一个broker(其本质上就不能称为集群)。这种方式也只能是在测试时使用,生产环境下不能使用,因为存在单点问题。
多Master
broker集群仅由多个master构成,不存在Slave。同一Topic的各个Queue会平均分布在各个master节点上。
- 优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高;
- 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅(不可消费),
消息实时性会受到影响。以上优点的前提是,这些Master都配置了RAID磁盘阵列。如果没有配置,一旦出现某Master宕机,则会发生大量消息丢失的情况。
多Master多Slave模式-异步复制
broker集群由多个master构成,每个master又配置了多个slave(在配置了RAID磁盘阵列的情况下,一个master一般配置一个slave即可)。master与slave的关系是主备关系,即master负责处理消息的读写请求,而slave仅负责消息的备份与master宕机后的角色切换。
异步复制即前面所讲的复制策略中的异步复制策略
,即消息写入master成功后,master立即向producer返回成功ACK,无需等待slave同步数据成功。
该模式的最大特点之一是,当master宕机后slave能够自动切换
为master。不过由于slave从master的同步具有短暂的延迟(毫秒级),所以当master宕机后,这种异步复制方式可能会存在少量消息的丢失问题。
Slave从Master同步的延迟越短,其可能丢失的消息就越少
对于Master的RAID磁盘阵列,若使用的也是异步复制策略,同样也存在延迟问题,同样也可能会丢失消息。但RAID阵列的秘诀是微秒级的(因为是由硬盘支持的),所以其丢失的数据量会更少。
多Master多Slave模式-同步双写
该模式是多Master多Slave模式
的同步复制
实现。所谓同步双写
,指的是消息写入master成功后,master会等待slave同步数据成功后才向producer返回成功ACK,即master与slave都要写入成功后才会返回成功ACK,也即双写
。
该模式与异步复制模式
相比,优点是消息的安全性更高,不存在消息丢失的情况。但单个消息的RT略高,从而导致性能要略低(大约低10%)。
该模式存在一个大的问题:对于目前的版本,Master宕机后,Slave 不会自动切换
到Master。
最佳实践
一般会为Master配置RAID10磁盘阵列,然后再为其配置一个Slave。即利用了RAID10磁盘阵列的高效、安全性,又解决了可能会影响订阅的问题。
1)RAID磁盘阵列的效率要高于Master-Slave集群。因为RAID是硬件支持的。也正因为如此,所以RAID阵列的搭建成本较高。
2)多Master+RAID阵列,与多Master多Slave集群的区别是什么?
- 多Master+RAID阵列,其仅仅可以保证数据不丢失,即不影响消息写入,但其可能会影响到消息的订阅。但其执行效率要远高于
多Master多Slave集群
- 多Master多Slave集群,其不仅可以保证数据不丢失,也不会影响消息写入。其运行效率要低于多
Master+RAID阵列
2.6 磁盘阵列RAID
2.6.1 RAID历史
1988 年美国加州大学伯克利分校的 D. A. Patterson 教授等首次在论文 “A Case of Redundant Array of Inexpensive Disks” 中提出了 RAID 概念 ,即廉价冗余磁盘阵列
( Redundant Array of Inexpensive Disks )。由于当时大容量磁盘比较昂贵, RAID 的基本思想是将多个容量较小、相对廉价的磁盘进行有机组合,从而以较低的成本获得与昂贵大容量磁盘相当的容量、性能、可靠性。随着磁盘成本和价格的不断降低, “廉价” 已经毫无意义。因此, RAID 咨询委员会( RAID Advisory Board, RAB )决定用“ 独立 ” 替代 “ 廉价 ” ,于时 RAID 变成了独立磁盘冗余阵列
( Redundant Array of Independentb Disks )。但这仅仅是名称的变化,实质内容没有改变。
内存:32m 6.4G(IBM 10.1G)
2.6.2 RAID等级
RAID 这种设计思想很快被业界接纳, RAID 技术作为高性能、高可靠的存储技术,得到了非常广泛的应用。 RAID 主要利用镜像、数据条带和数据校验三种技术来获取高性能、可靠性、容错能力和扩展性,根据对这三种技术的使用策略和组合架构,可以把 RAID 分为不同的等级,以满足不同数据应用的需求。
D. A. Patterson 等的论文中定义了 RAID0 ~ RAID6 原始 RAID 等级。随后存储厂商又不断推出 RAID7、 RAID10、RAID01 、 RAID50 、 RAID53 、 RAID100 等 RAID 等级,但这些并无统一的标准。目前业界与学术界公认的标准是 RAID0 ~ RAID6 ,而在实际应用领域中使用最多的 RAID 等级是 RAID0 、RAID1 、 RAID3 、 RAID5 、 RAID6 和 RAID10。
RAID 每一个等级代表一种实现方法和技术,等级之间并无高低之分。在实际应用中,应当根据用户的数据应用特点,综合考虑可用性、性能和成本来选择合适的 RAID 等级,以及具体的实现方式。
2.6.3 关键技术
2.6.4 RAID分类
2.6.5 常见RAID等级详解
RAID0 是一种简单的、无数据校验的数据条带化技术
。实际上不是一种真正的 RAID ,因为它并不提供任何形式的冗余策略。 RAID0 将所在磁盘条带化后组成大容量的存储空间,将数据分散存储在所有磁盘中,以独立访问方式实现多块磁盘的并读访问。
理论上讲,一个由 n 块磁盘组成的 RAID0 ,它的读写性能是单个磁盘性能的 n 倍,但由于总线带宽等多种因素的限制,实际的性能提升低于理论值。由于可以并发执行 I/O 操作,总线带宽得到充分利用。
再加上不需要进行数据校验, RAID0 的性能在所有 RAID 等级中是最高的
。
RAID0 具有低成本、高读写性能、 100% 的高存储空间利用率等优点,但是它不提供数据冗余保护,一旦数据损坏,将无法恢复。
应用场景:对数据的顺序读写要求不高,对数据的安全性和可靠性要求不高,但对系统性能要求很高的场景。
RAID0与JBOD相同点:
1)存储容量:都是成员磁盘容量总和
2)磁盘利用率,都是100%,即都没有做任何的数据冗余备份
RAID0与JBOD不同点:
JBOD:数据是顺序存放的,一个磁盘存满后才会开始存放到下一个磁盘
RAID:各个磁盘中的数据写入是并行的,是通过数据条带技术写入的。其读写性能是JBOD的n倍
简单来说就是,先做镜像
再做条带
。即将进来的数据先做镜像,再将镜像数据写入到与之前数据不同的磁盘,即再做条带。
RAID10要比RAID01的容错率再高,所以生产环境下一般是不使用RAID01的。
2.7 集群搭建实践
# 指定整个broker集群的名称,或者说是RocketMQ集群的名称
brokerClusterName=DefaultCluster
# 指定master-slave集群的名称。一个RocketMQ集群可以包含多个master-slave集群
brokerName=broker-a
# master的brokerId为0
brokerId=0
# 指定删除消息存储过期文件的时间为凌晨4点
deleteWhen=04
# 指定未发生更新的消息存储文件的保留时长为48小时,48小时后过期,将会被删除
fileReservedTime=48
# 指定当前broker为异步复制master
brokerRole=ASYNC_MASTER
# 指定刷盘策略为异步刷盘
flushDiskType=ASYNC_FLUSH
# 指定Name Server的地址
namesrvAddr=192.168.249.103:9876;192.168.249.104:9876
修改broker-b-s.properties
将该配置文件内容修改为如下:
brokerClusterName=DefaultCluster
# 指定这是另外一个master-slave集群
brokerName=broker-b
# slave的brokerId为非0
brokerId=1
deleteWhen=04
fileReservedTime=48
# 指定当前broker为slave
brokerRole=SLAVE
flushDiskType=ASYNC_FLUSH
namesrvAddr=192.168.249.103:9876;192.168.249.104:9876
# 指定Broker对外提供服务的端口,即Broker与producer与consumer通信的端口。默认
10911。由于当前主机同时充当着master1与slave2,而前面的master1使用的是默认端口。这
里需要将这两个端口加以区分,以区分出master1与slave2
listenPort=11911
# 指定消息存储相关的路径。默认路径为~/store目录。由于当前主机同时充当着master1与
slave2,master1使用的是默认路径,这里就需要再指定一个不同路径
storePathRootDir=~/store-s
storePathCommitLog=~/store-s/commitlog
storePathConsumeQueue=~/store-s/consumequeue
storePathIndex=~/store-s/index
storeCheckpoint=~/store-s/checkpoint
abortFile=~/store-s/abort
其他配置
除了以上配置外,这些配置文件中还可以设置其它属性。
#指定整个broker集群的名称,或者说是RocketMQ集群的名称
brokerClusterName=rocket-MS
#指定master-slave集群的名称。一个RocketMQ集群可以包含多个master-slave集群
brokerName=broker-a
#0 表示 Master,>0 表示 Slave
brokerId=0
#nameServer地址,分号分割
namesrvAddr=nameserver1:9876;nameserver2:9876
#默认为新建Topic所创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker 自动创建Topic,建议生产环境中关闭
autoCreateTopicEnable=true
#是否允许 Broker 自动创建订阅组,建议生产环境中关闭
autoCreateSubscriptionGroup=true
#Broker对外提供服务的端口,即Broker与producer与consumer通信的端口
listenPort=10911
#HA高可用监听端口,即Master与Slave间通信的端口,默认值为listenPort+1
haListenPort=10912
#指定删除消息存储过期文件的时间为凌晨4点
deleteWhen=04
#指定未发生更新的消息存储文件的保留时长为48小时,48小时后过期,将会被删除
fileReservedTime=48
#指定commitLog目录中每个文件的大小,默认1G
mapedFileSizeCommitLog=1073741824
#指定ConsumeQueue的每个Topic的每个Queue文件中可以存放的消息数量,默认30w条
mapedFileSizeConsumeQueue=300000
#在清除过期文件时,如果该文件被其他线程所占用(引用数大于0,比如读取消息),此时会阻止
#此次删除任务,同时在第一次试图删除该文件时记录当前时间戳。该属性则表示从第一次拒绝删除
#后开始计时,该文件最多可以保留的时长。在此时间内若引用数仍不为0,则删除仍会被拒绝。不过
#时间到后,文件将被强制删除
destroyMapedFileIntervalForcibly=120000
#指定commitlog、consumequeue所在磁盘分区的最大使用率,超过该值,则需立即清除过期文
件
diskMaxUsedSpaceRatio=88
#指定store目录的路径,默认在当前用户主目录中
storePathRootDir=/usr/local/rocketmq-all-4.5.0/store
#commitLog目录路径
storePathCommitLog=/usr/local/rocketmq-all-4.5.0/store/commitlog
#consumeueue目录路径
storePathConsumeQueue=/usr/local/rocketmq-all-4.5.0/store/consumequeue
#index目录路径
storePathIndex=/usr/local/rocketmq-all-4.5.0/store/index
#checkpoint文件路径
storeCheckpoint=/usr/local/rocketmq-all-4.5.0/store/checkpoint
#abort文件路径
abortFile=/usr/local/rocketmq-all-4.5.0/store/abort
#指定消息的最大大小
maxMessageSize=65536
#Broker的角色
# - ASYNC_MASTER 异步复制Master
# - SYNC_MASTER 同步双写Master
# - SLAVE
brokerRole=SYNC_MASTER
#刷盘策略
# - ASYNC_FLUSH 异步刷盘
# - SYNC_FLUSH 同步刷盘
flushDiskType=SYNC_FLUSH
#发消息线程池数量
sendMessageThreadPoolNums=128
#拉消息线程池数量
pullMessageThreadPoolNums=128
#强制指定本机IP,需要根据每台机器进行修改。官方介绍可为空,系统默认自动识别,但多网卡
时IP地址可能读取错误
brokerIP1=192.168.3.105
brokerClusterName=DefaultCluster
brokerName=broker-b
brokerId=0
deleteWhen=04
fileReservedTime=48
brokerRole=ASYNC_MASTER
flushDiskType=ASYNC_FLUSH
namesrvAddr=192.168.249.103:9876;192.168.249.104:9876
修改broker-a-s.properties
将该配置文件内容修改为如下:
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=1
deleteWhen=04
fileReservedTime=48
brokerRole=SLAVE
flushDiskType=ASYNC_FLUSH
namesrvAddr=192.168.59.164:9876;192.168.59.165:9876
listenPort=11911
storePathRootDir=~/store-s
storePathCommitLog=~/store-s/commitlog
storePathConsumeQueue=~/store-s/consumequeue
storePathIndex=~/store-s/index
storeCheckpoint=~/store-s/checkpoint
abortFile=~/store-s/abort
6 启动服务器
启动NameServer集群
分别启动rocketmqOS1与rocketmqOS2两个主机中的NameServer。启动命令完全相同。
nohup sh bin/mqnamesrv &
tail -f ~/logs/rocketmqlogs/namesrv.log
启动两个Master
分别启动rocketmqOS1与rocketmqOS2两个主机中的broker master。注意,它们指定所要加载的配置文件是不同的。
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a.properties &
tail -f ~/logs/rocketmqlogs/broker.log
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b.properties &
tail -f ~/logs/rocketmqlogs/broker.log
启动两个Slave
分别启动rocketmqOS1与rocketmqOS2两个主机中的broker slave。注意,它们指定所要加载的配置文件是不同的。
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b-s.properties &
tail -f ~/logs/rocketmqlogs/broker.log
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a-s.properties &
tail -f ~/logs/rocketmqlogs/broker.log
2.8 mqadmin命令
在mq解压目录的bin目录下有一个mqadmin命令,该命令是一个运维指令,用于对mq的主题,集群,broker 等信息进行管理。
2.8.1 修改bin/tools.sh
在运行mqadmin命令之前,先要修改mq解压目录下bin/tools.sh配置的JDK的ext目录位置。本机的ext
目录在/usr/java/jdk1.8.0_161/jre/lib/ext
。
使用vim命令打开tools.sh文件,并在JAVA_OPT配置的-Djava.ext.dirs这一行的后面添加ext的路径。
JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn256m -
XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m"
JAVA_OPT="${JAVA_OPT} -
Djava.ext.dirs=${BASE_DIR}/lib:${JAVA_HOME}/jre/lib/ext:${JAVA_HOME}/lib/
ext:/usr/java/jdk1.8.0_161/jre/lib/ext"
JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"
2.8.2 运行mqadmin
直接运行该命令,可以看到其可以添加的commands。通过这些commands可以完成很多的功能。
[root@mqOS rocketmq-all-4.8.0-bin-release]# ./bin/mqadmin
The most commonly used mqadmin commands are:
updateTopic Update or create topic
deleteTopic Delete topic from broker and NameServer.
updateSubGroup Update or create subscription group
deleteSubGroup Delete subscription group from broker.
updateBrokerConfig Update broker's config
updateTopicPerm Update topic perm
topicRoute Examine topic route info
topicStatus Examine topic Status info
topicClusterList get cluster info for topic
brokerStatus Fetch broker runtime status data
queryMsgById Query Message by Id
queryMsgByKey Query Message by Key
queryMsgByUniqueKey Query Message by Unique key
queryMsgByOffset Query Message by offset
QueryMsgTraceById query a message trace
printMsg Print Message Detail
printMsgByQueue Print Message Detail
sendMsgStatus send msg to broker.
brokerConsumeStats Fetch broker consume stats data
producerConnection Query producer's socket connection and client
version
consumerConnection Query consumer's socket connection, client
version and subscription
consumerProgress Query consumers's progress, speed
consumerStatus Query consumer's internal data structure
cloneGroupOffset clone offset from other group.
clusterList List all of clusters
topicList Fetch all topic list from name server
updateKvConfig Create or update KV config.
deleteKvConfig Delete KV config.
wipeWritePerm Wipe write perm of broker in all name server
resetOffsetByTime Reset consumer offset by timestamp(without
client restart).
updateOrderConf Create or update or delete order conf
cleanExpiredCQ Clean expired ConsumeQueue on broker.
cleanUnusedTopic Clean unused topic on broker.
startMonitoring Start Monitoring
statsAll Topic and Consumer tps stats
allocateMQ Allocate MQ
checkMsgSendRT check message send response time
clusterRT List All clusters Message Send RT
getNamesrvConfig Get configs of name server.
updateNamesrvConfig Update configs of name server.
getBrokerConfig Get broker config by cluster or special broker!
queryCq Query cq command.
sendMessage Send a message
consumeMessage Consume message
updateAclConfig Update acl config yaml file in broker
deleteAccessConfig Delete Acl Config Account in broker
clusterAclConfigVersion List all of acl config version information in
cluster
updateGlobalWhiteAddr Update global white address for acl Config File
in broker
getAccessConfigSubCommand List all of acl config information in
cluster
2.8.3 该命令的官网详解
该命令在官网中有详细的用法解释。
https://github.com/apache/rocketmq/blob/master/docs/cn/operation.md
3 RocketMQ工作原理
3.1 消息的生产
3.1.1 消息的生产过程
Producer可以将消息写入到某Broker中的某Queue中,其经历了如下过程:
- Producer发送消息之前,会先向NameServer发出获取
消息Topic的路由信息
的请求 - NameServer返回该Topic的
路由表
及Broker列表
- Producer根据代码中指定的Queue选择策略,从Queue列表中选出一个队列,用于后续存储消息
- Produer对消息做一些特殊处理,例如,消息本身超过4M,则会对其进行压缩
- Producer向选择出的Queue所在的Broker发出RPC请求,将消息发送到选择出的Queue
路由表:实际是一个Map,key为Topic名称,value是一个QueueData实例列表。QueueData并不是一个Queue对应一个QueueData,而是一个Broker中该Topic的所有Queue对应一个QueueData。即,只要涉及到该Topic的Broker,一个Broker对应一个QueueData。QueueData中包含brokerName。简单来说,路由表的key为Topic名称,value则为所有涉及该Topic的BrokerName列表。
Broker列表:其实际也是一个Map。key为brokerName,value为BrokerData。一个Broker对应一个BrokerData实例,对吗?不对。一套brokerName名称相同的Master-Slave小集群对应一个BrokerData。BrokerData中包含brokerName及一个map。该map的key为brokerId,value为该broker对应的地址。brokerId为0表示该broker为Master,非0表示Slave。
3.1.2 Queue选择算法
对于无序消息,其Queue选择算法,也称为消息投递算法,常见的有两种:
轮询算法
默认选择算法。该算法保证了每个Queue中可以均匀的获取到消息。
该算法存在一个问题:由于某些原因,在某些Broker上的Queue可能投递延迟较严重。从而导致Producer的缓存队列中出现较大的消息积压,影响消息的投递性能。
最小投递延迟算法
该算法会统计每次消息投递的时间延迟,然后根据统计出的结果将消息投递到时间延迟最小的Queue。
如果延迟相同,则采用轮询算法投递。该算法可以有效提升消息的投递性能。
该算法也存在一个问题:消息在Queue上的分配不均匀。投递延迟小的Queue其可能会存在大量的消息。而对该Queue的消费者压力会增大,降低消息的消费能力,可能会导致MQ中消息的堆积。
3.2 消息的存储
RocketMQ中的消息存储在本地文件系统中,这些相关文件默认在当前用户主目录下的store目录中。
- abort:该文件在Broker启动后会自动创建,正常关闭Broker,该文件会自动消失。若在没有启动 Broker 的情况下,发现这个文件是存在的,则说明之前Broker的关闭是非正常关闭。
- checkpoint:其中存储着commitlog、consumequeue、index文件的最后刷盘时间戳
- commitlog:其中存放着commitlog文件,而消息是写在commitlog文件中的
- config:存放着Broker运行期间的一些配置数据
- consumequeue:其中存放着consumequeue文件,队列就存放在这个目录中
- index:其中存放着消息索引文件indexFile
- lock:运行期间使用到的全局资源锁
3.2.1 commitlog文件
3.2.2 consumequeue
3.3.3 对文件的读写
3.3.4 与Kafka的对比
3.3 indexFile
3.3.1 索引条目结构
3.3.2 indexFile的创建
3.3.3 查询流程
3.4 消息的消费
3.4.1 获取消费类型
3.4.2 消费模式
3.4.3 Rebalance机制
Rebalance机制讨论的前提是:集群消费。
3.4.4 Queue分配算法
3.4.5 至少一次原则
3.5 订阅关系的一致性
3.5.1 正确订阅关系
3.5.2 错误订阅关系
3.6 offset管理
3.6.1 offset本地管理模式
3.6.2 offset远程管理模式
3.6.3 offset用途
3.6.4 重试队列
3.6.5 offset的同步提交与异步提交
3.7 消费幂等
3.7.1 什么是消费幂等
3.7.2 消息重复的场景分析
3.7.3 通用解决方案
3.7.4 消费幂等的实现
3.8 消息堆积与消费延迟
3.8.1 概念
3.8.2 产生原因分析
3.8.3 消费耗时
3.8.4 消费并发度
3.8.5 单机线程数计算
3.8.6 如何避免
3.9 消息的清理
4 RocketMQ应用
4.1 普通消息
4.1.1 消息发送分类
4.1.2 代码举例
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.0</version>
</dependency>
</dependencies>
定义同步消息发送生产者
public class SyncProducer {
public static void main(String[] args) throws Exception {
//创建一个 producer,参数为 Producer Group名称
DefaultMQProducer producer = new DefaultMQProducer("pg");
//指定nameServer地址
producer.setNamesrvAddr("192.168.249.103:9876");
//可以设置一些自定义参数
// 设置当发送失败时重试发送的次数,默认为2次
producer.setRetryTimesWhenSendAsyncFailed(3);
// 设置发送超时时限为5s,默认为3s
producer.setSendMsgTimeout(5000);
//开启生产者
producer.start();
//生产并发送100条消息
for (int i = 0; i < 100; i++) {
byte[] body = ("Hi," + i).getBytes();
Message message = new Message("someTopic", "someTag", body);
//为消息指定key
message.setKeys("key-" + i);
//发送消息
SendResult sendResult = producer.send(message);
System.out.println(sendResult);
}
// 关闭 producer
producer.shutdown();
}
}
定义异步消息发送生产者
public class AsyncProducer {
public static void main(String[] args) throws Exception{
//创建一个 producer,参数为 Producer Group名称
DefaultMQProducer producer = new DefaultMQProducer("pg");
指定nameServer地址
producer.setNamesrvAddr("192.168.249.103:9876");
//自定义参数设置
//指定异步发送失败后不进行重试发送
producer.setRetryTimesWhenSendAsyncFailed(0);
//指定新创建的Topic的Queue数量为 2,默认为4
producer.setDefaultTopicQueueNums(2);
//启动
producer.start();
//生产消息
for (int i = 0; i < 100; i++) {
byte[] body = ("Hi," + i).getBytes();
Message message = new Message("myTopic", "myTag", body);
//异步发送,指定回调方法
producer.send(message, new SendCallback() {
//当 producer 接收到MQ发送来的ACK后就会触发该回调方法的执行
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
@Override
public void onException(Throwable e) {
e.printStackTrace();
}
});
}
//sleep 一会儿
//由于采用的是异步发送,所以若这里不sleep,
//则消息还未发送就会将 producer 给关闭,报错
TimeUnit.SECONDS.sleep(3);
producer.shutdown();
}
}
定义单向消息发送生产者
public class OnewayProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("192.168.249.103:9876");
producer.start();
for (int i = 0; i < 100; i++) {
byte[] body = ("Hi," + i).getBytes();
Message message = new Message("single", "myTag", body);
//单向发送
producer.sendOneway(message);
}
producer.shutdown();
System.out.println("producer shutdown");
}
}
定义消息消费者
public class SomeConsumer {
public static void main(String[] args) throws Exception {
//定义一个pull 消费者
// DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("cg");
//定义一个 push 消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
//指定 nameServer
consumer.setNamesrvAddr("192.168.249.103:9876");
//指定从第一条消息开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//指定消费 topic 和 tag * 代表所有
consumer.subscribe("someTopic","*");
//指定采用“广播模式”进行消费,默认为 集群模式
// consumer.setMessageModel(MessageModel.BROADCASTING);
//匿名内部类方式
//注册消息监听器
// consumer.registerMessageListener(new MessageListenerConcurrently() {
//一旦broker中有了其订阅的消息就会触发该方法的执行
// 其返回值为当前consumer消费的状态
// @Override
// public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
//
// //逐条消费消息
// for (MessageExt msg : msgs) {
// System.out.println(msg);
// }
// //返回消费状态:消费成功
// return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
// }
// });
//lambda 表达式类型
consumer.registerMessageListener((MessageListenerConcurrently)(msgs,context) ->{
//逐条消费消息
for (MessageExt msg : msgs) {
System.out.println(msg);
}
//返回消费状态:消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
//开启消费者消费
consumer.start();
System.out.println("Consumer started");
}
}
4.2 顺序消息
4.2.1 什么是顺序消息
4.2.2 为什么需要顺序消息
4.2.3 有序性分类
4.2.4 代码举例
public class OrderProducer {
public static void main(String[] args) throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("192.168.249.103:9876");
producer.start();
for (int i = 0; i < 100; i++) {
//使用整型数作为 orderId
Integer orderId = i;
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("TopicA", "TagA", body);
//将orderId作为消息key
msg.setKeys(orderId.toString());
//send() 的第三个参数值 orderId 会传递给选择器的select()的第三个参数 arg
//使用的是同步发送
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
//使用消息 key 作为选择的选择算法
String keys = msg.getKeys();
Integer id = Integer.valueOf(keys);
//使用arg 作为选择算法
// Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.println(sendResult);
}
producer.shutdown();
}
}
4.3 延时消息
4.3.1 什么是延时消息
4.3.2 延时等级
4.3.3 延时消息实现原理
4.3.4 代码举例
DelayProducer
public class DelayProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("192.168.249.103:9876");
producer.start();
for (int i = 0; i < 100; i++) {
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("TopicB", "someTag", body);
//指定消息延迟等级为3 级,即延迟10s
msg.setDelayTimeLevel(3);
SendResult sendResult = producer.send(msg);
//输出消息被发送的时间
System.out.println(new SimpleDateFormat("mm:ss").format(new Date()));
System.out.println(","+sendResult);
}
producer.shutdown();
}
}
DelayConsumer
public class DelayConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
consumer.setNamesrvAddr("192.168.249.103:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("TopicB","*");
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
//输出消息被消费的时间
System.out.println(new SimpleDateFormat("mm:ss").format(new Date()));
System.out.println("," + msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
System.out.println("consumer started");
}
}
4.4 事务消息
4.4.1 问题引入
4.4.2 解决思路
4.4.3 基础知识
4.4.4 XA模式三剑客
4.4.5 XA模式架构
4.4.6 注意
4.4.7 代码举例
定义工行事务监听器
public class ICBCTransactionListener implements TransactionListener {
//回调操作方法
消息预提交成功就会触发该方法的执行,用于完成本地事务
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
System.out.println("预提交消息成功:" + msg);
//假设接收到TAGA的消息就表示扣款操作成功,TAGB的消息表示扣款失败,
// TAGC表示扣款结果不清楚,需要执行消息回查
if (StringUtils.equals("TAGA",msg.getTags())){
return LocalTransactionState.COMMIT_MESSAGE;
}else if (StringUtils.equals("TAGB",msg.getTags())){
return LocalTransactionState.ROLLBACK_MESSAGE;
}else if (StringUtils.equals("TAGC",msg.getTags())){
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.UNKNOW;
}
//消息回查方法
//引发消息回查的原因最常见的有两个:
//1):回调操作返回UNKONW;
//2):TC没有接收到TM的最终全局事务确认指令
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println("执行消息回查" + msg.getTags());
return LocalTransactionState.COMMIT_MESSAGE;
}
}
定义事务消息生产者
public class TransactionProducer {
public static void main(String[] args) throws MQClientException {
TransactionMQProducer producer = new TransactionMQProducer("tpg");
producer.setNamesrvAddr("192.168.249.103:9876");
/**
* * 定义一个线程池
* corePoolSize 线程池中核心线程数量
* maximumPoolSize 线程池中最多线程数
* keepAliveTime 当线程池中线程数量大于核心线程数量时,多余空闲线程的存活时长
* unit 时间单位
* workQueue 临时存放任务的队列,其参数就是队列的长度
* threadFactory 线程工厂
*
*/
ExecutorService executorService = new ThreadPoolExecutor(2, 5,
100, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2000),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
});
//为生产者指定一个线程池
producer.setExecutorService(executorService);
//为生产者添加事务监听器
producer.setTransactionListener(new ICBCTransactionListener());
producer.start();
String[] tags = {"TAGA","TAGB","TAGC"};
for (int i = 0; i < 3; i++) {
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("TTopic", tags[i], body);
//发送事务
//第二个参数用于指定在执行本地事务时要使用的业务参数
SendResult sendResult = producer.sendMessageInTransaction(msg,null);
System.out.println("发送结果为:" + sendResult.getSendStatus());
}
}
}
定义事务消息消费者
public class SomeConsumer {
public static void main(String[] args) throws Exception {
//定义一个pull 消费者
// DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("cg");
//定义一个 push 消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
//指定 nameServer
consumer.setNamesrvAddr("192.168.249.103:9876");
//指定从第一条消息开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//指定消费 topic 和 tag * 代表所有
consumer.subscribe("TTopic","*");
//指定采用“广播模式”进行消费,默认为 集群模式
// consumer.setMessageModel(MessageModel.BROADCASTING);
//注册消息监听器
// consumer.registerMessageListener(new MessageListenerConcurrently() {
//一旦broker中有了其订阅的消息就会触发该方法的执行
// 其返回值为当前consumer消费的状态
// @Override
// public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
//
// //逐条消费消息
// for (MessageExt msg : msgs) {
// System.out.println(msg);
// }
// //返回消费状态:消费成功
// return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
// }
// });
//lambda 表达式类型
consumer.registerMessageListener((MessageListenerConcurrently)(msgs,context) ->{
//逐条消费消息
for (MessageExt msg : msgs) {
System.out.println(msg);
}
//返回消费状态:消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
//开启消费者消费
consumer.start();
System.out.println("Consumer started");
}
}
4.5 批量消息
4.5.1 批量发送消息
4.5.2 批量消费消息
批量修改属性
4.5.3 代码举例
该批量发送的需求是,不修改最大发送4M的默认值,但要防止发送的批量消息超出4M的限制。
定义消息列表分割器
//消息列表分割器:其只会处理每条消息的大小不超过4M的情况。
//若存在某条消息,其本身消息大小大于 4M,这个分割器无法处理,
// 其直接将这条消息构成一个子列表返回,并没有再进行分割
public class MessageListSplitter implements Iterator<List<Message>> {
//指定极限值为 4M
private final int SIZE_LIMIT = 4 * 1024 * 1024;
//存放所有要发送的消息
private final List<Message> messages;
// 要进行批量发送消息的小集合起始索引
private int currIndex;
public MessageListSplitter(List<Message> messages){
this.messages = messages;
}
@Override
public boolean hasNext() {
//判断当前开始遍历的消息索引要小于消息总数
return currIndex < messages.size();
}
@Override
public List<Message> next() {
int nextIndex = currIndex;
//记录当前要发送的这一批次消息列表的大小
int totalSize = 0;
for (; nextIndex < messages.size();nextIndex ++){
//获取当前遍历的消息
Message message = messages.get(nextIndex);
//统计当前遍历的message 的大小
int tmpSize = message.getTopic().length() +
message.getBody().length;
Map<String,String> properties = message.getProperties();
for (Map.Entry<String,String> entry : properties.entrySet()){
tmpSize += entry.getKey().length() +
entry.getValue().length();
}
tmpSize = tmpSize + 20;
//判断当前消息本身是否大于 4M
if (tmpSize > SIZE_LIMIT){
if (nextIndex - currIndex == 0){
nextIndex ++;
}
break;
}
if (tmpSize + totalSize > SIZE_LIMIT){
break;
}else {
totalSize += tmpSize;
}
}
//获取当前message列表的子集合[currIndex, nextIndex0
List<Message> subList = messages.subList(currIndex,nextIndex);
//下次遍历的开始索引
currIndex = nextIndex ;
return subList;
}
}
定义批量消息生产者
public class BatchProducer {
public static void main(String[] args) throws MQClientException {
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("192.168.249.103:9876");
//指定要发送的消息的最大大小,默认是4M
// 不过,仅修改该属性是不行的,还需要同时修改broker加载的配置文件中的
// maxMessageSize 属性
// producer.setMaxMessageSize(8* 1024* 1024);
producer.start();
//定义要发送的消息的集合
List<Message> messages = new ArrayList<>();
for (int i = 0; i < 100; i++) {
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("someTopic1", "someTag1", body);
messages.add(msg);
}
//定义消息列表分割器 ,将消息列表分割成多个不超过4M 大小的小列表
MessageListSplitter splitter = new MessageListSplitter(messages);
while (splitter.hasNext()){
try {
List<Message> listItem = splitter.next();
producer.send(listItem);
} catch (Exception e) {
e.printStackTrace();
}
}
producer.shutdown();
}
}
定义批量消息消费者
public class BatchConsumer {
public static void main(String[] args) throws Exception {
//定义一个pull 消费者
// DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("cg");
//定义一个 push 消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
//指定 nameServer
consumer.setNamesrvAddr("192.168.249.103:9876");
//指定从第一条消息开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//指定消费 topic 和 tag * 代表所有
consumer.subscribe("someTopic", "*");
//指定每次可以消费10 条消息,默认为1
consumer.setConsumeMessageBatchMaxSize(10);
//指定每次可以从Broker 拉取 40 条消息,默认为 32
consumer.setPullBatchSize(40);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
System.out.println(msg);
}
//消费成功的返回结果
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
//消费异常的返回结果
// return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
consumer.start();
System.out.println("Consumer Started");
}
}
4.6 消息过滤
4.6.1 Tag过滤
4.6.2 SQL过滤
4.6.3 代码举例
定义Tag过滤生产者
public class FilterByTagProducer {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("192.168.249.103:9876");
producer.start();
//发送的消息均包含 Tag ,为以下三种Tag 之一
String[] tags = {"myTagA", "myTagB", "myTagC"};
for (int i = 0; i < 10; i++) {
byte[] body = ("hi" + i).getBytes();
String tag = tags[i % tags.length];
Message msg = new Message("myTopic", tag, body);
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
producer.shutdown();
}
}
定义Tag过滤消费者
public class FilterByTagConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
consumer.setNamesrvAddr("192.168.249.103:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//仅订阅Tag 为 myTopiA 和 myTagB的消息 ,不包含myTagC
consumer.subscribe("myTopic","myTopiA || myTagB");
consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
for (MessageExt mg:list){
System.out.println(mg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
System.out.println("Consumer Started");
}
}
定义SQL过滤生产者
public class FilterBySQLProducer {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("192.168.249.103:9876");
producer.start();
// String[] tags = {"myTagA","myTagB","myTagC"};
for (int i = 0; i < 10; i++) {
byte[] body = ("hi" + i ).getBytes();
// String tag = tags[i % tags.length];
Message msg = new Message("myTopic", "myTag", body);
//事先埋入用户属性 age
msg.putUserProperty("age",i + " ");
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
producer.shutdown();
}
}
定义SQL过滤消费者
public class FilterBySQLConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
consumer.setNamesrvAddr("192.168.249.103:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// consumer.subscribe("myTopic","myTopiA || myTagB");
//要从 myTopic的消息中过滤出age在 0-6之间的消息
consumer.subscribe("myTopic", MessageSelector.bySql("age between 0 and 6"));
consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
for (MessageExt mg : list) {
System.out.println(mg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
System.out.println("Consumer Started");
}
}
4.7 消息发送重试机制
4.7.1 说明
4.7.2 同步发送失败策略
4.7.3 异步发送失败策略
4.7.4 消息刷盘失败策略
4.8 消息消费重试机制
4.8.1 顺序消息的消费重试
4.8.2 无序消息的消费重试
4.8.3 消费重试次数间隔
4.8.4 重试队列
4.8.5 消费重试配置方式
4.8.6 消费不重试配置方式
4.9 死信队列
4.9.1 什么是死信队列
4.9.2 死信队列的特征
4.9.3 死信消息的处理
总结
该课程,怎么说呢,有点属于是偏理论方式讲解,此外,老师可能没有备课,所以听着感觉一顿一顿的,希望后续有更好的教程出来吧。