RocketMQ
- 一:RocketMQ概述
- 二:单击RockerMq的安装与部署
- 三: 磁盘阵列RAID
- 四:集群的搭建
- 五:Rocketmq的工作原理
- 五:Rocketmq应用
- 六:Springboot整合RockerMq
一:RocketMQ概述
- RocketMQ是一个统一消息引擎、轻量级数据处理平台。
- RocketMQ是⼀款阿⾥巴巴开源的消息中间件。2016年11⽉28⽇,阿⾥巴巴向 Apache 软件基⾦会捐赠RocketMQ,成为 Apache 孵化项⽬。2017 年 9 ⽉ 25 ⽇,Apache 宣布 RocketMQ孵化成为 Apache 顶
级项⽬(TLP ),成为国内⾸个互联⽹中间件在 Apache 上的顶级项⽬。
官⽹地址:http://rocketmq.apache.org
1.1 基本概念
- 消息(message)
- 消息是指,消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。
- 主题(Topic)
- Topic表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行
消息订阅的基本单位。 topic:message 1:n message:topic 1:1 - 一个生产者可以同时发送多种Topic的消息;而一个消费者只对某种特定的Topic感兴趣,即只可以订阅
和消费一种Topic的消息。 producer:topic 1:n consumer:topic 1:1
- 标签(Tag)
- 为消息设置的标签,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业
务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提
供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。
- 队列(Queue)
- 存储消息的物理实体。一个Topic中可以包含多个Queue,每个Queue中存放的就是该Topic的消息。一
个Topic的Queue也被称为一个Topic中消息的分区(Partition)。 - 一个Topic的Queue中的消息只能被一个消费者组中的一个消费者消费。一个Queue中的消息不允许同
一个消费者组中的多个消费者同时消费。
- 在学习参考其它相关资料时,还会看到一个概念:分片(Sharding)。分片不同于分区。在RocketMQ
中,分片指的是存放相应Topic的Broker。每个分片中会创建出相应数量的分区,即Queue,每个
Queue的大小都是相同的
- 消息标识(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:由用户指定的业务相关的唯一标识
1.2 系统架构
1 Producer
- 消息生产者,负责生产消息。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
- RocketMQ中的消息生产者都是以生产者组(Producer Group)的形式出现的。生产者组是同一类生产
者的集合,这类Producer发送相同Topic类型的消息。一个生产者组可以同时发送多个主题的消息。
- Consumer
消息消费者,负责消费消息。一个消息消费者会从Broker服务器中获取到消息,并对消息进行相关业务
处理。
- RocketMQ中的消息消费者都是以消费者组(Consumer Group)的形式出现的。消费者组是同一类消费者的集合,这类Consumer消费的是同一个Topic类型的消息。消费者组使得在消息消费方面,实现负载均衡(将一个Topic中的不同的Queue平均分配给同一个Consumer Group的不同的Consumer,注意,并不是将消息负载均衡)和容错(一个Consmer挂了,该Consumer Group中的其它Consumer可以接着消费原Consumer消费的Queue)的目标变得非常容易
- 消费者组中Consumer的数量应该小于等于订阅Topic的Queue数量。如果超出Queue数量,则多出的
Consumer将不能消费消息。
- 不过,一个Topic类型的消息可以被多个消费者组同时消费。
1.3 NameServer
- 功能介绍
NameServer是一个Broker与Topic的注册中心,支持Broker的注册与发现
Rockermq的思想来源于kafka,而kafka是依赖于zookeeper的,所以在rokermq的早期版本中,是依赖于zk的,从3.0开始就祛除了,而是使用自己的NameServer,主要包含两个功能
- Broker的管理:接受Broker的集群信息,并保存下来作为路由的基本数据,提供心跳检测机制,检查Broker是否存活
- **路由信息管理:**每个NameServer中都保存的Broker集群的,整个路由信息和用于客户端查询的队列信息,producer和conumser通过NameServer获得整个Broker集群的路由信息,进而实现消息的投递
1.3.1 路由注册
- NameServer通常也是以集群方式部署的,不过NameServer是无状态的,既NameServer中各个节点是无差异的,各个节点相互不进行消息通讯,那各个节点的数据是如何进行同步的呢?在Broker节点启动的时候,会轮训NameServer列表,与每个NameServer建立长连接,发起请求注册,在NameServer中内部有个Broker列表,用来动态的存储Broker的信息
注意,这是与其它像zk、Eureka、Nacos等注册中心不同的地方。
这种NameServer的无状态方式,有什么优缺点:
优点:NameServer集群搭建简单,扩容简单。
缺点:对于Broker,必须明确指出所有NameServer地址。否则未指出的将不会去注册。也正因
为如此,NameServer并不能随便扩容。因为,若Broker不重新配置,新增的NameServer对于
Broker来说是不可见的,其不会向这个NameServer进行注册。
- Broker上的节点为了证明自己还活着,为了维护与NameNode的连接,会将最新消息以心跳包的方式发送给NameServer,每30s发送一次心跳,心跳之中包含Broker,Broker的地址,Broker的名称,Broker所属的集群名,NameNode在收到心跳包之后,会更新心跳时间戳,记录这Broker的存货时间
1.3.2 路由剔除
- 由于Broker关机,宕机,等抖动原因,NameServer没有收到Broker的心跳,NameServer可能会将其从Broker的列表中剔除
- NameServer有一个定时任务,每隔10s就会扫描一次Borker表,查看每一个Broker的最新心跳时间,戳距离当前时间是都超过了120s,超过120s,就会判定Broker失效,从而进行剔除
扩展:对于RocketMQ日常运维工作,例如Broker升级,需要停掉Broker的工作。OP需要怎么
做?
OP需要将Broker的读写权限禁掉。一旦client(Consumer或Producer)向broker发送请求,都会收
到broker的NO_PERMISSION响应,然后client会进行对其它Broker的重试。
当OP观察到这个Broker没有流量后,再关闭它,实现Broker从NameServer的移除。
1.3.4 路由发现
- RockerMQ的路由发现采用的是pull模型,当Topic的路由信息发生变化的时候,NameServer不会主动的发送个客户端,而是客户端定时的拉取最新内容,默认是30s
扩展:
1)Push模型:推送模型。其实时性较好,是一个“发布-订阅”模型,需要维护一个长连接。而
长连接的维护是需要资源成本的。该模型适合于的场景:
实时性要求较高
Client数量不多,Server数据变化较频繁
2)Pull模型:拉取模型。存在的问题是,实时性较差。
3)Long Polling模型:长轮询模型。其是对Push与Pull模型的整合,充分利用了这两种模型的优
势,屏蔽了它们的劣势。
1.3.5 客户端NameServer选择策略
- 客户端在配置的时候必须要写上NameServer的集群地址,那么客户端到底是连接哪一个NameServer的节点呢,客户端首先会产生一个随机数,然后再与NameServer的节点数取模,此时得到的就是所要连接的节点的索引,然后再进行连接,如果连接失败,则会采用round-robin策略,逐个尝试去连接其他的节点
- 首先采用的是随机策略,然后采用的是轮询策略
扩展:Zookeeper Client是如何选择Zookeeper Server的?
简单来说就是,经过两次Shufæe,然后选择第一台Zookeeper Server。
详细说就是,将配置文件中的zk server地址进行第一次shufæe,然后随机选择一个。这个选择出
的一般都是一个hostname。然后获取到该hostname对应的所有ip,再对这些ip进行第二次
shufæe,从shufæe过的结果中取第一个server地址进行连接。
1.4 Broker
- Broker充当的是消息的中专角色,负责存储消息,转发消息,Broker在Rockermq系统中负责接受并存储,从生产者发送而来的数据同时为消费者的拉取请求做准备,Broker同时也存储的消息相关的元数据,包括消费者消息进度的偏移,主题,队列等,
- Remoting Module:整个Broker的实体,负责处理来自Clients的请求,而这个Broker的实体则右以下几个模块构成
- Client Manager:客户端管理器,负责接受,解析客户端(produre/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。
1.5 执行流程
- 启动NameServer,NameServer启动后开始监听端口,等待Broker,producer,consumer连接
- 启动Broker时,Broker会和所有的NameServer建立长连接,然后每30s发送心跳包
- 发送消息之前,可以先创建Tocpic,创建Tocpic之前需要指定该Tocpic需要存储在哪个Broker上,当然在创建Tocpic时也会将Tocpic的和Broker的关系写入到NameService中,当然这步是可选的,也可以在发送消息的时候自动创建Tocpic,
- Produce发送消息,首先会先和NameServer中的一台建立长连接,并从NameServer中获取路由信息,既当前发送的Topic消息的queue与Broker的地址映射关系,然后根据算法策略从对选择一个Queue,与队列所在的Broker建立长连接,从而向Broker发送消息,当然在获取路由信息后,Produce首先会将路由的信息缓存到本地,在每30s从NameServer更新一次信息
- Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取其所订阅Topic的路由信息,
然后根据算法策略从路由信息中获取到其所要消费的Queue,然后直接跟Broker建立长连接,开始消费其中的消息。Consumer在获取到路由信息后,同样也会每30秒从NameServer更新一次路由信息。不过不同于Producer的是,Consumer还会向Broker发送心跳,以确保Broker的存活状态。
1.5.1 Topic的创建模式
手动创建Topic有两种模式:
- 集群模式: 该模式创建的Topic会在该集群中,所有Broker的queue数量是相同的
- Broker模式:该模式下创建的Topic会在集群中,每个Broker的数量是不相同的
自动创建Topic时,默认采用的是Broker模式,会为每个Broker默认创建4个Queue。
1.5.2 读/写队列
从物理上来讲,读/写队列是同一个队列。所以,不存在读/写队列数据同步问题。读/写队列是逻辑上进
行区分的概念。一般情况下,读/写队列数量是相同的。
例如,创建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表示读写。
二:单击RockerMq的安装与部署
官网下载地址:https://rocketmq.apache.org/dowloading/releases/
- 在这个目录下,将下载的压缩包放进来
- 我这里本身就有jdk的环境就不配置了
使用unzip解压一下文件
2.1 修改初始化内存
- 设置的内存太大,需要调小点
- 进入到bin目录
- 我们需要修改这两个
vim runserver.sh - 因为他这个占用内存太大了是启动不了的
vim runbroker.sh
2.1 启动
https://rocketmq.apache.org/docs/quickStart/02quickstart
- 首先启动NameServer
- 启动成功
- 然后启动Broker
- 我的版本是4.9的,官网的是5.0的启动,所以会稍有不同
nohup sh bin/mqbroker -n localhost:9876 &
tail -f ~/logs/rocketmqlogs/broker.log
-
看日志的时候可能会出错,因为这个内存不足的问题,这个先无视,最起码启动成功了
-
Broker启动会和NameServer建立长连接,定时发送心跳
-
启动成功
-
启动流程是:先启动NameServer然后NameServer会监听Broker,producer,consumer连接
2.2 发送消息测试
- 消费者发送消息首先去连接NameServer去建立长连接,然后去获取路由信息,然后和Broker建立长连接,去发送消息到Broker中
- 之前说过可手动的创建Topic,也可以自动的创建Topic。自动的创建Topic的个数为4个
- 自动创建Topic时,默认采用的是Broker模式,会为每个Broker默认创建4个Queue。
创建环境变量,执行:export NAMESRV_ADDR=localhost:9876
执行: sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer是发送消息
执行sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer是消费数据
- 关闭:
sh bin/mqshutdown broker
sh bin/mqshutdown namesrv
- 先关闭Broker在关闭NameServer
2.3 可视化页面下载
https://github.com/apache/rocketmq-externals/tags
-
zip下载
解压
-
这是一个Springboot的项目,我们需要修改一下端口号,在application.properties文件中
-
默认是8080,改成7000
! 然后发现需要配置连接Broker的地址 -
我们的客户端和服务端都需要连接Broker的地址,那么这里也要配置
-
虚拟机运行的ip+端口号
-
在pom文件中新增
<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>
- 不增加会报错
- 打包
- 然后再终端通过java -jar运行
- 浏览器访问:http://127.0.0.1:7000/
- 查看消息
- 点进去一个就可以看到
2.4 数据复制与刷盘策略
- 复制策略
- 复制策略是指master的数据复制到slave中,形成数据的备份,分为同步和异步
- 同步复制:消息写入到Master后,master会等待数据同步完成后在向producer返回ACK
- 异步复制:消息写入到slaver后,master立即想producer返回ACK,无需等待数据同步完成
- 异步复制会降低写入延迟,RT变小,提高系统的吞吐量
- 刷盘策略
- 刷盘既指broker中的落盘当时,既数据从内存中持久化到磁盘中的方式,分为同步和异步
- 同步刷盘:当消息持久化到Broker磁盘中才算写入成功
- 异步刷盘:当消息写入到Broker的内存中算写入成功,无需等待消息持久化到磁盘中
异步刷盘策略会降低系统的写入延迟,RT变小,提高了系统的吞吐量
消息写入到了Broker的内存中,一般是写入到了pageCache中
对于异步刷盘而言消息写入到pageCache中会立即返回ACK,但并不会立即做落盘操作,而是等待pageCache达到了一定的数量进行落盘操作
2.5 2 Broker集群模式
- 根据Broker集群中各个节点间关系的不同,Broker集群可以分为以下几类:
2.5.1 单Master
单Broker模式(其本质上不能称为是集群),这种不能用于生产环境
2.5.2 多Master
Boker集群由多个Master组成,不存在Slave,同一个Topic的各个queue会均匀的分布在各个Master节点上
- 优点:配置简单:单个Master宕机对数据没有影响,前提是配置了RAID10磁盘阵列,既是机器宕机不可恢复的情况下,由于RAID阵列非常的可靠,消息不会丢失(异步刷盘丢失少量的数据,同步不会丢失)
- 缺点:单台机器宕机这台机器未被消费的消息在机器恢复之前不可订阅(不可消费),消息实时性差
2.5.3 多Master多Slave模式-异步复制
- Broker集群由多个Master组成,每个Maser又配置了Slave,(在配置了RAID的情况下,一个Master一个Slave即可)Master与Slaver的关系是主备关系,既Master负责处理读写请求,而Slave负责消息的备份和主Master宕机之后身份的转变
- 异步复制,既消息写入到Master之后,Master立即想producer返回ACK,无需等待Slaver数据同步成功
- 当Master宕机之后Slaver会变为Master,不过由于Master同Slave同步的消息有短暂的延迟,所以当Master宕机之后,这种异步复制的方式可能会造成少量的丢失
Slave同Master同步数据,延迟越短其丢失的消息就越少
对于Master的RAID来说,若使用的也是异步复制策略,同样也存在延迟问题,同样也能会丢失消息,但是RAID阵列的秘诀是微妙级,所以丢失的消息更少
2.5.4 多Master多Slave模式-同步双写
- 该模式是多Master多Slave模式的同步复制实现。所谓同步双写,指的是消息写入master成功后,
master会等待slave同步数据成功后才向producer返回成功ACK,即master与slave都要写入成功后才会
返回成功ACK,也即双写。
该模式与异步复制模式相比,优点是消息的安全性更高,不存在消息丢失的情况。但单个消息的RT略
高,从而导致性能要略低(大约低10%)。
该模式存在一个大的问题:对于目前的版本,Master宕机后,Slave不会自动切换到Master。
2.5.5 最佳实践
- 一般来说是为Master配置上RAID,然后再为其配置一个Slave,既利用了 RAID的的高效安全性,又解决可消息可能会影响订阅的问题
RAID磁盘阵列的效率要高于Maser-Slave。因为RAID是硬件支持的,也正因如从RAID的搭建成本比较高
多Master+RAID和多Master+多Slave的区别是什么?
多Master+RAID,其仅仅可以保证数据不丢失,即不影响消息写入,但其可能会影响到
消息的订阅。但其执行效率要远高于多Master多Slave集群
多Master多Slave集群,其不仅可以保证数据不丢失,也不会影响消息写入。其运行效率要低
于多Master+RAID阵
三: 磁盘阵列RAID
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 等级,以及具体的实现方式。
关键技术
镜像技术
-
镜像技术是一种冗余技术,为磁盘提供数据备份功能,防止磁盘发生故障而造成数据丢失。对于 RAID
而言,采用镜像技术最典型地的用法就是,同时在磁盘阵列中产生两个完全相同的数据副本,并且分布
在两个不同的磁盘上。镜像提供了完全的数据冗余能力,当一个数据副本失效不可用时,外部系统仍可
正常访问另一副本,不会对应用系统运行和性能产生影响。而且,镜像不需要额外的计算和校验,故障
修复非常快,直接复制即可。镜像技术可以从多个副本进行并发读取数据,提供更高的读 I/O 性能,但
不能并行写数据,写多个副本通常会导致一定的 I/O 性能下降。 -
镜像技术提供了非常高的数据安全性,其代价也是非常昂贵的,需要至少双倍的存储空间。高成本限制
了镜像的广泛应用,主要应用于至关重要的数据保护,这种场合下的数据丢失可能会造成非常巨大的损
失。
数据条带技术 -
数据条带化技术是一种自动将 I/O操作负载均衡到多个物理磁盘上的技术。更具体地说就是,将一块连
续的数据分成很多小部分并把它们分别存储到不同磁盘上。这就能使多个进程可以并发访问数据的多个
不同部分,从而获得最大程度上的 I/O 并行能力,极大地提升性能。
数据校验技术 -
数据校验技术是指, RAID 要在写入数据的同时进行校验计算,并将得到的校验数据存储在 RAID 成员
磁盘中。校验数据可以集中保存在某个磁盘或分散存储在多个不同磁盘中。当其中一部分数据出错时,
就可以对剩余数据和校验数据进行反校验计算重建丢失的数据。 -
数据校验技术相对于镜像技术的优势在于节省大量开销,但由于每次数据读写都要进行大量的校验运
算,对计算机的运算速度要求很高,且必须使用硬件 RAID 控制器。在数据重建恢复方面,检验技术比
镜像技术复杂得多且慢得多
RAID分类 -
从实现角度看, RAID 主要分为软 RAID、硬 RAID 以及混合 RAID 三种。
软 RAID
所有功能均有操作系统和 CPU 来完成,没有独立的 RAID 控制处理芯片和 I/O 处理芯片,效率自然最
低。
硬 RAID
配备了专门的 RAID 控制处理芯片和 I/O 处理芯片以及阵列缓冲,不占用 CPU 资源。效率很高,但成
本也很高。
混合 RAID
具备 RAID 控制处理芯片,但没有专门的I/O 处理芯片,需要 CPU 和驱动程序来完成。性能和成本在软
RAID 和硬 RAID 之间。
3.1 常见RAID等级
- JBOD ,Just a Bunch of Disks,磁盘簇。表示一个没有控制软件提供协调控制的磁盘集合,这是 RAID
区别与 JBOD 的主要因素。 JBOD 将多个物理磁盘串联起来,提供一个巨大的逻辑磁盘。 - JBOD 的数据存放机制是由第一块磁盘开始按顺序往后存储,当前磁盘存储空间用完后,再依次往后面
的磁盘存储数据。 JBOD 存储性能完全等同于单块磁盘,而且也不提供数据安全保护。 - JBOD 常指磁盘柜,而不论其是否提供 RAID 功能。不过,JBOD并非官方术语,官方称为Spanning。
RAID0
- RAID0 是一种简单的、无数据校验的数据条带化技术。实际上不是一种真正的 RAID ,因为它并不提
供任何形式的冗余策略。 RAID0 将所在磁盘条带化后组成大容量的存储空间,将数据分散存储在所有
磁盘中,以独立访问方式实现多块磁盘的并读访问。 - 理论上讲,一个由 n 块磁盘组成的 RAID0 ,它的读写性能是单个磁盘性能的 n 倍,但由于总线带宽等
多种因素的限制,实际的性能提升低于理论值。由于可以并发执行 I/O 操作,总线带宽得到充分利用。
再加上不需要进行数据校验,RAID0 的性能在所有 RAID 等级中是最高的。 - RAID0 具有低成本、高读写性能、 100% 的高存储空间利用率等优点,但是它不提供数据冗余保护,一
旦数据损坏,将无法恢复。
应用场景:对数据的顺序读写要求不高,对数据的安全性和可靠性要求不高,但对系统性能要求很高的
场景。
RAID0与JBOD相同点:
1)存储容量:都是成员磁盘容量总和
2)磁盘利用率,都是100%,即都没有做任何的数据冗余备份
RAID0与JBOD不同点:
JBOD:数据是顺序存放的,一个磁盘存满后才会开始存放到下一个磁盘
RAID:各个磁盘中的数据写入是并行的,是通过数据条带技术写入的。其读写性能是JBOD的n
倍
RAID1
- RAID1 就是一种镜像技术,它将数据完全一致地分别写到工作磁盘和镜像磁盘,它的磁盘空间利用率
为 50% 。 RAID1 在数据写入时,响应时间会有所影响,但是读数据的时候没有影响。 RAID1 提供了
最佳的数据保护,一旦工作磁盘发生故障,系统将自动切换到镜像磁盘,不会影响使用。 - RAID1是为了增强数据安全性使两块磁盘数据呈现完全镜像,从而达到安全性好、技术简单、管理方
便。 RAID1 拥有完全容错的能力,但实现成本高。 - 应用场景:对顺序读写性能要求较高,或对数据安全性要求较高的场景。
RAID10
- RAID10是一个RAID1与RAID0的组合体,所以它继承了RAID0的快速和RAID1的安全。
简单来说就是,先做条带,再做镜像。发即将进来的数据先分散到不同的磁盘,再将磁盘中的数据做
镜像。
RAID01
四:集群的搭建
这里要搭建一个双主双从异步复制的Broker集群。为了方便,这里使用了两台主机来完成集群的搭建。
102服务器是 NameServer + Broker Master1 + Slave2
103服务器是 NameServer + Broker Master2 + Slave1
克隆单击点主机,并修改配置。(修改完内存)。
然后进入到Config目录
- 进入 cd 2m-2s-async/
- 首先修改vim broker-a.properties 文件,为Master1
# 指定整个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.116.132:9876;192.168.116.133:9876
- 修改vim broker-b-s.properties文件为Slave2
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.116.132:9876;192.168.116.133: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
- 然后配置103服务器
- 先修改内存
- 修改Slave1文件
vim broker-a-s.properties
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=1
deleteWhen=04
fileReservedTime=48
brokerRole=SLAVE
flushDiskType=ASYNC_FLUSH
namesrvAddr=192.168.116.132:9876;192.168.116.133: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
- 修改 vim broker-b.properties
brokerClusterName=DefaultCluster
brokerName=broker-b
brokerId=0
deleteWhen=04
fileReservedTime=48
brokerRole=ASYNC_MASTER
flushDiskType=ASYNC_FLUSH
namesrvAddr=192.168.116.132:9876;192.168.116.133:9876
4.1 集群的启动
首先102和103启动NameServer一样的命令
nohup sh bin/mqnamesrv &
tail -f ~/logs/rocketmqlogs/namesrv.log
- 102 执行
- 第一行命令是启动我们的Master1
- 第二行是启动Slave2
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a.properties &
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b-s.properties &
- 103上执行
- 第一行命令是启动我们的Master2
- 第二行是启动Slave1
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b.properties &
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a-s.properties &
- 小细节
- 修改,重新打包,运行Jar包
五:Rocketmq的工作原理
5.1 消息的生产过程
5.1.1 消息的生产过程
- producer将消息写入,产生了如下
- 首先连接NameServer获取其中的
消息Topic
的路有信息 - NameServer返回该Topic中的
路由信息和Broker队列
- produce会根据代码中指定的Queue选择策略,从Queue列表中选择一个队列,用于后续的消息存储
- Produce会对消息做一些特殊的处理,比如超4m对消息进行压缩
- produce想选择的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。
5.5.2 消息的选择策略
对于无序消息,其Queue选择算法,也称为消息投递算法,常见的有两种:
轮询算法
默认选择算法。该算法保证了每个Queue中可以均匀的获取到消息。
该算法存在一个问题:由于某些原因,在某些Broker上的Queue可能投递延迟较严重。从而导致
Producer的缓存队列中出现较大的消息积压,影响消息的投递性能。
最小投递延迟算法
该算法会统计每次消息投递的时间延迟,然后根据统计出的结果将消息投递到时间延迟最小的Queue。
如果延迟相同,则采用轮询算法投递。该算法可以有效提升消息的投递性能。
该算法也存在一个问题:消息在Queue上的分配不均匀。投递延迟小的Queue其可能会存在大量
的消息。而对该Queue的消费者压力会增大,降低消息的消费能力,可能会导致MQ中消息的堆
积。
5.2 消息的存储
-消息是默认存储在本地文件store下的
- 因为我的集群没有发送消息,所以就用之前单节点集群的图
5.2.1 store目录介绍
- abort:该文件会在broker运行的时候创建,正常关闭Broker,文件会自动消失,若没有启动Broker的情况下,文件还存在,说明Broker是非正常关闭的
- checkpoint 其中存放的commitlog,consumequeue,index文件的最后刷盘时间
- commitlog:其中存放的commitlog文件,其消息就是写在comitlog的
- config:存放的是broker运行的时候一些配置
- consumequeue:里面存放的是conumequeue文件,队列就放在这个目录中
- index:其中存放的消息的索引文件
- lock:运行期间使用到的全局资源锁
5.2.2 commitlog
- 目录与文件
- 在很多资料中commitlog目录中的文件简单就称为commitlog文件。但在源码中,该文件
被命名为mappedFile。
- commitlog目录中存放着很多的mapperFile文件,当前Broker的所有消息都是落盘位到mapperFile文件中的,mapperFile的起始文件是1G,文件名由20位十进制数构成,表示
当前文件的第一条消息的起始位移偏移量。
第一个文件名是20个0构成的,因为第一个文件的第一条的消息偏移量commitlog offset为0,
当第一个文件放满了之后,则会生成第二个文件继续存放数据,假设第一个文件的大小是1073741820字节(1G = 1073741824字节),则第二个文件名就是00000000001073741820,
以此类推,第n个文件名应该是前n-1个文件大小之和。
一个Broker中所有mappedFile文件的commitlog offset是连续的
需要之一的是一个broker之仅包含一个commitlog,所有的mapperFile,文件都是存放在该目录的,既无论当前Broker存放的多少Topic消息,这些消息都被顺序的写入到mapperFile中,,也就是说这些文件并没有按照Topic进行分类存放
mappedFile文件是顺序读写的文件,所有其访问效率很高
无论是SSD磁盘还是SATA磁盘,通常情况下,顺序存取效率都会高于随机存取
- commitlog的消息单元
- mappedFile文件内容由一个个的消息单元构成。每个消息单元中包含消息总长度MsgLen、消息的物理
位置physicalOffset、消息体内容Body、消息体长度BodyLength、消息主题Topic、Topic长度
TopicLength、消息生产者BornHost、消息发送时间戳BornTimestamp、消息所在的队列QueueId、消
息在Queue中存储的偏移量QueueOffset等近20余项消息相关属性
一个mappedFile文件中第m+1个消息单元的commitlog offset偏移量
L(m+1) = L(m) + MsgLen(m) (m >= 0)
5.2.2 consumequeuq
- 文件里面是存放的是我们的Topic,topic里面存放的是该Topic对应的queueid,queueid里面的文件,对应的是Broker里面存放的数据的索引,可以通过索引去寻找到对应的消息的数据
- 为了提高效率,会为每个Topic在~/store/consumequeue中创建一个目录,目录名为Topic名称。在该
Topic目录下,会再为每个该Topic的Queue建立一个目录,目录名为queueId。每个目录中存放着若干
consumequeue文件,consumequeue文件是commitlog的索引文件,可以根据consumequeue定位到具
体的消息。
consumequeue文件名也由20位数字构成,表示当前文件的第一个索引条目的起始位移偏移量。与
mappedFile文件名不同的是,其后续文件名是固定的。因为consumequeue文件大小是固定不变的。
索引条目
- 每个consumequeue文件可以包含30w个索引条目,每个索引条目包含了三个消息重要属性:消息在
mappedFile文件中的偏移量CommitLog Offset、消息长度、消息Tag的hashcode值。这三个属性占20
个字节,所以每个文件的大小是固定的30w * 20字节。
一个consumequeue文件中所有消息的Topic一定是相同的。但每条消息的Tag可能是不同的。
对文件的读写
- 消息的写入
- Broker根据queueid获取到该消息对应的索引条目主要在consumequeuq目录中写入偏移量,即Queueoffset
- 将queueID,queueoffset等一期封装为消息单元,然后写入
- 将消息单元写入到commitlog中,同时形成消息的索引条目,
- 将消息的索引条目分发到不同的consumequeuq中
消息的拉取
- 当Consumer来拉取消息时会经历以下几个步骤:
- consumer获取到要消费的数据所在的偏移量,计算出要消费信息的offerSet
消费offset即消费进度,consumer对某个Queue的消费offset,即消费到了该Queue的第几
条消息
消息offset = 消费offset + 1
- cousumer向broker发送拉取信息,其中会包含其要拉取消息的Queue、消息offset及消息Tag。
- Broker计算在该consumequeue中的queueOffset
- 从该queueOffset处开始向后查找第一个指定Tag的索引条目。
- 解析该索引条目的前8个字节,即可定位到该消息在commitlog中的commitlog offset
- 从对应commitlog offset中读取消息单元,并发送给Consumer
5.2.3 性能优化
RocketMQ中,无论是消息本身还是消息索引,都是存储在磁盘上的。其不会影响消息的消费吗?当然
不会。其实RocketMQ的性能在目前的MQ产品中性能是非常高的。因为系统通过一系列相关机制大大
提升了性能。
首先,RocketMQ对文件的读写操作是通过mmap零拷贝进行的,将对文件的操作转化为直接对内存地
址进行操作,从而极大地提高了文件的读写效率。
其次,consumequeue中的数据是顺序存放的,还引入了PageCache的预读取机制,使得对
consumequeue文件的读取几乎接近于内存读取,即使在有消息堆积情况下也不会影响性能。
- PageCache机制,页缓存机制,是OS对文件的缓存机制,用于加速对文件的读写操作。一般来
说,程序对文件进行顺序读写的速度几乎接近于内存读写速度,主要原因是由于OS使用
PageCache机制对读写访问操作进行性能优化,将一部分的内存用作PageCache。
写操作:OS会先将数据写入到PageCache中,随后会以异步方式由pdæush(page dirty æush)
内核线程将Cache中的数据刷盘到物理磁盘
读操作:若用户要读取数据,其首先会从PageCache中读取,若没有命中,则OS在从物理磁
盘上加载该数据到PageCache的同时,也会顺序对其相邻数据块中的数据进行预读取。 - RocketMQ中可能会影响性能的是对commitlog文件的读取。因为对commitlog文件来说,读取消息时
会产生大量的随机访问,而随机访问会严重影响性能。不过,如果选择合适的系统IO调度算法,比如
设置调度算法为Deadline(采用SSD固态硬盘的话),随机读的性能也会有所提升。
*indexFile
5.4 消息的消费
- 消费者从Broker中获取消息的方式有两种:pull拉取方式和push推动方式。消费者组对于消息消费的模
式又分为两种:集群消费Clustering和广播消费Broadcasting。 - 拉取消费:
- 客户端主动的去消费者拉取消息,主动权由客户端控制,一旦获取了批量消息,就会启动消费进程,不过这种的实时性比较差,既Broker中有了新的消费并不能及时消费
由于拉取时间间隔是由用户指定的,所以在设置该间隔时需要注意平稳:间隔太短,空请求比
例会增加;间隔太长,消息的实时性太差
- 推送式消费:
- 该模式写Broker收到的数据会主动的推送给consumer,该方法的实时性比较高
- 该获取方式是典型的
发布订阅
模式,既consumer向其关联的queue注册了监听器,一旦有了新的消息到来就会触发回调方法,回调方法是consumer中去queue中去主动的拉取消息,而这些都是基于consumer对broker的长连接,长连接是需要占用系统的资源的 - 对比
- pull:需要应用去实现关联queue的遍历,实时性差,用于应用消息的拉取
- push:封装了对关联queue的遍历,实时性强,但是会占用较多的系统资源
5.4.1 消费模式
- 广播模式
- 广播消费模式下,相同Consumer Group的每个Consumer实例都接收同一个Topic的全量消息。即每条
消息都会被发送到Consumer Group中的每个Consumer。
集群模式
- 集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊同一个Topic的消息。即每条消
息只会被发送到Consumer Group中的某个Consumer
消费的进度保存
- 广播模式:消费进度保存在consumer端。因为广播模式下consumer group中每个consumer都会
消费所有消息,但它们的消费进度是不同。所以consumer各自保存各自的消费进度。 - 集群模式:消费进度保存在broker中。consumer group中的所有consumer共同消费同一个Topic
中的消息,同一条消息只会被消费一次。消费进度会参与到了消费的负载均衡中,故消费进度是
需要共享的。下图是broker中存放的各个Topic的各个Queue的消费进度。
5.4.2 Rebalance
rebalance的机制的前提是集群消费
-
rebalance指的是在均衡,指的是一个topic下的多个Queue,在同一个consumer groub中多个consumer重新分配的过程
rebalance的机制是为了提高并行的消费效率,例如一个topic下的5个队列,在只要一个消费者的情况下,这个消费者在负责消费这5个队列的消息,此时如果我们增加一个消费者,那么就可以给其中一个消费者增加2个队列,另一个分3个队列,这样提高了消费并行的能力 -
rebalance限制
-
由于一个队列最多分给一个消费者,因此当每个消费者组下的消费者实际数量大于队列的数量,多余的消费者是分配不到任何的队列
-
reblance的危害
-
消费暂停
在只有一个consumer时,负责消费所有的队列,在增加了一个consumer时,会触发reblancae,此时原来的consumer就需要暂停部分的消费,等这些队列分配给新的consumer时,这些暂停的队列才能继续的消费 -
消费重复
consumer在重新分配给自己队列的时候,必须接着之前consumer提交的offerSet重新消费,然而默认情况下offerSet是异步的,这个异步会导致提交到的Broker的offerset与consumer的实际的offerset会发生不一致,这个不一致的差就会导致重复消费
同步提交:consumer提交了其消费完毕的一批offerSet给broker之后,需要等待broker成功的ACK,consumer才会继续消费下一批,这个过程会导致堵塞
异步提交:consumer提交了其消费完毕的一批消息的offset给broker后,不需要等待broker的成
功ACK。consumer可以直接获取并消费下一批消息。
对于一次性读取消息的数量,需要根据具体业务场景选择一个相对均衡的是很有必要的。因为
数量过大,系统性能提升了,但产生重复消费的消息数量可能会增加;数量过小,系统性能会
下降,但被重复消费的消息数量可能会减少
消费突刺
由于reblanace会导致重复消费,如果需要重复消费的消息过多,或者因为reblanace暂停的时间过长,导致了部分数据的堆积,那么可能会导致在结束之后需要消费很多的消息
5.4.3 Rebalance产生的原因
- 导致rebalnace的原因就两个:消费者订阅的topic的queue发生了变化,或消费者组中的数量发生变化
- Queue数量发生变化的场景:
- Broker的扩容和缩容
- Broker的升级的运维
- Broker和NameServer的网络异常
- Queue的扩容和缩容
- 消费者数量发生变化的场景:
- consumer Group的扩容和缩容
- consumer的运维
- consumer和NameServer间的网络异常
5.5 Queue分配算法
- 一个Topic中的Queue只能由Consumer Group中的一个Consumer进行消费,而一个Consumer可以同时消费多个Queue中的消息。那么Queue与Consumer间的配对关系是如何确定的,即Queue要分配给哪个Consumer进行消费,也是有算法策略的。常见的有四种策略。这些策略是通过在创建Consumer时的构造器传进去的。
- 该算法是要根据avg = QueueCount / ConsumerCount 的计算结果进行分配的。如果能够整除,
则按顺序将avg个Queue逐个分配Consumer;如果不能整除,则将多余出的Queue按照Consumer顺序逐个分配
环形平均策略
环形平均算法是指,根据消费者的顺序,依次在由queue队列组成的环形图中逐个分配。
一致性Hash算法
该算法会将consumer的hash值作为Node节点存放到hash环上,然后将queue的hash值也放到hash环
上,通过顺时针方向,距离queue最近的那个consumer就是该queue要分配的consumer。
- 同机房策略
- 该算法会根据queue的部署机房位置和consumer的位置,过滤出当前consumer相同机房的queue。然
后按照平均分配策略或环形平均策略对同机房queue进行分配。如果没有同机房queue,则按照平均分
配策略或环形平均策略对所有queue进行分配。
对比:
一致性Hash产生的问题:
两种平均分配策略的分配效率较高,一致性hash策略的较低。因为一致性hash算法较复杂。另外,一
致性hash策略分配的结果也很大可能上存在不平均的情况。
- 一致性Hash产生的意义
- 可以有效的减少由于消费者的扩容和缩容产生的Rebalace
一致性算法的使用场景
consumer算法比较频繁的场景
5.4.4 至少一次原则
RocketMQ有一个原则:每条消息必须要被成功消费一次。
那么什么是成功消费呢?Consumer在消费完消息后会向其消费进度记录器提交其消费消息的offset,
offset被成功记录到记录器中,那么这条消费就被成功消费了
什么是消费进度记录器?
对于广播消费模式来说,Consumer本身就是消费进度记录器。
对于集群消费模式来说,Broker是消费进度记录器
5.6 订阅关系的一致性
订阅关系的一致性指的是,同一个消费者组(Group ID相同)下所有Consumer实例所订阅的Topic与
Tag及对消息的处理逻辑必须完全一致。否则,消息消费的逻辑就会混乱,甚至导致消息丢失。
5.6.1 正确的订阅
5.6.2 错误的订阅关系
一个消费者组订阅了多个Topic,但是该消费者组里的多个Consumer实例的订阅关系并没有保持一致
订阅了不同Topic
该例中的错误在于,同一个消费者组中的两个Consumer实例订阅了不同的Topic。
Consumer实例1-1:(订阅了topic为jodie_test_A,tag为所有的消息)
Properties properties = new Properties();
properties.put(PropertyKeyConst.GROUP_ID, "GID_jodie_test_1");
Consumer consumer = ONSFactory.createConsumer(properties);
consumer.subscribe("jodie_test_A", "*", new MessageListener() {
public Action consume(Message message, ConsumeContext context) {
System.out.println(message.getMsgID());
return Action.CommitMessage;
}
});
Consumer实例1-2:(订阅了topic为jodie_test_B,tag为所有的消息)
Properties properties = new Properties();
properties.put(PropertyKeyConst.GROUP_ID, "GID_jodie_test_1");
Consumer consumer = ONSFactory.createConsumer(properties);
consumer.subscribe("jodie_test_B", "*", new MessageListener() {
public Action consume(Message message, ConsumeContext context) {
System.out.println(message.getMsgID());
return Action.CommitMessage;
}
});
订阅了不同Tag
该例中的错误在于,同一个消费者组中的两个Consumer订阅了相同Topic的不同Tag。
Consumer实例1-1:(订阅了topic为jodie_test_A,tag为所有的消息)
Properties properties = new Properties();
properties.put(PropertyKeyConst.GROUP_ID, "GID_jodie_test_2");
Consumer consumer = ONSFactory.createConsumer(properties);
consumer.subscribe("jodie_test_A", "TagA", new MessageListener() {
public Action consume(Message message, ConsumeContext context) {
System.out.println(message.getMsgID());
return Action.CommitMessage;
}
});
Consumer实例2-2:(订阅了topic为jodie_test_A,tag为所有的消息)
Properties properties = new Properties();
properties.put(PropertyKeyConst.GROUP_ID, "GID_jodie_test_2");
Consumer consumer = ONSFactory.createConsumer(properties);
consumer.subscribe("jodie_test_A", "*", new MessageListener() {
public Action consume(Message message, ConsumeContext context) {
System.out.println(message.getMsgID());
return Action.CommitMessage;
}
});
订阅不同数量的Topic
该例中的错误在于,同一个消费者组中的两个Consumer订阅了不同数量的Topic。
Consumer实例3-1:(该Consumer订阅了两个Topic)
Properties properties = new Properties();
properties.put(PropertyKeyConst.GROUP_ID, "GID_jodie_test_3");
Consumer consumer = ONSFactory.createConsumer(properties);
consumer.subscribe("jodie_test_A", "TagA", new MessageListener() {
public Action consume(Message message, ConsumeContext context) {
System.out.println(message.getMsgID());
return Action.CommitMessage;
}
});
consumer.subscribe("jodie_test_B", "TagB", new MessageListener() {
public Action consume(Message message, ConsumeContext context) {
System.out.println(message.getMsgID());
return Action.CommitMessage;
}
});
Consumer实例3-2:(该Consumer订阅了一个Topic)
Properties properties = new Properties();
properties.put(PropertyKeyConst.GROUP_ID, "GID_jodie_test_3");
Consumer consumer = ONSFactory.createConsumer(properties);
consumer.subscribe("jodie_test_A", "TagB", new MessageListener() {
public Action consume(Message message, ConsumeContext context) {
System.out.println(message.getMsgID());
return Action.CommitMessage;
}
});
5.7 offSet
- 消费进度offset是用来记录每个Queue的不同消费组的消费进度的。根据消费进度记录器的不同,可以分为两种模式:本地模式和远程模式。
5.7.1 offset本地管理模式
- 当消费模式为广播消费时,offset使用本地模式存储。因为每条消息会被所有的消费者消费,每个消费者管理自己的消费进度,各个消费者之间不存在消费进度的交集
- Consumer在广播消费模式下offset相关数据以json的形式持久化到Consumer本地磁盘文件中,默认文件路径为当前用户主目录下的.rocketmq_offsets/
c
l
i
e
n
t
I
d
/
{clientId}/
clientId/{group}/Offsets.json 。
其中 c l i e n t I d 为当前消费者 i d ,默认为 i p @ D E F A U L T ; {clientId}为当前消费者id,默认为ip@DEFAULT; clientId为当前消费者id,默认为ip@DEFAULT;{group}为消费者组名称。
5.7.2 offset远程管理模式
-
当消费模式为集群消费时,offset使用远程模式管理。因为所有Cosnumer实例对消息采用的是均衡消费,所有Consumer共享Queue的消费进度。
-
Consumer在集群消费模式下offset相关数据以json的形式持久化到Broker磁盘文件中,文件路径为当前用户主目录下的store/config/consumerOffset.json 。
-
Broker启动时会加载这个文件,并写入到一个双层Map(ConsumerOffsetManager)。外层map的key为topic@group,value为内层map。内层map的key为queueId,value为offset。当发生Rebalance时,新的Consumer会从该Map中获取到相应的数据来继续消费。集群模式下offset采用远程管理模式,主要是为了保证Rebalance机制。
5.7.3 offset用途
- 消费者是如何从最开始持续消费消息的?消费者要消费的第一条消息的起始位置是用户自己通过
consumer.setConsumeFromWhere()方法指定的。在Consumer启动后,其要消费的第一条消息的起始位置常用的有三种,这三种位置可以通过枚举类型常量设置。这个枚举类型为ConsumeFromWhere - 当消费完一批消息后,Consumer会提交其消费进度offset给Broker,Broker在收到消费进度后会将其更新到那个双层Map(ConsumerOffsetManager)及consumerOffset.json文件中,然后向该Consumer进行ACK,而ACK内容中包含三项数据:当前消费队列的最小offset(minOffset)、最大offset(maxOffset)、及下次消费的起始offset(nextBeginOffset)。
5.7.4 重试队列
- 当rocketMQ对消息的消费出现异常时,会将发生异常的消息的offset提交到Broker中的重试队列。系统
在发生消息消费异常时会为当前的topic@group创建一个重试队列,该队列以%RETRY%开头,到达重
试时间后进行消费重试。
5.7.5 offset的同步提交与异步提交
集群消费模式下,Consumer消费完消息后会向Broker提交消费进度offset,其提交方式分为两种:
同步提交
:消费者在消费完一批消息后会向broker提交这些消息的offset,然后等待broker的成功响
应。若在等待超时之前收到了成功响应,则继续读取下一批消息进行消费(从ACK中获取
nextBeginOffset)。若没有收到响应,则会重新提交,直到获取到响应。而在这个等待过程中,消费
者是阻塞的。其严重影响了消费者的吞吐量。
异步提交
·:消费者在消费完一批消息后向broker提交offset,但无需等待Broker的成功响应,可以继续
读取并消费下一批消息。这种方式增加了消费者的吞吐量。但需要注意,broker在收到提交的offset
后,还是会向消费者进行响应的。可能还没有收到ACK,此时Consumer会从Broker中直接获取
nextBeginOffset。
5.8 消息的幂等性
- 当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消
费并未对业务系统产生任何负面影响,那么这个消费过程就是消费幂等的。
幂等:若某操作执行多次与执行一次对系统产生的影响是相同的,则称该操作是幂等的。
-
在互联网应用中,尤其在网络不稳定的情况下,消息很有可能会出现重复发送或重复消费。如果重复的
消息可能会影响业务处理,那么就应该对消息做幂等处理。
消息重复的场景分析
发送时消息重复
当一条消息已被成功发送到Broker并完成持久化,此时出现了网络闪断,从而导致Broker对Producer应答失败。 如果此时Producer意识到消息发送失败并尝试再次发送消息,此时Broker中就可能会出现两条内容相同并且Message ID也相同的消息,那么后续Consumer就一定会消费两次该消息。
消费时消息重复 -
消息已投递到Consumer并完成业务处理,当Consumer给Broker反馈应答时网络闪断,Broker没有收到消费成功响应。为了保证消息至少被消费一次的原则,Broker将在网络恢复后再次尝试投递之前已被处理过的消息。此时消费者就会收到与之前处理过的内容相同、Message ID也相同的消息
Rebalance时消息重复
当Consumer Group中的Consumer数量发生变化时,或其订阅的Topic的Queue数量发生变化时,会触
发Rebalance,此时Consumer可能会收到曾经被消费过的消息。
通用解决方案
幂等解决方案的设计中涉及到两项要素:幂等令牌,与唯一性处理。只要充分利用好这两要素,就可以
设计出好的幂等解决方案。
幂等令牌:是生产者和消费者两者中的既定协议,通常指具备唯⼀业务标识的字符串。例如,订
单号、流水号。一般由Producer随着消息一同发送来的。
唯一性处理:服务端通过采用⼀定的算法策略,保证同⼀个业务逻辑不会被重复执行成功多次。
例如,对同一笔订单的多次支付操作,只会成功一次。
解决方案
对于常见的系统,幂等性操作的通用性解决方案是:
- 首先通过缓存去重。在缓存中如果已经存在了某幂等令牌,则说明本次操作是重复性操作;若缓
存没有命中,则进入下一步。 - 在唯一性处理之前,先在数据库中查询幂等令牌作为索引的数据是否存在。若存在,则说明本次
操作为重复性操作;若不存在,则进入下一步。 - 在同一事务中完成三项操作:唯一性处理后,将幂等令牌写入到缓存,并将幂等令牌作为唯一索
引的数据写入到DB中。
第1步已经判断过是否是重复性操作了,为什么第2步还要再次判断?能够进入第2步,说明已经
不是重复操作了,第2次判断是否重复?
当然不重复。一般缓存中的数据是具有有效期的。缓存中数据的有效期一旦过期,就是发生缓
存穿透,使请求直接就到达了DBMS。
解决方案举例
以支付场景为例:
- 当支付请求到达后,首先在Redis缓存中却获取key为支付流水号的缓存value。若value不空,则
说明本次支付是重复操作,业务系统直接返回调用侧重复支付标识;若value为空,则进入下一步
操作 - 到DBMS中根据支付流水号查询是否存在相应实例。若存在,则说明本次支付是重复操作,业务
系统直接返回调用侧重复支付标识;若不存在,则说明本次操作是首次操作,进入下一步完成唯
一性处理 - 在分布式事务中完成三项操作:
完成支付任务
将当前支付流水号作为key,任意字符串作为value,通过set(key, value, expireTime)将数
据写入到Redis缓存
将当前支付流水号作为主键,与其它相关数据共同写入到DBMS
5.9 消息堆积与消费延迟
- 概念:
- 消息处理过程中,如果consmer的消费速度跟不上producer的发送速度,Mq中未处理的消息就会越来越多,这部分就被称为堆积消息
- 以下场景需要重点关注消息的堆积和消费延迟的问题:
- 业务系统上下游能力不匹配造成的持续堆积,且无法恢复
- 业务系统对消息的消费实时性比较高,既是是短暂的消息堆积也无法接受
5.9.1 消息堆积的主要原因
-
Consumer使用长轮询Pull模式消费消息时,分为以下两个阶段:
-
消息拉取:
-
- consumer通过长轮询,pull模型,批量拉取的方式,从服务端拉取数据,将拉取的消息缓存到本地缓冲队列中,对于拉取消费在内网环境下有很高的吞吐量,所以这一阶段不会造成消息的积压
-
消息的消费
-
- consumer将本地缓存的消息提交到消费线程中,使用业务消费逻辑对消息进行处理,处理完毕后获取到一个结果,这就是真正的消息消费过程,此时consumer的消费能力完全依赖于
消费耗时
和消费并发度
了,如果由于业务处理逻辑等复杂原因,导致单条消息的耗时较长,则整体的消息吞吐量,肯定不高,就会导致consumer的消费上限,停止从服务端拉取信息
- consumer将本地缓存的消息提交到消费线程中,使用业务消费逻辑对消息进行处理,处理完毕后获取到一个结果,这就是真正的消息消费过程,此时consumer的消费能力完全依赖于
-
结论:
-
- 消息堆积的主要原因在于客户端的消费能力,而消费能力由
消费耗时
和消费并发布
决定的,注意:消费耗时的优先级要高于消费并发度既在保证了消费耗时的合理前提下,在考虑消息并发度的问题
- 消息堆积的主要原因在于客户端的消费能力,而消费能力由
5.9.2 消费的耗时
影响消息的处理的时长的主要因素是代码逻辑,而代码逻辑中可能会响应处理时长的有两种因素cpu内部计算型代码
和外部I/O型代码
通常情况下代码中没有复杂了递归和循环的话,内部计算耗时相对于外部Io操作来说几乎可以忽略,所以外部IO才是耗时的存在
外部IO举例:
- 读写外部数据库 Mysql
- 读写外部缓存系统 Redis
- 下游调用 例如Dubbo的Rpc远程调用,SpringCloud对下游的Http调用
关于下游调用逻辑需要提前梳理,掌握每个调用操作预期的时间,这么做是为了可以判断消费逻辑中,io操作的耗时是否合理,通常消息的堆积是由于下游出现了
服务异常或者达到了DBMS的容量限制
,导致耗时增加,可以增加消费集群,增加DBMS的容量
服务异常并不仅仅是系统中出现了类似于500这样的错误,而可能是更加隐蔽的问题,例如网络带宽的问题,达到了DBMS的容量限制,其也会引发消息的消费耗时增加
5.9.3 消费的并发度
- 一般情况下,消费者端的消费并发度由节点线程数和节点数量共同决定的,其值为
单节点线程数*节点数量
只不过需要优先调整节点的线程数量,若单击硬件资源达到了上限,则需要通过横向扩展来提高消息的并发度
单节点线程数:既单个consumer所包含的线程数
节点数量:既consumer所包含的consumer的数量
对于普通消息,延迟消息 以及事务消息,并发度计算都是
单节点线程数*节点数量
的但是对于顺序消息是不同的,顺序消息的消费并发度等于Topic的Queue数量
1)全局顺序消息:该类型的Topic只有一个Queue分区,其可以保证Topic的所有消息被顺序消费,为了保证这个全局顺序性,Consumer Group 中在同一个时刻只能有一个Consumer的一个线程进行消费,所以并发度为1
2)分区顺序消费性;该类型的Topic有多个Queue,其仅可以保证Topic的每个Queue被顺序的消费掉,不能保证整个Topic的消息被顺序消费,为了保证这个分区的顺序性,每个Queue分区中的消息,在Consumer Group中的统一时刻,只能有一个Consumer的一个线程进行消费,既,在同一时刻,最多会出现多个Queue有多个Consumer的多线程进行消费,其并发度在于Topic分区的数量
5.9.4 单击线程的计算
对于一台主机中线程池中线程数的设置需要谨慎,不能盲目的调大,设置过大的线程数反而会带来大量的线程的切换和开销,理想环境下单节点的最优线程计算是C *(T1 + T2)/ T1。
- C:CPU内核数
- T1:CPU内部逻辑计算耗时
- T2:外部IO操作耗时
最优线程数 = C *(T1 + T2)/ T1 = C * T1/T1 + C * T2/T1 = C + C * T2/T1
注意,该计算出的数值是理想状态下的理论数据,在生产环境中,不建议直接使用。而是根据
当前环境,先设置一个比该值小的数值然后观察其压测效果,然后再根据效果逐步调大线程
数,直至找到在该环境中性能最佳时的值。
5.9.5 如何避免消息的堆积和延迟
为了避免在业务使用时出现非预期的消息堆积和消费延迟问题,需要在前期设计阶段对整个业务逻辑进
行完善的排查和梳理。其中最重要的就是梳理消息的消费耗时和设置消息消费的并发度。
梳理消息的消费耗时
通过压测获取消息的消费耗时,并对耗时较高的操作的代码逻辑进行分析。梳理消息的消费耗时需要关注以下信息:
- 消息消费逻辑的计算复杂度是否过高,代码是否存在无限循环和递归等缺陷
- 消息消费逻辑中的I/O操作是否是必须的,能否用本地缓存等方案规避。
- 消费逻辑中的复杂耗时的操作是否可以做异步化处理。如果可以,是否会造成逻辑错乱。
设置消费并发度 - 对于消息消费并发度的计算,可以通过以下两步实施:
- 逐步调大单个Consumer节点的线程数,并观测节点的系统指标,得到单个节点最优的消费线程数
和消息吞吐量。 - 根据上下游链路的流量峰值计算出需要设置的节点数
节点数 = 流量峰值 / 单个节点消息吞吐量
5.9.7 消息的清理
消息被消费过后会被清理掉吗?不会的。
消息是被顺序存储在commitlog文件的,且消息大小不定长,所以消息的清理是不可能以消息为单位进
行清理的,而是以commitlog文件为单位进行清理的。否则会急剧下降清理效率,并实现逻辑复杂。
- commitlog文件存在一个
过期时间
默认是72小时,既三天,除了用户手动清理之外,以下的时间也会自动的清理,无论文件中的消息是否被消费过 - 1)文件过期,且到达
清理时间
(默认是凌晨4点),自动清理过期文件 - 2)文件过期,且磁盘占用率已经
过期清理警戒线
(默认75%)之后,无论是否达到清理点,都会自动清理过期文件 - 3)磁盘占用率达到
清理警戒线
(默认85%)之后,开始按照设计好的规则清理文件,无论文件是否过期 - 4)磁盘占用率达到
系统默认危险警戒线
(默认90%)之后,Broker将拒绝写入
需要注意以下几点:
1)对于RocketMQ系统来说,删除一个1G大小的文件,是一个压力巨大的IO操作。在删除过程
中,系统性能会骤然下降。所以,其默认清理时间点为凌晨4点,访问量最小的时间。也正因如
果,我们要保障磁盘空间的空闲率,不要使系统出现在其它时间点删除commitlog文件的情况。
2)官方建议RocketMQ服务的Linux文件系统采用ext4。因为对于文件删除操作,ext4要比ext3性
能更好
五:Rocketmq应用
- 消息的发送分类
- 同步发送消息
- produce发送消息之后会收到mq返回的ACK才会发送下一条消息,该消息的可靠性高,但是效率太低了
- 异步发送消息
- 异步消息是指produce发送消息之后无需等待produce返回ACK,直接发送下一条消息,该方法的消息可靠性得到保证,效率还可以
- 单向发送消息
- 单向发送消息是指,proudce仅负责发送消息,不等待不处理mq的ACK,该发送方式MQ不返回ACK,效率最高,可靠性最差
5.1 同步发送消息
- 创建Maven工程,添加POM
- 版本号需要与自己电脑的Rockermq的版本一致
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.0</version>
</dependency>
</dependencies>
- 代码实现
package com.bj.sh;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
/**
* @author LXY
* @desc 消息的同步发送
* @time 2022--12--25--11:36
*/
public class test1 {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
//创建一个Produce,参数为 produce groub
DefaultMQProducer defaultMQProducer=new DefaultMQProducer("pg");
//指定NameServer的地址
defaultMQProducer.setNamesrvAddr("192.168.116.132:9876");
//设置 发送失败重试,默认是2
defaultMQProducer.setRetryTimesWhenSendAsyncFailed(3);
//设置消息的超时时间,默认是3s
defaultMQProducer.setSendMsgTimeout(5000);
//开启生产者
defaultMQProducer.start();
for (int i = 0; i < 100; i++) {
//将消息转为byte类型
byte [] bytes=("Hi "+i).getBytes();
//指定Topic,tag和发送分消息体
Message message=new Message("someTopic","somTag",bytes);
//发送消息
SendResult send = defaultMQProducer.send(message);
//send 可以查看返回的状态等信息
System.out.println(send.toString());
}
//关闭资源
defaultMQProducer.shutdown();
}
}
- SendStatus 类的源码
// 消息发送的状态
public enum SendStatus {
SEND_OK, // 发送成功
FLUSH_DISK_TIMEOUT, // 刷盘超时。当Broker设置的刷盘策略为同步刷盘时才可能出
现这种异常状态。异步刷盘不会出现
FLUSH_SLAVE_TIMEOUT, // Slave同步超时。当Broker集群设置的Master-Slave的复
制方式为同步复制时才可能出现这种异常状态。异步复制不会出现
SLAVE_NOT_AVAILABLE, // 没有可用的Slave。当Broker集群设置为Master-Slave的
复制方式为同步复制时才可能出现这种异常状态。异步复制不会出现
}
- 原则我们刚创建的交换机,就可以查看消息了
5.2 消息的异步发送
package com.bj.sh;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
/**
* @author LXY
* @desc 异步发送
* @time 2022--12--25--11:58
*/
public class test2 {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
//创建一个Produce,参数为 produce groub
DefaultMQProducer defaultMQProducer=new DefaultMQProducer("pg");
//指定NameServer的地址
defaultMQProducer.setNamesrvAddr("192.168.116.132:9876");
//开启生产者
defaultMQProducer.start();
for (int i = 0; i < 100; i++) {
//将消息转为byte类型
byte [] bytes=("Hi "+i).getBytes();
//指定Topic,tag和发送分消息体
Message message=new Message("MyTopic","somTag",bytes);
// 异步发送消息
defaultMQProducer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
@Override
public void onException(Throwable throwable) {
throwable.printStackTrace();
}
});
}
Thread.sleep(3000);
//关闭资源
defaultMQProducer.shutdown();
}
}
-
Thread.sleep(3000) 很重要,因为是异步发送的,如果没有这个就会报错,理解为消息还没发送成功就关闭了,就会抛出异常
5.3 消息的单向发送
package com.bj.sh.Produce;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
/**
* @author LXY
* @desc 消息的单向发送
* @time 2022--12--25--12:09
*/
public class test3 {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
//创建一个Produce,参数为 produce groub
DefaultMQProducer defaultMQProducer=new DefaultMQProducer("pg");
//指定NameServer的地址
defaultMQProducer.setNamesrvAddr("192.168.116.132:9876");
defaultMQProducer.start();
for (int i = 0; i < 100; i++) {
//将消息转为byte类型
byte [] bytes=("Hi "+i).getBytes();
//指定Topic,tag和发送分消息体
Message message=new Message("single","somTag",bytes);
defaultMQProducer.sendOneway(message);
}
//关闭资源
defaultMQProducer.shutdown();
}
}
5.4 消息的消费
package com.bj.sh.consumer;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import java.util.List;
/**
* @author LXY
* @desc 消息的消费(集群模式)
* @time 2022--12--25--12:18
*/
public class test1 {
public static void main(String[] args) throws MQClientException {
// 定义一个push消费者
DefaultMQPushConsumer defaultMQPushConsumer=new DefaultMQPushConsumer("cg");
//连接NameServer
defaultMQPushConsumer.setNamesrvAddr("192.168.116.132:9876");
//从第一条开始消费
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//定义消费的Topic和消费的tag
defaultMQPushConsumer.subscribe("someTopic","*");
//广播模式,默认是集群模式
// defaultMQPushConsumer.setMessageModel(MessageModel.BROADCASTING);
//消息的监听
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.err.println(messageExt);
}
//返回成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//开启消费者消费
defaultMQPushConsumer.start();
}
}
- 默认是集群消费,也可以设置广播消费
5.5 顺序消费
- 消息有序指的是,消费者端消费消息时,需按照消息的发送顺序来消费,即先发送的消息,需要先消费(FIFO)。
- 举个容易理解的例子:通常创建订单后,会经历一系列的操作:【订单创建 -> 订单支付 -> 订单发货 -> 订单配送 -> 订单完成】。在创建完订单后,会发送五条消息到MQ Broker中,消费的时候要按照【订单创建 -> 订单支付 -> 订单发货 -> 订单配送 -> 订单完成】这个顺序去消费,这样的订单才是有效的。
- RocketMQ采用
局部顺序一致性
的机制,实现了单个队列中消息的有序性,使用FIFO
顺序提供有序消息。简而言之,我们的消息要保证有序,就必须把一组消息存放在同一个队列,然后由Consumer进行逐一消费
5.5.1 顺序消息的原理
在默认的情况下,消息发送会采取Round Robin轮询方式把消息发送到不同的queue;而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序的。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。
5.5.1 全局有序
- 全局顺序消息的话,我们需要将所有消息都发送到同一个队列,然后消费者端也订阅同一个队列,这样就能实现顺序消费消息的功能。下面通过一个示例说明如何实现全局顺序消息。
- 生产者代码
package com.bj.sh.Order;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.util.List;
/**
* @author LXY
* @desc 全局消息
* @time 2022--12--25--13:09
*/
public class OrderMessage {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
//创建一个Produce,参数为 produce groub
DefaultMQProducer defaultMQProducer=new DefaultMQProducer("pg");
//指定NameServer的地址
defaultMQProducer.setNamesrvAddr("192.168.116.132:9876");
defaultMQProducer.start();
for (int i = 0; i < 10; i++) {
//将消息转为byte类型
byte [] bytes=("全局有序消息 "+i).getBytes();
//指定Topic,tag和发送分消息体
Message message=new Message("OrderTopic","somTag",bytes);
//发送消息
SendResult send = defaultMQProducer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
return list.get(Integer.parseInt(o+""));
}
},1);
System.out.println(send.toString());
}
//关闭资源
defaultMQProducer.shutdown();
}
}
- 消费者代码
package com.bj.sh.Order;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
/**
* @author LXY
* @desc
* @time 2022--12--25--13:11
*/
public class OrderMessageConsumer {
public static void main(String[] args) throws MQClientException {
// 定义一个push消费者
DefaultMQPushConsumer defaultMQPushConsumer=new DefaultMQPushConsumer("cg");
//连接NameServer
defaultMQPushConsumer.setNamesrvAddr("192.168.116.132:9876");
//从第一条开始消费
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//定义消费的Topic和消费的tag
defaultMQPushConsumer.subscribe("OrderTopic","*");
//广播模式,默认是集群模式
// defaultMQPushConsumer.setMessageModel(MessageModel.BROADCASTING);
//消息的监听
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : list) {
System.out.println("消费线程=" + Thread.currentThread().getName() +
", queueId=" + msg.getQueueId() + ", 消息内容:" + new String(msg.getBody()));
}
//返回成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//开启消费者消费
defaultMQPushConsumer.start();
}
}
- 只有一个queue并且顺序是同的
5.5.2 局部顺序消息
- 下面用订单进行分区有序的示例。一个订单创建完成后,订单的状态流转大概是:【订单创建 -> 订单支付 -> 订单完成】,我们在创建MessageQueueSelector消息队列选择器的时候,需要根据业务唯一标识自定义队列选择算法,如本例中则可以使用orderId订单号去选择队列。这样的话,订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。
package com.bj.sh.Order;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author LXY
* @desc 分区消息
* @time 2022--12--25--13:22
*/
public class orderProduce {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
//创建一个Produce,参数为 produce groub
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("pg");
//指定NameServer的地址
defaultMQProducer.setNamesrvAddr("192.168.116.132:9876");
defaultMQProducer.setDefaultTopicQueueNums(2);
defaultMQProducer.start();
for (int i = 0; i < 100; i++) {
Integer orderId = i;
//将消息转为byte类型
byte[] bytes = ("分区 " + i).getBytes();
//指定Topic,tag和发送分消息体
Message message = new Message("OrderTopic", "somTag", bytes);
//发送消息
int finalI = i;
SendResult send = defaultMQProducer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message message, Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.println(send.toString());
}
//关闭资源
defaultMQProducer.shutdown();
}
}
5.6 延迟消息
- 当消息写入到Broker后,在指定的时长后才可被消费处理的消息,称为延时消息。
- 采用RocketMQ的延时消息可以实现
定时任务
的功能,而无需使用定时器。典型的应用场景是,电商交
易中超时未支付关闭订单的场景,12306平台订票超时未支付取消订票的场景。
在电商平台中,订单创建时会发送一条延迟消息。这条消息将会在30分钟后投递给后台业务系
统(Consumer),后台业务系统收到该消息后会判断对应的订单是否已经完成支付。如果未完
成,则取消订单,将商品再次放回到库存;如果完成支付,则忽略。
在12306平台中,车票预订成功后就会发送一条延迟消息。这条消息将会在45分钟后投递给后台
业务系统(Consumer),后台业务系统收到该消息后会判断对应的订单是否已经完成支付。如
果未完成,则取消预订,将车票再次放回到票池;如果完成支付,则忽略。
5.6.1 延迟等级
- 延时消息的延迟时长不支持随意时长的延迟,是通过特定的延迟等级来指定的。延时等级定义在
RocketMQ服务端的MessageStoreConfig类中的如下变量中:
- 即,若指定的延时等级为3,则表示延迟时长为10s,即延迟等级是从1开始计数的。
- 当然,如果需要自定义的延时等级,可以通过在broker加载的配置中新增如下配置(例如下面增加了1
天这个等级1d)。配置文件在RocketMQ安装目录下的conf目录中。
5.6.2 延迟的实现原理
Producer将消息发送到Broker后,Broker会首先将消息写入到commitlog文件,然后需要将其分发到相
应的consumequeue。不过,在分发之前,系统会先判断消息中是否带有延时等级。若没有,则直接正
常分发;若有则需要经历一个复杂的过程:
- 修改消息的Topic为SCHEDULE_TOPIC_XXXX
- 根据延时等级,在consumequeue目录中SCHEDULE_TOPIC_XXXX主题下创建出相应的queueId
目录与consumequeue文件(如果没有这些目录与文件的话)
延迟等级delayLevel与queueId的对应关系为queueId = delayLevel -1
需要注意,在创建queueId目录时,并不是一次性地将所有延迟等级对应的目录全部创建完毕,
而是用到哪个延迟等级创建哪个目录
- 修改消息索引单元内容。索引单元中的Message Tag HashCode部分原本存放的是消息的Tag的
Hash值。现修改为消息的投递时间
。投递时间是指该消息被重新修改为原Topic后再次被写入到
commitlog中的时间。投递时间 = 消息存储时间 + 延时等级时间
。消息存储时间指的是消息
被发送到Broker时的时间戳。
5.6.3 投递延时消息
- Broker内部有⼀个延迟消息服务类ScheuleMessageService,其会消费SCHEDULE_TOPIC_XXXX中的消息,即按照每条消息的投递时间,将延时消息投递到⽬标Topic中。不过,在投递之前会从commitlog中将原来写入的消息再次读出,并将其原来的延时等级设置为0,即原消息变为了一条不延迟的普通消息。然后再次将消息投递到目标Topic中。
ScheuleMessageService在Broker启动时,会创建并启动一个定时器TImer,用于执行相应的定时
任务。系统会根据延时等级的个数,定义相应数量的TimerTask,每个TimerTask负责一个延迟
等级消息的消费与投递。每个TimerTask都会检测相应Queue队列的第一条消息是否到期。若第
一条消息未到期,则后面的所有消息更不会到期(消息是按照投递时间排序的);若第一条消
息到期了,则将该消息投递到目标Topic,即消费该消息。
5.6.4 将消息重新写入commitlog
延迟消息服务类ScheuleMessageService将延迟消息再次发送给了commitlog,并再次形成新的消息索
引条目,分发到相应Queue。
5.6.5 代码实现
- message.setDelayTimeLevel(3);
5.7 事务消息
- 这里的一个需求场景是:工行用户A向建行用户B转账1万元。
我们可以使用同步消息来处理该需求场景:
- 工行系统发送一个给B增款1万元的同步消息M给Broker
- 消息被Broker成功接收后,向工行系统发送成功ACK
- 工行系统收到成功ACK后从用户A中扣款1万元
- 建行系统从Broker中获取到消息M
- 建行系统消费消息M,即向用户B中增加1万
这其中是有问题的:若第3步中的扣款操作失败,但消息已经成功发送到了Broker。对于MQ来
说,只要消息写入成功,那么这个消息就可以被消费。此时建行系统中用户B增加了1万元。出
现了数据不一致问题。
- 解决思路,使用分布式消息
- 第1、2、3步具有原子性,要么全部成功,要么全部失败。即消息发送成功后,必须要
保证扣款成功。如果扣款失败,则回滚发送成功的消息。而该思路即使用事务消息
- 事务管理器Tm向事务协调管理器Tc发起指令,开启全局事务
- 工行系统发一个给B增加1w元的事务消息给Tc
- TC会向Broker发送
半事务消息prepareHalf
,将消息预提交
到Broker中,此时建行系统是看不到这个消息的 - Broker会将预提交的结果返回给Tc
- 如果
预提交
失败,则Tc会向Tm发送失败的请求,全局事务结束,如果预提交成功,Tc会调用回调的方法
,去完成工行用户的预扣款
操作, - 工行系统会向Tc发送
预扣款的处理结果
既本地事务
的执行状态 - Tc收到预扣款的结果后,将消息上报给Tm
- TM会根据上报结果向TC发出不同的确认指令
- 若预扣款成功(本地事务状态为COMMIT_MESSAGE),则TM向TC发送Global Commit指令
- 若预扣款失败(本地事务状态为ROLLBACK_MESSAGE),则TM向TC发送Global Rollback指令
- 若现未知状态(本地事务状态为UNKNOW),则会触发工行系统的本地事务状态回查操作。回查操作会将回查结果,即COMMIT_MESSAGE或ROLLBACK_MESSAGE Report给TC。TC将结果上
报给TM,TM会再向TC发送最终确认指令Global Commit或Global Rollback
- TC在接收到指令后会向Broker与工行系统发出确认指令
- TC接收的若是Global Commit指令,则向Broker与工行系统发送Branch Commit指令。此时
Broker中的消息M才可被建行系统看到;此时的工行用户A中的扣款操作才真正被确认 - TC接收到的若是Global Rollback指令,则向Broker与工行系统发送Branch Rollback指令。此时
Broker中的消息M将被撤销;工行用户A中的扣款操作将被回滚
5.8 事务消息的代码实现
package com.bj.sh.Transaction;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;
import java.util.concurrent.*;
/**
* @author LXY
* @desc 事务消息的消息发送
* @time 2022--12--25--15:31
*/
public class TransactionProducer {
public static void main(String[] args) throws MQClientException {
TransactionMQProducer defaultMQProducer=new TransactionMQProducer("tpg");
//指定NameServer的地址
defaultMQProducer.setNamesrvAddr("192.168.116.132:9876");
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;
}
});
// 为生产者指定一个线程池
defaultMQProducer.setExecutorService(executorService);
// 为生产者添加事务监听器
defaultMQProducer.setTransactionListener(new ICBCTransactionListener());
defaultMQProducer.start();
String[] tags = {"AB","AC","AD"};
for (int i = 0; i < 3; i++) {
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("TTopic", tags[i], body);
//事务发送消息
TransactionSendResult sendResult = defaultMQProducer.sendMessageInTransaction(msg, null);
System.out.println(sendResult);
sendResult.getSendStatus();
}
}
}
package com.bj.sh.Transaction;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
/**
* @author LXY
* @desc
* @time 2022--12--25--15:33
*/
public class ICBCTransactionListener implements TransactionListener {
// 回调操作方法
// 消息预提交成功就会触发该方法的执行,用于完成本地事务
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object ags) {
System.out.println("预提交");
//成功
if (message.getTags().equals("AB")) {
return LocalTransactionState.ROLLBACK_MESSAGE; //成功
} else if (message.getTags().equals("AC")) {
return LocalTransactionState.COMMIT_MESSAGE; //失败
} else {
return LocalTransactionState.UNKNOW; //扣款结果不清楚,需要执行消息回查
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
System.out.println("执行回查");
return LocalTransactionState.COMMIT_MESSAGE;
}
}
package com.bj.sh.Transaction;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
/**
* @author LXY
* @desc
* @time 2022--12--25--13:11
*/
public class OrderMessageConsumer {
public static void main(String[] args) throws MQClientException {
// 定义一个push消费者
DefaultMQPushConsumer defaultMQPushConsumer=new DefaultMQPushConsumer("cg");
//连接NameServer
defaultMQPushConsumer.setNamesrvAddr("192.168.116.132:9876");
//从第一条开始消费
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//定义消费的Topic和消费的tag
defaultMQPushConsumer.subscribe("TTopic","*");
//广播模式,默认是集群模式
// defaultMQPushConsumer.setMessageModel(MessageModel.BROADCASTING);
//消息的监听
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : list) {
System.out.println("消费线程=" + Thread.currentThread().getName() +
", queueId=" + msg.getQueueId() + ", 消息内容:" + new String(msg.getBody()));
}
//返回成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//开启消费者消费
defaultMQPushConsumer.start();
}
}
提交了三个 “AB”,“AC”,“AD”
其中AB是成功的
AC是失败的
AD是需要回查的,回查的结果是成功,故我们发送了三条可以消费两条数据
5.9 消息的批量
5.9.1 批量发送消息
发送限制
生产者可以对消息可以一次性发送多条消息,这可以大大提升produce的生产效率,不过需要注意以下几点
- 批量生产的消息必须是相同的Topic,
- 批量生产的消息,必须具备相同的刷盘策略
- 批量生产的消息不能是延迟消息与事务消息
批量发送大小 - 默认情况下发送的消息大小不能超过4m,如果超出了有两种解决方案
- 方案一:将批量消息进行拆分,拆分为若干不大于4M的消息集合分多次批量发送
- 方案二:在Producer端与Broker端修改属性
** Producer端需要在发送之前设置Producer的maxMessageSize属性
** Broker端需要修改其加载的配置文件中的maxMessageSize属性
生产者发送的消息大小
生产者通过send()方法发送的Message,并不是直接将Message序列化后发送到网络上的,而是通过这
个Message生成了一个字符串发送出去的。这个字符串由四部分构成:Topic、消息Body、消息日志
(占20字节),及用于描述消息的一堆属性key-value。这些属性中包含例如生产者地址、生产时间、
要发送的QueueId等。最终写入到Broker中消息单元中的数据都是来自于这些属性。
5.9.2 批量消费消息
Consumer的MessageListenerConcurrently监听接口的consumeMessage()方法的第一个参数为消息列
表,但默认情况下每次只能消费一条消息。若要使其一次可以消费多条消息,则可以通过修改
Consumer的consumeMessageBatchMaxSize属性来指定。不过,该值不能超过32。因为默认情况下消
费者每次可以拉取的消息最多是32条。若要修改一次拉取的最大值,则可通过修改Consumer的
pullBatchSize属性来指定。
存在的问题
Consumer的pullBatchSize属性与consumeMessageBatchMaxSize属性是否设置的越大越好?当然不
是。
- pullBatchSize值设置的越大,Consumer每拉取一次需要的时间就会越长,且在网络上传输出现
问题的可能性就越高。若在拉取过程中若出现了问题,那么本批次所有消息都需要全部重新拉取。 - consumeMessageBatchMaxSize值设置的越大,Consumer的消息并发消费能力越低,且这批被消
费的消息具有相同的消费结果。因为consumeMessageBatchMaxSize指定的一批消息只会使用一
个线程进行处理,且在处理过程中只要有一个消息处理异常,则这批消息需要全部重新再次消费
处理。
5.9.3 代码实现
package com.bj.sh.split;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
/**
* @author LXY
* @desc
* @time 2022--12--25--16:12
*/
public class BatchConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new
DefaultMQPushConsumer("cg");
consumer.setNamesrvAddr("192.168.116.132:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET
);
consumer.subscribe("someTopicA", "*");
// 指定每次可以消费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");
}
}
package com.bj.sh.split;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import java.util.ArrayList;
import java.util.List;
/**
* @author LXY
* @desc
* @time 2022--12--25--16:12
*/
public class BatchProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("192.168.116.132: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("someTopic", "someTag", 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();
}
}
package com.bj.sh.split;
import org.apache.rocketmq.common.message.Message;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* @author LXY
* @desc
* @time 2022--12--25--16:11
*/
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;
}
} // end-for
// 获取当前messages列表的子集合[currIndex, nextIndex)
List<Message> subList = messages.subList(currIndex, nextIndex);
// 下次遍历的开始索引
currIndex = nextIndex;
return subList;
}
}
5.10 消息的过滤
消息者在进行消息订阅时,除了可以指定要订阅消息的Topic外,还可以对指定Topic中的消息根据指定
条件进行过滤,即可以订阅比Topic更加细粒度的消息类型。
对于指定Topic消息的过滤有两种过滤方式:Tag过滤与SQL过滤。
5.10.1 Tag过滤
通过consumer的subscribe()方法指定要订阅消息的Tag。如果订阅多个Tag的消息,Tag间使用或运算
符(双竖线||)连接。
package com.bj.sh.filter;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
/**
* @author LXY
* @desc
* @time 2022--12--25--16:21
*/
public class producefile {
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
//创建一个Produce,参数为 produce groub
DefaultMQProducer defaultMQProducer=new DefaultMQProducer("pg");
//指定NameServer的地址
defaultMQProducer.setNamesrvAddr("192.168.116.132:9876");
//开启生产者
defaultMQProducer.start();
//将消息转为byte类型
byte [] bytes=("Hi 001").getBytes();
//指定Topic,tag和发送分消息体
Message message=new Message("someTopic","somTagA",bytes);
//发送消息
SendResult send = defaultMQProducer.send(message);
byte [] bytess=("Hi 003").getBytes();
//指定Topic,tag和发送分消息体
Message messages=new Message("someTopic","somTagB",bytess);
//发送消息
SendResult sends = defaultMQProducer.send(messages);
byte [] bytessC=("Hi 003").getBytes();
//指定Topic,tag和发送分消息体
Message messagses=new Message("someTopic","somTagB",bytessC);
//发送消息
SendResult sendss = defaultMQProducer.send(messagses);
//关闭资源
defaultMQProducer.shutdown();
}
}
package com.bj.sh.filter;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
/**
* @author LXY
* @desc
* @time 2022--12--25--16:23
*/
public class consumerfile {
public static void main(String[] args) throws MQClientException {
// 定义一个push消费者
DefaultMQPushConsumer defaultMQPushConsumer=new DefaultMQPushConsumer("cg");
//连接NameServer
defaultMQPushConsumer.setNamesrvAddr("192.168.116.132:9876");
//从第一条开始消费
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//定义消费的Topic和消费的tag
defaultMQPushConsumer.subscribe("someTopic","somTagB || somTagA");
//广播模式,默认是集群模式
// defaultMQPushConsumer.setMessageModel(MessageModel.BROADCASTING);
//消息的监听
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.err.println(messageExt);
}
//返回成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//开启消费者消费
defaultMQPushConsumer.start();
}
}
-发送了3个tag,只接受其中两个
5.10.2 SQL过滤
SQL过滤是一种通过特定表达式对事先埋入到消息中的用户属性进行筛选过滤的方式。通过SQL过滤,
可以实现对消息的复杂过滤。不过,只有使用PUSH
模式的消费者才能使用SQL过滤
SQL过滤表达式中支持多种常量类型与运算符。
支持的常量类型:
- 数值:比如:123,3.1415
- 字符:必须用单引号包裹起来,比如:‘abc’
- 布尔:TRUE 或 FALSE
- NULL:特殊的常量,表示空
支持的运算符有: - 数值比较:>,>=,<,<=,BETWEEN,=
- 字符比较:=,<>,IN
- 逻辑运算 :AND,OR,NOT
- NULL判断:IS NULL 或者 IS NOT NULL
- 默认情况下Broker没有开启消息的SQL过滤功能,需要在Broker加载的配置文件中添加如下属性,以开启该功能:
1 enablePropertyFilter = true
在启动Broker时需要指定这个修改过的配置文件。例如对于单机Broker的启动,其修改的配置文件是
conf/broker.conf,启动时使用如下命令:
1 sh bin/mqbroker -n localhost:9876 -c conf/broker.conf &
- 代码
public class FilterBySQLProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketmqOS:9876");
producer.start();
for (int i = 0; i < 10; i++) {
try {
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("myTopic", "myTag", body);
msg.putUserProperty("age", i + "");
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
} catch (Exception e) {
e.printStackTrace();
}
}
producer.shutdown();
}
}
public class FilterBySQLConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new
DefaultMQPushConsumer("pg");
consumer.setNamesrvAddr("rocketmqOS:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET
);
consumer.subscribe("myTopic", MessageSelector.bySql("age between
0 and 6"));
consumer.registerMessageListener(new
MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus
consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext
context) {
for (MessageExt me:msgs){
System.out.println(me);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("Consumer Started");
}
}
5.11 消息重试
Producer对发送失败的消息进行重新发送的机制,称为消息发送重试机制,也称为消息重投机制。
对于消息重试需要注意以下几点:
- 生 产者在发送消息的时候,若采用的
同步或者异步方式
,发送失败就会重试,但是oneway不会重试, - 只有普通消息回重试的,顺序消息不会重试
- 消息重试机制可以保证消息尽可能的发送成功,不丢失,但是可能会造成消息的重复,消息的重复在Rocketmq中是不可避免的
- 消息重试在一般情况下不会发生,出现网络波动大,就会大概率发生
- producer主动重发、consumer负载变化(发生Rebalance,不会导致消息重复,但可能出现重复
消费)也会导致重复消息 - 消息重复无法避免,但是要避免消息的重消费
- 避免消息重复消费的解决方案是,为消息添加唯一标识(例如消息key),使消费者对消息进行消
费判断来避免重复消费
-消息发送重试有三种策略可以选择:同步发送失败策略、异步发送失败策略、消息刷盘失败策略
5.11.1 同步消息发送失败策略
- 对于普通消息,消息发送默认采用round-robin策略来选择所发送到的队列。如果发送失败,默认重试2次。但在重试时是不会选择上次发送失败的Broker,而是选择其它Broker。当然,若只有一个Broker其也只能发送到该Broker,但其会尽量发送到该Broker上的其它Queue。
// 创建一个producer,参数为Producer Group名称
DefaultMQProducer producer = new DefaultMQProducer("pg");
// 指定nameServer地址
producer.setNamesrvAddr("rocketmqOS:9876");
// 设置同步发送失败时重试发送的次数,默认为2次
producer.setRetryTimesWhenSendFailed(3);
// 设置发送超时时限为5s,默认3s
producer.setSendMsgTimeout(5000);
同时,Broker还具有失败隔离
功能,使Producer尽量选择未发生过发送失败的Broker作为目标
Broker。其可以保证其它消息尽量不发送到问题Broker,为了提升消息发送效率,降低消息发送耗时。
如何自己实现`失败隔离`
1)方案一:Producer中维护某JUC的Map集合,其key是发生失败的时间戳,value为Broker实
例。Producer中还维护着一个Set集合,其中存放着所有未发生发送异常的Broker实例。选择目
标Broker是从该Set集合中选择的。再定义一个定时任务,定期从Map集合中将长期未发生发送
异常的Broker清理出去,并添加到Set集合。
2)方案二:为Producer中的Broker实例添加一个标识,例如是一个AtomicBoolean属性。只要该
Broker上发生过发送异常,就将其置为true。选择目标Broker就是选择该属性值为false的
Broker。再定义一个定时任务,定期将Broker的该属性置为false。
3)方案三:为Producer中的Broker实例添加一个标识,例如是一个AtomicLong属性。只要该
Broker上发生过发送异常,就使其值增一。选择目标Broker就是选择该属性值最小的Broker。若
该值相同,采用轮询方式选择。
- 如果超过重试次数,则抛出异常,由Producer去保证消息不丢。当然当生产者出现
RemotingException、MQClientException和MQBrokerException时,Producer会自动重投消息
5.11.1 异步消息发送失败策略
- 异步发送失败重试时,异步重试不会选择其他broker,仅在同一个broker上做重试,所以该策略无法保
证消息不丢。
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketmqOS:9876");
// 指定异步发送失败后不进行重试发送
producer.setRetryTimesWhenSendAsyncFailed(0);
5.11.2 消息刷盘失败策略
- 消息刷盘超时(Master或Slave)或slave不可用(slave在做数据同步时向master返回状态不是
SEND_OK)时,默认是不会将消息尝试发送到其他Broker的。不过,对于重要消息可以通过在Broker的配置文件设置retryAnotherBrokerWhenNotStoreOK属性为true来开启。
5.12 消息的消费重试机制
5.12.1 顺序消费的重试
对于顺序消费,当consumer消费消息失败之后为了八正消息的顺序性,其会自动不断地进行消息重
试,直到消费成功。消费重试默认间隔时间为1000毫秒。重试期间应用会出现消息消费被阻塞的情
况
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
// 顺序消息消费失败的消费重试时间间隔,单位毫秒,默认为1000,其取值范围为[10,
30000]
consumer.setSuspendCurrentQueueTimeMillis(100);
由于对顺序消息的重试是无休止的,不间断的,直至消费成功,所以,对于顺序消息的消费,
务必要保证应用能够及时监控并处理消费失败的情况,避免消费被永久性阻塞。
注意,顺序消息没有发送失败重试机制,但具有消费失败重试机制
5.12.1 无序消息重试
- 对于无序消息(普通消息、延时消息、事务消息),当Consumer消费消息失败时,可以通过设置返回
状态达到消息重试的效果。不过需要注意,无序消息的重试只对集群消费方式生效,广播消费方式不
提供失败重试特性。即对于广播消费,消费失败后,失败消息不再重试,继续消费后续消息
5.12.2 消费重试次数和间隔
对于无序消息
集群消费下的重试消费,每条消息默认最多重试16次,但每次重试的间隔时间是不同
的,会逐渐变长。每次重试的间隔时间如下表。
- 若一条消息在一直消费失败的前提下,将会在正常消费后的第
4小时46分
后进行第16次重试。
若仍然失败,则将消息投递到死信队列
修改消费重试次数
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
// 修改消费重试次数
consumer.setMaxReconsumeTimes(10);
-
对于修改过的重试次数,将按照以下策略执行:
-
若修改值小于16,则按照指定间隔进行重试
-
若修改值大于16,则超过16次的重试时间间隔均为2小时
-
对于Consumer Group,若仅修改了一个Consumer的消费重试次数,则会应用到该Group中所有
其它Consumer实例。若出现多个Consumer均做了修改的情况,则采用覆盖方式生效。即最后被
修改的值会覆盖前面设置的值。
5.12.3 重试队列
对于需要重试消费的消息,并不是Consumer在等待了指定时长后再次去拉取原来的消息进行消费,而
是将这些需要重试消费的消息放入到了一个特殊Topic的队列中,而后进行再次消费的。这个特殊的队
列就是重试队列。
当出现需要进行重试消费的消息时,Broker会为每个消费组都设置一个Topic名称
为%RETRY%consumerGroup@consumerGroup 的重试队列。
1)这个重试队列是针对消息才组的,而不是针对每个Topic设置的(一个Topic的消息可以让多
个消费者组进行消费,所以会为这些消费者组各创建一个重试队列)
2)只有当出现需要进行重试消费的消息时,才会为该消费者组创建重试队列
注意,消费重试的时间间隔与延时消费的延时等级十分相似,除了没有延时等级的前两个时间
外,其它的时间都是相同的
- Broker对于重试消息的处理是通过延时消息实现的。先将消息保存到SCHEDULE_TOPIC_XXXX延迟队列中,延迟时间到后,会将消息投递到%RETRY%consumerGroup@consumerGroup重试队列中。
5.12.4 消费重试配置方式
集群消费方式下,消息消费失败后若希望消费重试,则需要在消息监听器接口的实现中明确进行如下三
种方式之一的配置:
- 方式1:返回ConsumeConcurrentlyStatus.RECONSUME_LATER(推荐)
- 方式2:返回Null
- 方式3:抛出异常
5.12.5 消费不重试配置方式
集群消费方式下,消息消费失败后若不希望消费重试,则在捕获到异常后同样也返回与消费成功后的相
同的结果,即ConsumeConcurrentlyStatus.CONSUME_SUCCESS,则不进行消费重试。
5.13 死信队列
- 当一条消息初次消费失败,消息队列会自动进行消费重试;达到最大重试次数后,若消费依然失败,则
表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送
到该消费者对应的特殊队列中。这个队列就是死信队列(Dead-Letter Queue,DLQ),而其中的消息
则称为死信消息(Dead-Letter Message,DLM)。
5.13.1 死信队列的特征
死信队列具有如下特征:
- 死信队列中的消息不会再被消费者正常消费,即DLQ对于消费者是不可见的
- 死信存储有效期与正常消息相同,均为 3 天(commitlog文件的过期时间),3 天后会被自动删除
- 死信队列就是一个特殊的Topic,名称为%DLQ%consumerGroup@consumerGroup ,即每个消
- 费者组都有一个死信队列
- 如果⼀个消费者组未产生死信消息,则不会为其创建相应的死信队列
5.13.2 死信消息的处理
实际上,当⼀条消息进入死信队列,就意味着系统中某些地方出现了问题,从而导致消费者无法正常消
费该消息,比如代码中原本就存在Bug。因此,对于死信消息,通常需要开发人员进行特殊处理。最关
键的步骤是要排查可疑因素,解决代码中可能存在的Bug,然后再将原来的死信消息再次进行投递消
费
六:Springboot整合RockerMq
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>Springboot-RocketMq</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version> <!-- 1.5.1-->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
</dependencies>
</project>
- yml文件
server:
port: 8080
rocketmq:
name-server: 192.168.116.132:9876
producer:
group: group_rocketmq
- 消息发送
package com.bj.sh.connction;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @author LXY
* @desc
* @time 2022--12--25--17:08
*/
@RestController
@RequestMapping("/mqMessageController")
public class UserConnction {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@RequestMapping("/pushMessage")
public String get( String id) {
System.out.println("执行");
rocketMQTemplate.convertAndSend("first-topic","你好,Java旅途" + id);
return "55";
}
}
6.1 接受消息
package com.bj.sh.Consumer;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
/**
* @author LXY
* @desc 服务的消费方
* @time 2022--12--25--18:36
*/
@Component
@RocketMQMessageListener(topic = "first-topic",consumerGroup = "my-consumer-group")
public class Consumers implements RocketMQListener<String> {
@Override
public void onMessage(String mess) {
System.out.println("接受到的消息是"+mess);
}
}