RocketMQ笔记(尚硅谷教程)_小帅学编程的博客-CSDN博客
文章目录
- 1.MQ简述
- 2.MQ的用途
- 3.常见的MQ产品
- 4.MQ常用的协议
- 5.RockerMQ的安装与启动
- 6.MQ的安装
- 7.RocketMQ-admin的安装
- 8.集群搭建理论
- 9.RAID磁盘阵列
- 10.集群搭建实践
- 11.mqadmin命令
- 12.RocketMQ工作原理
- 13.RockerMQ应用
- 13.SpringBoot集成RocketMQ
1.MQ简述
MQ,Message Queue,是一种提供消息队列服务的中间件,也称为消息中间件,是一套提供了消费、存储、消费全过程API的软件系统,消息即数据,一般消息的体量不会很大。
2.MQ的用途
2.1 限流削峰
MQ可以将系统的超量请求暂存其中,以便系统后期可以慢慢进行处理,从而避免了请求的丢失或系统被压垮
2.2 异步解耦
上游系统对下游系统的调用若为同步调用,则会大大降低系统的吞吐量与并发量,且系统耦合度太高,而异步调用则会解决这些问题,随意两层之间若要实现由同步到异步的转化,一般的做法就是,在这两层之间田间一个MQ层
2.3数据收集
分布式系统会产生海量数据流,如:业务日志、监控数据、用户行为等。针对这些数据流进行实时或批量采集汇总,然后对这些数据流进行大数据分析,这是当前互联网平台的必备技术,通过MQ完成此类数据收集是最好的选择
3.常见的MQ产品
4.MQ常用的协议
5.RockerMQ的安装与启动
5.1消息(Message)
消息是指,消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题
5.2主题(Topic)
Topic表示一类消息的集合,每个主题包含若干条消息,每条消息只属于一个主题,是RocketMQ进行消费订阅的基本单位。
一个生产者可以同时发送多种Topic的消息;而一个消费者只对某种特定的Topic感兴趣,即只可以订阅和消费一种Topic的消息
5.3 标签(tag)
官方定义:
换句话的意思就是自主题,为用户提供了额外的灵活性。有了标签,来自同一个业务模块的具有不同目的的消息可以有相同的主题和不同的标记。标签有助与保持代码的清晰和连贯,同时标签也方便ROcketMQ提供的查询功能。
为消息设置的标签,用与同一主题下区分不同类型的消息。来自一业务单元的消息,可以根据不同的业务目的在同一主题下设置不同的标签。标签能够有效保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消息者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。
Topic是消息的一级分类,Tag是消息的二级分类
5.4队列(queue)
储存消息的物理实体,一个Topic中包含多个Queue,每个Queue中存放的就是该Topic的消息,一个Topic的Queue也称为一个Topic中消息的分区。
一个分区只能被一个消费者组里的一个消费者消费
5.4.1分片
在RocketMQ中,分片是指存放到相应Topic的Broker。每个分片会创建相应数量的分区,每个Queue都是相同的
5.5消息标识
RocketMQ中每个消息拥有唯一的MessageId,可以携带具有业务标识的key,以方便对消息的查询,不过需要注意的是,MessageId有两个:在生产者send()消息时会自动生成MessageId(msgId),当消费者到达Broker后,Broker也会生成一个MessageId(offsetMsgId)。msgId,offsetMsgId与key都称为消息标识。
5.6系统架构
5.6.1生产者(producer)
消息生产者,负责生产消息。Producer通过MQ的负载均衡模式选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
例如,业务系统产生的日志写入到MQ的过程,就是消息生产的过程
再如,电商平台中用户提交的秒杀请求写入到MQ的过程,就是消息生产的过程
- 1
- 2
RockerMQ中的消息生产者都是以生产者组(producer Group)的形式出现的。生产者组是同一类生产者的集合,这类Producer发送相同数量和种类Topic类型的信息,但是,一个生产者组可以同时发送多个主题的消息,此时生产者组中的每个生产者都生产多个相同类型的主题消息。
5.6.1消费者(consumer)
消息消费者,负责消费消息,一个消息消费者会从broker服务器中获取到消息,并对消息进行相关业务处理。
例如,Qos系统从MQ中读取消息,并对日志进行解析处理的过程就是消息消费的过程
再入,电商平台的业务系统从MQ中读取到秒杀请求,并对请求进行处理的过程就是消息消费的过程
- 1
- 2
RockerMQ中消息消费者都是以消费者组(consumer Group)的形式出现的,消费者组是同一类消费者的集合,这类consumer消费者是同一Topic类型的消息。消费者组使得在消息消费方面,实现负载均衡
(将一个Topic中的Queue平均分给一个Consumer Group中的Consumer)和容错
(一个Comsumer挂了,该Consumer Group中的其他Consumer可以接着消费原Consumer中的Queue)的目标变得非常容易。
消费者组中Consumer的数量应该小于或等于一个Topic中Queue的数量,如果大于,多余的Consumer将不再消费消息。
注意:
消费者组只能一个Topic的消息,不能消费多个Topic的消息。
一个消费者组中的消费者只能订阅完全相同的topic。
一个Topic类型的消息可以被多个消费者组同时消费。
但是一个Queue不能被同一消费者组多个消费者消费,一个消费者可以消费多个Queue。
- 1
- 2
- 3
- 4
- 5
5.6.2命名服务中心(NameServer)
NameServer是一个Broker与Topic路由的注册中心,支持Broker的动态注册与发现。
- Broker管理:接收Broker集群的注册信息并且保存下来作为路由信息的基本数据;提供心跳检测机制,检查Broker是否还存活。
- 路由信息管理:每个Name Server中都保存着Broker集群的整个路由信息和用于客户端查询的队列信息。Producer和Comsumer通过NameService可以获取整个Broker集群的路由信息,从而进行消息的投递和消费。
- 路由注册:NameServer通常也是以集群的方式部署,不过,NameServer是无状态的,即Name Server集群中的各个节点间是无差异的,各个节点间相互不进行信息通信。那各个节点是如何进行数据同步的呢?在Broker节点启动时,轮询NameServer列表,与每个NameServer节点建立长连接,发起注册请求。在NameServer内部维护着一个Broker列表,用来动态储存Broker的信息。
Broker节点为了证明自己是活着的,为了维护与Name Service间的长连接,会将最新的信息一心跳包的方式上报给NameServer,每30秒发送一次心跳。心跳包中包含了BrokerId,Broker地址(IP+Port),Broker名称、Broker所属集群名称等等,NameServer在接收到心跳包时,会更新线条时间戳,记录这个Broker的最新存活时间。 - 路由剔除:由于Broker关机、宕机或网络抖动等原因,NameServer没有收到Broker的心跳,NameServer可能会将其从Broker列表中剔除。
NameServer中有一个定时任务,每隔10描绘扫描一次Broker表,查看一个Broker的最新心跳时间戳,距离当前时间是否超过120秒,如果超过,则会判定Broker失效,然后将其从Broker列表中剔除 - 路由发现:RockerMQ的路由发现采用的是Pull模型,当Topic路由信息出现变化时,NameServer不会主动推送给客户端,而是客户端定时拉取主题最新的路由。默认客户端每30秒拉去一次最新的路由。
- 客户端NameServer选择策略
这里的客户端指的是Producer与Consumer
客户端在配置时必须要写上NameServer集群的地址,那么客户端到底连接的是那个NameServer节点呢?客户端首先会产生一个随机数,然后与NameServer节点数数量取模,此时得到的就是所要连接的节点索引,然后会进行连接。如果连接失败,则会采用round-robin策略,逐个尝试着去连接其它节点。
首先采用的是随机策略进行的选择,失败后采用的轮询策略
5.6.3Broker
- 功能介绍:Broker充当着消息中转角色,负责储存消息,转发消息。Broker在RocketMQ系统中负责接收生产者发送来的消息,同时为消费者的拉取请求做准备。Broker同时储存着消息相关的元数据,包括消费者组消费进度偏移offset、主题、队列等。
- 框图:
- 集群部署:
为了增强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信息到所有Name Server。 - 工作流程:
1.启动NameServer,NameServer启动后开始监听端口,等待Broker,Consumer,Producer连接。
2.启动Broker,Broker会与所有的NameServer建立长连接,然后每30秒向NameServer定时发送心跳包。
3.发送消息前,可以先创建Topic,创建Topic时需要指定该Topic要储存在那些Broker上,当然,在创建Topic时,也会将Topic与Broker的关系写入到NameServer中。不过,这步是可选的,也可以在发送消息时自动创建Topic。
4.Producer发送消息,启动时先跟Name Server集群中的一台建立长连接,并从NameServer中获取路由信息,即当前发送的信息的Queue与Broker的地址(IP+Port)的映射关系。然后根据算法策略中选择一个queue,与队列所在的Broker建立长连接从而向Broker发消息。当然,在获取到路由信息后,Producer会首先将路由信息缓存到本地,再每30秒从Name Server跟新一次路由信息。
5.Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取其订阅Topic的路由信息,然后根据算法策略从路由信息中获取到其索要小费的Queue,然后直接跟Broker建立长连接,开始消费其中的消息,Consumer在获取到路由信息后,同样也会每30秒从NameServer更新一次路由信息。不过不同于Producer的是,Consumer还会向Broker发送心跳,以确保Broker的存话状态。
手动创建Topic时,有两种模式:
- 集群模式:该模式下创建的Topic在该集群中,所有Broker中的Queue数量是相同的
- Broker模式:该模式下创建的Topic在该集群中,每个Broker中的Queue的数量可以不同
自动创建Topic时,默认采用的Broker模式,会为每个Broker默认创建4个Queue
从物理机上来讲,读/写队列是同一个队列。所以不存在读/写队列数据同步问题。读/写队列是逻辑上进行区分的概念。一般情况下,读/写队列数量是相同的。
perm用于设置对当前创建Topic的操作权限:2表示只写,4表示只读,6表示读写
6.MQ的安装
6.1 步骤
1.上传rockerBroker
2.nozip,解压安装包
3.修改runbroker.sh和runservice.sh配置文件
3.启动NameServer和Broker(单机启动)
nohup sh bin/mqnamesrv &
nohup sh bin/mqbroker -n localhost:9876 &
- 1
- 2
4.查看启动日志
tail -f ~/logs/rockermqlogs/namesrv.log
- 1
5.测试用例
再发送和接收消息之前,我们必须告诉客户端nameserver的位置。RockerMQ提供多种方式获得,简单的办法就是使用环境变量NAMESRV_ADDR
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
- 1
- 2
- 3
7.RocketMQ-admin的安装
7.1下载资源包
github搜索RocketMQ-DashBoard
7.2修改配置
修改application.yml文件的namesrvAddr
修改pom.xml中yarn版本和npm版本改为本机版本
7.3打包项目
7.4jar包上传虚拟机
7.5运行jar包
nohup java -jar jar包名 &
- 1
7.6访问控制台
8.集群搭建理论
8.1数据复制与刷盘策略
8.1.1复制策略
复制策略是Broker的Master与Slave间的数据同步方式。分为同步复制和异步复制:
- 同步复制:消息写入master后,master会等待slave同步数据成功后才向producer返回成功ACK
- 异步复制:消息写入到master后,master立即向producer返回成功ACK,无需等待slave同步数据成功
8.1.2刷盘策略
刷盘策略指的是broker中消息的落盘方式,即消息发送到Broker内存后消息持久化到磁盘的方式。分为同步刷盘和异步刷盘
- 同步刷盘:当消息持久化到Broker的磁盘后才算是消息写入成功
- 异步刷盘:当消息持久化到Broker的内存后即表示写入成功,无需等待消息持久化到磁盘
8.2Broker集群关系
8.2.1单Master
只有一个Broker(其本质上就不能成为集群)。这种方式也只能是在测试时使用,生产环境下不能使用,因为存在单点问题。
单点故障:系统中一点失效,就会让整个系统无法运行。
8.2.2多Master
broker集群仅由多个master构成,不存在slave。同一个Topic的各个Queue会平均分布在各个master节点上。
- 优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息不会丢失(一部刷盘丢失少量信息,同步刷盘一条也不会丢失),性能最高
- 缺点:单台机器宕机期间,这台机器上未被消费的信息在机器恢复之前不可订阅(不可消费),消息实时性会受到影响。
以上优缺点都是在集群配置磁盘阵列的基础上做的,如果没有配置磁盘阵列,就会出现大量数据丢失现象。
8.2.3多Master多Slave模式-异步复制
broker集群有多个master构成,每个master又配置了多个slave(在配置RAID磁盘阵列的情况下,一个master一般配置一个slave即可)。master和slave的关系是主备关系,即master负责处理消息的读写请求,而slave仅负责消息的备份与master宕机后的角色切换。
异步复制即前面所讲的复制策略中的异步复制策略,即消息写入到master成功后,master立即向producer返回成功ACK,无需等待salve同步数据成功。
该模式的最大特点之一是,当master宕机后slave能够自动切换为master。不过由于slave从master的同步具有短暂的延迟(毫秒级),所以当master宕机后,这种异步复制方式可能存在少量消息丢失问题。
Slave从Master同步的延迟越短,其可能丢失的消息就越少。
对于Master的RAID磁盘阵列,若使用的也是同步复制策略,同样也存在延迟问题,同样也可能会丢失消息,但RAID阵列的延迟是微秒级的(硬件支持),所以其丢失的数据量会更少。
8.2.4多Master和多Slave模式-同步复制
该模式是多Master和多Slave模式的同步实现。所谓同步双写,指的是消息写入master成功后,master会等待slave同步数据成功后才向producer返回陈成功ACK,即master与slave都要写入成功后才返回成功ACK,也即双写。
该模式与异步复制模式相比,优点是消息的安全性更高,不存在消息队时的情况。但单个消息的RT略高,从而导致性能要略低(大约低10%)。
该模式存在一个大的问题:对于目前的版本,Master宕机后,Slave不会自动切换到Master。
8.2.5最佳实践
9.RAID磁盘阵列
9.1概念
RAID磁盘阵列:廉价冗余磁盘阵列,由于当时大容量磁盘比较昂贵,RAID的基本思想就是将多个容量较小、相对廉价的磁盘进行有机组合,从而以较低的成本获得与昂贵大容量磁盘相当的容量、性能、可靠性。由于磁盘价格和成本的不断降低,‘廉价‘已经毫无意义。因此后来决定用’独立‘代替’廉价’,于是RAID变成了独立磁盘冗余阵列。但是内容没有改变。
9.2RAID等级
9.3关键技术
9.4RAID分类
从实现角度上来看,RAID主要分为软RAID,硬RAID以及混合RAID三种。
软RAID
所有功能均由操作系统和CPU完成,没有独立的RAID控制处理新皮纳和I/O处理芯片,效率自然最低
硬RAID
配备了专门的RAID控制处理芯片和I/O处理芯片以及仅列缓冲,不占用CPU资源。效率很高,但成本也很高。
混合RAID
具备RAID控制处理芯片,但没有专门的I/O处理芯片,需要CPU和驱动程序来完成。性能和成本在软RAID和硬RAID之间。
9.5常见的RAID等级
JBOD
其只是简单提供一种扩展存储空间的机制,JBOD可用存储容量等于所有磁盘的存储空间之和
RAID0
RAID0与JBOD形同点:
1)存储容量:都是成员磁盘容量的总和
2)磁盘利用率,都是100%,其都没有做任何的数据冗余备份
RAID0与JBOD不同点:
JBOD:数据是吮吸存放的,一个磁盘存满后才会开始存放到下一个磁盘
RAID:各个磁盘中的数据写入时并行的,是通过数据条带技术写入的。其读写性能是JBOD的N倍。
应用场景:对数据的顺序读写要求不高,对数据的安全性和可靠性不高,但对系统性能要求很高的场景。
RAID1
RAID10
RAID01
RAID10要比RAID01的容错率要高,因此在生产环境中一般是不使用RAID01的
10.集群搭建实践
10.1 VMware克隆
修改hostname
vi /etc/hostname
- 1
修改虚拟机ip地址
vi /etc/sysconfig/network-scripts/ifcfg-ens33
- 1
修改完之后,记得重启network
systemctl restart network
- 1
10.2修改RocketMQ配置文件
10.2.1配置文件位置
conf文件下,RocketMQ官方为我们提供了三种集群配置形式
我们使用2主2备异步模式,进入文件后,文件属性如下:
10.2.2修改borker-a.application
nemesrvAddr=两个ip地址
- 1
10.2.3修改broker-b-s.properties
namesrvAddr=两个ip地址
listenPort=11911
storePathRootDir=~/store-s
storePathCommitLog=~/store-s/commitlog
storePathConsumeQueue=~/store-s/consumequeue
storePathIndex=~/store-s/index
storePathCheckpoint=~/store-s/checkpoint
abortFile=~/store-s/abort
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
10.2.4其他配置
10.2.5RocketMQOS2的改动
OS1改动的是borker-a和borker-b-s,OS2需要改动borker-b和borker-a-s,步骤与OS1相同。
10.3 启动服务器
启动NameServer
分别启动rockermqOS1与rocketmqOS2两个主机中的NameServer
nohup sh bin/mqnamesrv &
tail -f ~/log/rocketmqlogs/namesrv.log
- 1
- 2
启动两个Master
分别启动rocketmqOS1与rocketmqOS2两个主机中的broker master。注意,他们指定所要加载的配置文件是不同的。
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a.properties &
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b.properties &
- 1
- 2
启动两个slave
分别启动rocketmqOS1与rocketmqOS2两个主机中的slave。注意,他们指定所要加载的配置文件是不同的。
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b-s.properties &
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a-s.properties &
- 1
- 2
注意点
记得关闭防火墙
11.mqadmin命令
新版本不需要配置java设置,详细的文档可以看这个网址
https://github.com/apache/rocketmq/blob/master/docs/cn/operation.md
- 1
12.RocketMQ工作原理
12.1消息的生产
12.1.1消息的生产过程
Producer可以将消息写入到某Broker中的Queue中,其经历了如下过程:
- Producer发送消息之前,会先向NameServer发出获取消息Topic的路由信息的请求
- Nameserver返回该Topic的路由表和Broker列表
- Producer根据代码中指定的Queue选择策略,从Queue列表中选出一个队列,用于后续存储消息
- Producer对消息进行一些特殊处理,例如,消息本身超过4M,则会对其进行压缩
- Producer想选择出的Queue所在的Broker发出RPC请求,将消息发送到选择的Queue上
路由表:实际上就是一个Map,key为Topic名称,value是一个QueueData实例列表QueueData并不是一个Queue对应一个Queuedata,而是一个Broker中该Topic的所有Queue对应一个QueueData。即,只要涉及到该Topic的Broker,一个Broker对应一个Queue。QueueData中包含brokerName。简单来说,路由表的key为Topic名称,value则为所有涉及该Topic的BrokerName列表。
Broker列表:其实际也是一个Map,key为brokerName,value为BrokerData,一个Broker对应一个Broker实例。对吗?不对。一个BrokerData对应着BrokerName相同的Master-slave集群。BrokerData中包含BrokerName及一个map,该map的key为brokerId,value为broker对应的地址,brokerId为0表示该Broker为Master,非0为salve。
RPC请求:远程过程调用 (RPC) 是一种协议,程序可使用这种协议向网络中的另一台计算机上的程序请求服务。
12.1.2Queue选择算法
对于无序消息,其Queue选择算法,也称为消息投递算法,常见的有两种:
轮询算法
默认选择算法。该算法保证了每个Queue中可以均匀的获取到消息:
该算法存在一个问题:由于某些原因,在某些Broker上的Queue可能投递延迟较严重。从而导致Producer的缓存队列中出现较大的消息积压,影响消息的投递性能。
最小投递延迟算法
该算法会统计每次消息投递的时间延迟,然后根据统计出的结果将消息投递到时间延迟最小的Queue。如果延迟相同,则采用轮询算法投递。该算法可以有效提升消息的投递性能。
该算法也存在一个问题:消息再Queue上的分配不均匀。投递延迟小的Queue其可能存在大量的消息。而对该Queue的消费者压力会增大,降低消息的消费能力,可能会导致MQ中消息的堆积。
12.2消息的储存
RocketMQ中的消息存储在本地文件系统中,这些相关文件默认在当前用户主目录下的store目录中。
- abort:该文件在Broker启动后会自动创建,正常关闭Broker,该文件会自动消失。若在没有启动Broker情况下,发现这个文件是存在的,则说明之前Broker的关闭时非正常关闭。
- checkpoint:其中存放着commintlog、consumerqueue、index文件的最后沙盘时间
- commitlog:其中存放着commitlog文件,而消息是写在commitlog文件中的
- config:存放着Broker运行期间的一些配置数据
- consumequeue:其中存放着consumer queue文件,队列就放在这个目录中
- index:其中存放着消息索引文件indexFile
- lock:运行期间使用到的全局资源锁
12.2.1commitlog文件
说明:在很多资料中commitlog目录中的文件简单就称为commitlog问年间。但在源码中,该文件被命名为mappedFile
目录与文件
commitlog目录中存放着很多的mappedFile文件,当前Broker中所有消息都是落盘到这些mappedFiel文件中的。mappedFile文件容量为1G(其中存放的数据可能会小于1G,因为该文件剩余的容量不足以储存下一条消息,此时下一条消息就会储存到另一个mappedFile文件中),文件由20位十进制数构成,表示当前文件的第一条消息的起始唯一偏移量。
第一个mappedFile文件名一定为20个0.
如果第一个文件储存了不足1G的消息,第二个文件的的偏移量就是第一个文件存放消息的大小,以此类推,第n个文件就为前n-1个文件大小之和
需要注意的是,一个Broker中仅包含着一个commit log目录,所有的mappedFile文件都是存放在该目录中的。即无论当前Broker中存放着多少Topic的消息,这些消息都是被顺序写入到mappedFile文件中的。也就是说,这些消息再Broker中存放时并没有按照Topic进行分类存放。
消息单元
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)
12.2.2consumeQueue文件
为了提高效率,会为每个在~/store/consumeQueue中创建一个目录,目录名为Topic名称。在该Topic目录下,会在为每个该Topic的Queue创建一个目录,目录名为queueId。每个目录中存放着若干consumequeue文件,consumequeue文件是commitlog的索引文件,可以根据consume queue定位到具体的消息。
consumequeue文件名也是由20位数字构成,表示当前文件的第一个索引条目的起始偏移量。与mappedFile文件名不同的是,其后续文件名是固定的。因为consumequeue文件大小是固定不变的。
索引条目
每个consumequeue文件可以包含30W个索引条目,每个索引条目包含了三个消息重要属性:消息在mappedFile文件中的偏移量CommitLogOffset、消息长度、消息Tag的hashCode置。这三个属性占20个字节,所以每个文件的大小是固定的30W*20字节
一个consumequeue文件中所有消息的Topic一定是相同的。但每条消息Tag可能不同的
12.2.3对文件的写入
消息写入
一条消息进入到Broker后经历了以下几个过程才最终被持久化。
- Borker会根据queueId获取到该消息对应索引条目要在consumequeue目录的写入偏移量,即QueueOffset
- 将queueId、queueOffset等数据,与消息一起封装为消息单元
- 将消息单元写入到commitlog
- 同时形成消息索引条目
- 将消息索引条目分发到相应的consumequeue
消息拉取
- 当Conusmer来拉去消息时会经历以下几个步骤:
- Consumer获取到其要消费消息所在Queue的消费偏移量offset计算出要消费消息的消息offset
消费offset及消费进度,consumer对某个Queue的消费offset,即消费到了该Queue的第几条消息
消息offset = 消费offset + 1- Consumer向Broker发送拉去请求,其中会包含其拉取消息的Queue、消息offset以及消息Tag
- Broker计算在该consume queue中的queueOffset
queueOffset = 消息offset * 20字节
- 从该queueOffset处开始向后查找第一个指定Tag的索引条目
- 解析该索引条目的前8个字节,即可定位到该消息在commitlog中的commitlog offset
- 从对应的commitlog offset中读取消息单元,并发送给Consumer
性能提升
12.3indexFile
除了通过通常的指定Topic进行消息消费外,RocketMQ还提供了根据key进行消息查询的功能。该查询是通过store目录中的index子目录中的indexFile进行索引实现的快速查询。当然,这个indexFile中的索引数据实在包含了key的消息被发送到Broker时写入的。如果消息中没有包含key,则不会写入。
12.3.1indexFile结构
每个Broker中包含一组indexFile,每个indexFile都是以一个时间戳命名的(这个indexFile被创建时的时间戳)。每个indexFile由三部分构成:indexHeader,slots槽位,indexes索引数据。每个indexFile文件中包含500W个slot槽。而每个slot槽又可能会挂在很多的index索引单元。
如何将slot的值修改为该index索引单元的indexNo?
indexNo是索引单元在indexes的标号,indexes中存放着所有的索引单元,slot槽中存放的是最新的索引单元对应的indexNo,indexes中的索引单元是按照索引单元存入时间顺序放入的,indexNo等于消息索引在indexed中第几个索引单元的值减一,因为在indexex中的索引单元是以0开始的
12.3.2 文件名的作用
indexFile的文件名为当前文件被创建时的时间戳。这个时间戳有什么用处?
根据业务key进行查询时,查询条件除了key之外,还需要指定一个要查询的时间戳,表示要查询不大于该时间戳的最新消息,即查询指定时间戳之前存储的最新消息。这个时间戳文件可以简化查询,提高查询效率。
indexFile文件是何时被创建的?其创建的条件有两个:
- 当第一条带有key的消息发送来后,系统发现没有indexFile,此时会创建一个indexFile文件
- 当一个index File中挂载的index索引单元数量超过2000W个时,会创建新的indexFile。当带key的消息发送到来后,系统会找到最新的indexFile,并从indexHeader的最后字节中读取到indexCount。若index Count>=2000W时,会场艰辛的indexFile
由此可以推算出,一个index File的最大大小是(40+500w*+ 2000W*20)字节
12.3.3查询流程
当消费者通过业务key来查询相应的消息时,其需要经过一个相对比较负责的查询流程。不过,在分析查询流程之前,首先要清楚几个定位计算的式子:
12.4消息的消费
消费者从Broker中获取消息的方式有两种:pull拉取方式和push推送方式。消费者组对于消息消费的模式又分为两种:集群消费Clustering和广播消费Broadcasting
12.4.1获取消费方式
拉去式消费
Consumer主动从Broker中拉取消息,主动权由Consumer控制。一旦获取了批量消息,就会启动消费过程。不过,该方式的实时性较弱,即Broker中有了新的消息时消费者并不能及时发现并消费。
由于拉去时间间隔是由用户指定的,所以以在设置该间隔时需要注意平稳:间隔太短,空请求比例会增加;间隔太长,消息的实时性太差
推送式消费
该模式下Broker收到数据后会主动推送给Consumer。该获取方式一般实时性较高。
该获取方式是典型的发布-订阅模式,即Consumer向其关联的Queue注册了监听器,一旦发现有新的消息到来就会触发回调的执行,回调方法是Consumer取Queue拉取消息。而这些都是基于Consumer与Broker间的长连接。长连接的维护是需要消耗系统资源的。
对比
- pull:需要应用去实现对关联Queue的遍历,实时性差;但便于应用控制消息的拉去。
- push:封装了对关联Queue的遍历,实时性较强,但会占用较多的系统资源
12.4.2消费模式
广播模式
广播消费模式下,相同Consumer Group的每个Consumer实力都接受同一个Topic的全量消息。即每条消息都会被发送到Consumer Group中的每个Consumer
集群消费
集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊到同一个Topic的消息。即每条消息只会被发送到Consumer Group 中的某个Consumer
消费进度保存
- 广播模式:消费进度保存在consumer端。因为广播模式下consumer group中每个consumer都会消费所有的消息,但他们的消费进度是不同。所以consumer各自保存各自的消费进度。
- 集群模式:消费进度保存在broker中。consumer group中的所有consumer共同消费同一个Topic中的消息,同一条消息只能被消费一次,消费进度会参与到了消费的负载均衡中,故消费进度是需要共享的。
12.4.3 Rebalance机制
Rebalance概念
Rebalance即在均衡,指的是,将一个Toic下的多个Queue在同一个ConsumerGroup中的多个Consumer间进行重新分配的过程。
Rebalance机制的本意是为了提高消息的并行消费能力。列入,一个Topic下5个队列,在只有一个消费者的情况下,这个消费者将负责消费这5哥队列的消息。如果我们此时增加一个消费者,那么就可以给其中一个消费者分配两个队列,给另一个分配3个队列,从而提高下oxide并行消费能力。
Rebalance产生的危害
消费暂停
在只有一个Consumer时,其负责消费所有的队列;再新增一个Consumer后,会触发Rebalance的发生。此时原Consumer就需要暂停部分队列的消费,等到这些队列分配给新的Consumer后,这些暂停消费的队列才能继续被消费。
消费重复
Consumer在消费新分配给自己的队列时,必须接着之前Consumer提交的消费进度的offset继续消费。然而默认消费下,offset是异步提交的,这个异步行导致提交到Broker的offset与Consumer实际消费的消息的不一致。这个不一致的插值就是可能重复消息的消息
消费突刺
由于Rebalance可能导致重复消费,如果需要重复消费的过多,或者因为Rebalance暂停时间过长从而导致挤压了部分消息。那么有可能会导致在Rebalance结束之后瞬间需要消费很多消息。blog.csdnimg.cn/89a52a00c5f9419faa9febb8fe2bbf3f.png)
Rebalance产生的原因
导致Rebalance产生的原因,无非就两个:消费者所订阅Topic的Queue数量发生变化,或消费者组中消费者数量发生变化
Rebalance过程
在Broker中维护着多个Map集合,这些集合中动态存放着Topic中Queue的消息、Consumer Group中Consumer实例的信息。一旦发现消费者所订阅的Queue数量发生变化 ,或消费者组中消费者数量发生变化,立即向Consumer Group中每个实例发出Rebalance通知。
Consumer实例在接收到通知后会采用Queue分配算法自己获取到相应的Queue,即由Consumer实力自行进行Rebalance。
Broker中的配置:
TopicConfigManager:key是topic名称,value是TopicConfig,TopicConfig中维护着该Topic中所有Queue的数据
ConsumerManager:key是ConsumerGroupId,Value是ConsumerCroupInfo,Consumer GroupInfo中维护着该Group中所有Consumer示例数据。
ConsumerOffsetMagager:key位Topic与订阅该Topic的Group的组合,value是一个内层Map。内层Map的key为QueueId,内层Map的Value为该Queue的消费进度Offset
Queue分配算法
一个Topic中的Queue只能由Consumer Group中的一个Consumer进行消费,那么他们间的配对关系是如何关系,即Queue要分配给哪个Consumer进行消费?也要有算法策略的。常见的有四种策略。这些策略是通过在创建Consumer是的构造器进行进去的。
平均分配策略
该算法是要根据avg=QueueCount / ConsumerCount的计算结果进行分配的。如果能够整除,则按顺序将avg哥Queue逐个分配Consumer;如果不能整除,则将多余的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算法存在的意义:
其可以有效减小由于消费者组扩容或缩容所带来的大量的Rebalance。
一致性hash算法的应用场景:
Consumer数量变化比较频繁的场景。
至少一次原则
RocketMQ有一个原则:每条消息必须要被成功消费一次。
那么什么是成功消费?Consumer在消费完消息会向其消费进度记录器提供其消费消息的offset,offset被成功记录到纪录器中,那么这条消费就被成功消费了。
什么是消费进度记录器?
对于广播消费模式来说,Consumer本身就是消费进度记录器
对于集群消费模式来说,Broker是消费进度记录器
12.5订阅关系的一致性
订阅关系的一致性指的是,同一个消费者组(GroupID相同)下所有Consumer实例所订阅的Topic与Tag及时对消息的处理逻辑必须完全一致。否则,消息消费的逻辑就会混乱,甚至导致消息丢失。
12.5.1正确订阅关系
多个消费者组订阅了多个Topic,并且每个消费者组里的多个消费者实例的订阅关系保持一致。
12.5.2错误的订阅关系
一个消费者组订阅了多个Topic,但是该消费者组里的多个Consumer实力的订阅关系并没有保持一致。
订阅了不同Topic
该例中的错误在于,同一消费者组中的两个Consumer实例订阅了不同的Topic
订阅了不同的Tag
该例中的错误在于,同一个消费者组中的两个Consumer订阅了相同的Topic的不同Tag
订阅了不同数量的Topic
该例中的错误在于,同一个消费者组中的两个Consumer订阅了不同数量的Topic。
12.6offset管理(consumer的消费offset)
消费进度offset用于记录每个Queue的不同消费组的消费的。根据消费进度记录器的不同,可以分为两中模式:本地模式和远程模式
12.6.1offset本地管理模式
当消费模式为广播模式,offset使用本地模式储存。因为每条消息会被所有的消费者消费,每个消费者管理自己的消费进度,各个消费之间不存在消费进度的交集。
Consumer在广播消费模式下offset相关数据以json的形式持久化到Consumer本地磁盘文件中,默认文件路径为当前用户主目录下的.roketmq_offsets/&{clientId}/${group}/offsets.json
。其中,clientid为当前消费者id,默认为ip@DEFAULT;group为消费者组名称。
12.6.2offset远程管理模式
当消费模式为集群消费时,offset使用远程模式管理,因为所有Consumer实例对消息采用的是对均衡消费,所有Consumer共享Queue的消费进度。
Consumer在集群消费模式下offset相关数据以json的形式持久化到Broker磁盘文件中,文件路径为当前用户主目录下的store/config/consumeroffset.josn
Broker启动时会加载这个文件,并写入到一个双层Map,外层Map的key为topic@group,value为内层map,内层map的key为queueId,value为offset。当发生Rebalance时,新的Consumer会从该Map中获取到相应的数据来继续消费。
集群模式下offset采用远程管理模式,主要是为了保证Rebalance机制。
12.6.3offset用途
消费者是如何从最开始持续消费消息的?消费者要消费的第一条消息的起始位置是用户自己通过consumer.setConsumerFromWhere()方法指定的。
在Consumer启动后,其要消费的第一条消息的起始位置常用的由三种,这三种位置可以通过模具类型常量设置。这个枚举的类型为ConsumeFromWhere
CONSUME_FROM_LAST_OFFSET: 从queue的当前最后一条消息开始收费的
CONSUME_FROM_FIRST_OFFSET:从queue的第一条消息开始消费
CONSUME_FROM_TIMESTAMP:从指定的具体时间戳位置的消息开始消费,这个具体时间戳是通过另一语句指定的。
consumer.setConsumerTimestamp(“20221001080000”)yyyyMMddHHmmss
当消费完一批消息后,Consumer会提交其消费进度offset给Broker,Broker在收到消息进度后会将其更新到那个双层Map(ConsumerOffsetManager)及consumerOffset.json文件中,然后向该Consumer进行ACK,而ACK内容中包含了三项数据:当前队列的最小offset、最大offset、及下次消费的起始offset(nextBeginOffset)
12.6.4重试队列
当rocketMQ对消息的消费出现异常时,会将发生异常的消息的offset提交到Broker中的重试队列。系统在发生消息消费异常时会为当前的topic@group创建一个重试队列,该队列以%RETRY%开头,到达重试时间后进行消费重试。
12.6.5offset的同步提交和异步提交
集群消费模式下,Consumer消费完消息后会向Broker提交消费进度offset,其提交方式分为两种:同步提交
:消费者在消费完一批消息后回想Broker提交这些消息的offset,然后等待broker的成功响应。如在等待超时之前收到了成功相应,则继续读取下一批消息进行消费。 若没有受到响应,则会重新提交,直到获取到相应。而在这个等待过程中,消费者是阻塞的。其严重影响了消费者的吞吐量。异步提交
:消费者在消费完一批消费后向Broker提交offset,但无需等待Broker的成功响应,可以继续读取并消费下一批消息。这种方式增加了消费者的吞吐量。但需要注意,broker在收到提交的offset后,还是会像消费者进行相应。
异步提交时,下一个offset是存放到broker中的
12.7消费幂等
12.7.1什么时消费幂等
当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并未对业务系统产生任何负面影响,那么这个消费过程是消费幂等。
幂等:若某操作执行多次与执行一次对系统产生的影响是相同的,则称该操作时幂等操作。
在互联网应用中,尤其在网络不稳定的情况下,消息很有可能会出现重复发送或者重复消费。如果重复的消息可能会影响业务处理,那么就应该对消息做幂等处理。
12.7.2消息重复的场景分析
什么情况下可能出现消息被重复消费?最常见的有以下三种情况:
12.7.3通用解决方案
两要素
幂等解决方案的设计中涉及到两项要素:幂等令牌,与唯一性处理。只要充分利用好这两要素,就可以设计出好的幂等解决方案。
- 幂等令牌:是生产者与消费者两者中的既定协议,通常指具备唯一业务标识的字符串
- 唯一性处理:服务端通过采用一定的算法逻辑,保证同一个逻辑不会被重复执行多次
解决方案
对于常见的系统,幂等性操作的通用性解决方案是:
1.首先通过缓存去重。在缓存中如果已经存在了某幂等令牌,则说明本次操作是重复性操作;若缓存没有命中,则进入下一步。
2.在唯一性处理之前,现在数据库中查询幂等令牌作为索引是否存在。若存在,则说明本次操作作为重复性操作;若不存在,则进入下一步。
3.同一事务中完成三项操作:唯一性处理后,将幂等令牌写入到缓存中去,并将幂等令牌作为唯一索引的数据写入到DB中。
第一部已经判断过是否是重复性操作了,为什么第二部还要再次判断?能够进入第2步,说明已经不是重复操作了,第2次判断是否重复?
当然不重复,一般缓存中的数据是具有有效期的,缓存中数据的有效期一旦过期,就是发生缓存穿透,事情i去的就到达了DBMS
12.7.4消息幂等的实现
消费幂等的解决方案很简单:为消息指定不会重复的唯一标识。因为MessageId有可能出现重复的情况,因为真正安全的幂等处理,不建议使用MessageId作为处理依据。最好的方式是以业务唯一标识作为幂等出力的关键依据,而业务的唯一标识可以通过消息key设置。
12.8消息堆积和延迟
12.8.1概念
消息处理流程中,如果Consumer的消费速度跟不上Producer的发送速度,MQ中未处理的消息就会越来越多,这部分消息就被称为消息堆积。消息出现堆积进而会造成消息的消费延迟。以下场景需要重点关注消息堆积和消费延迟问题:
- 业务系统上下有能力不匹配造成的持续堆积,且无法自行恢复
- 业务系统对消息的消费实时性要求较高,即使是短暂的堆积造成的消费延迟也无法接受
12.8.2产生原因分析
Consumer使用长轮询Pull模式消费消息时,分为以下两个阶段:
拉去消息
Consumer通过长轮询Pull模式批量拉取的方式从服务端获取消息,将拉取到的消息缓存到本地缓冲队列中。对于拉去是消费,在内网环境会有很高的吞吐量,所以这一阶段一般不会成为消息堆积的瓶颈。
一个 单线程单分区的低规格主机Consumer,其达到几万的TPS。如果多个分区多个线程,则可轻松达到几十万的TPS
消息消费
Consumer将本地缓存的消息提交到消息线程中,使用业务消费逻辑对消息进行处理,处理完毕后获取到一个结果。这是真正的消息消费过程。此时Consumer消费能力完全依赖于雄安锡的消费耗时和消费并发度了。如果由于业务处理逻辑复杂等原因,导致处理单挑信息的耗时比较长,则整体的消息吞吐量肯定不会高,此时就会导致Consumer本地缓存队列达到上限,停止从服务端拉去消息。
结论
消息堆积的主要瓶颈在于客户端的消费能力,而消费能力有消费耗时和消费并发度决定。注意,消费耗时的优先级要高于消费并发度。即在保证了消费耗时的合理性前提下,在考虑消费并发度问题。
12.8.3消费耗时
影响消息处理时长的主要因素是代码逻辑。而代码逻辑中可能影响处理时长代码主要有两种类型:CPU内部计算代码和外部I/O操作型代码。
通常情况下代码中如果没有负责的递归和循环的话。内部计算耗时相对外部I/O操作来收几乎可以忽略,所以外部IO型代码是影响消息处理时长的主要症状
12.8.4消息并发度
一般情况下,消费者端的消费并发度有单节点线程数和节点数量工程决定,其值伟单节点线程数和节点数量。不过,通常需要优先调节单节点的线程数,如单机硬件资源达到了上限,则需要通过横向扩展来提高消费并发度。
单节点线程数,即单个Consumer所包含的线程数量
节点类型,即Consumer Group所包含的Consumer数量
对于普通消息、延时消息及事务消息,并发度计算都是单节点线程数*节点数量。但对于顺序消息则是不同的。顺序消息的消费并发度等于Topic的Queue分区数量。
1)全局顺序消息:该类型消息的Topic只有一个Queue分区,其可以保证该Topic的所有消息被顺序消费。为了保证这个全局顺序性,Consumer Group中在同一时刻只能有一个Consumer的一个线程进行消费,所以其并发度为1
2)分区顺序消息:该类型消息的Topic中有多个Queue分区,其尽可以保证该Topic的每个Queue分区中的消息被顺序消费,不能保证整个Topic中消息的顺序消费。为了保证这个分区顺序性,每个Queue分区中的消息在Consumer Group中的同一时刻只能有一个Consumer的一个线程被消费。即,在同一时刻最多会出现多个Queue分区有多个Consumer的多个线程并行消费。所以其并发度为Topic的分区数量。
12.8.5单机线程数的计算
对一台主机中线程池中线程数的设置需要谨慎,不能盲目直接调大线程数,设置过大的线程数反而会带来大量的线程切换的开销。理想环境下单结点的最优线程数计算模型为:C*(T1 + T2) / T1
- C:CPU内核数
- T1:CPU内部逻辑计算耗时
- T2:外部IO操作耗时
上式可以转换为C + C*T2/T1
注意,该计算的数值是理想i情况下的理论数值,在生产环境中,不建议直接使用,而是根据当前环境,先设置一个比该值小的数值然后观察其压测效果,然后再根据效果逐步调大线程数,直到找到该环境中性能最佳时的值。
12.8.6如何避免
为了避免在业务使用时出现非预期的消息堆积和消费延迟问题,需要在前期设计阶段对整个业务逻辑进行完善的排查和梳理,其中最重要的就是梳理消息的消费延迟和设置消息消费的并发度。
梳理消息的消息耗时
通过压测获取消息的消费耗时,并对耗时较高的操作的代码逻辑进行分析,梳理消息的消费耗时需要关注一下信息:
- 消息消费逻辑的计算复杂度是否过高,代码是否存在无限循环和递归等缺陷。
- 消息消费逻辑的I/O操作是否是必须的,能否用本地缓存等方案规避。
- 消费逻辑的复杂耗时的操作是否可以做异步化操作,如果可以,是否会造成逻辑错乱。
设置消费并发度
对于消息消费并发度的计算,可以通过以下两步实施:
- 逐步调大单个Consumer节点的线程数,并观测结点的性能指标,得到单个节点最优的消费线程数和消费吞吐量。
- 根据上下游链路的流量峰值计算出需要设置的节点数
消息的清除
消息是被顺序存储在CommitLog文件的,且消息大小不定长,所以消息的清理是不可能以消息为单位进行清理的,而是以commitlog文件为单位进行清理的,否则会急剧下降清理效率,并实现逻辑复杂。
commitlog文件存在一个过期时间,默认为72小时,即三天,除了用户手动清理外,一下情况下也会被自动清理,无论文件中的消息是否给消费过:
- 文件过期,且达到清理时间点(默认为凌晨4点)后,自动清理过期文件。
- 文件过期,且磁盘空间占用率以达到过期清理警戒线(默认75%)后,无论是否达到清理时间点,都会自动清理过期文件。
- 磁盘占用率达到清理警戒线(默认为85%)后,开始按照设定好的规则清理文件,无论是否过期,默认会从最老的文件开始清理。
- 磁盘占用率达到系统危险警戒线(默认为90%)后,Broker将拒绝消息写入。
需要注意以下几点:
1)对于RockerMQ系统来说,删除一个1G大小的文件,是一个压力巨大的IO操作,在删除过程中,系统性能会骤然下降,所以,其默认请i时间点为凌晨四点,访问量最小的时间,也正因如此,我们要保障空间的空闲率,不要是系统出现在其他时间点删除Commitlog文件的情况。
2)官方建议RockerMQ服务的Linux文件系统采用ext4,性能较好。
13.RockerMQ应用
13.1普通消息
Producer对于消息的发送方式也有多种选择,不同的方式会产生不同的系统效果。
同步发送消息
同步发送消息是指,Prodecer发出一条消息后,会在收到MQ返回的ACK之后才发下一条消息。该方式的消息可靠性最高,但消息发送效率较低。
异步发送消息
异步发送消息是指,Producer发送消息后无需等待MQ返回ACK,直接发送下一个消息。该方式的消息可靠性可以得到保障,消息发送效率也可以。
单向发送消息
单向发送消息是指,Producer仅负责发送消息,不等待、不处理MQ的ACK,该发送方式时MQ也不返回ACK,该方式的消息发送效率最高,但消息可靠性较差。
Producer代码示例
/**
* 同步生产消息
*/
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
//1.创建生产者对象
DefaultMQProducer producer = new DefaultMQProducer("pg");
//2.设置nameserver地址
producer.setNamesrvAddr("192.168.18.128:9876");
//3.生产者开始消费
producer.start();
for (int i = 0; i < 100; i++) {
byte[] body = ("你好," + i).getBytes();
//创建消息,设置主题、标签和内容
Message message = new Message("someTopic", "one", body);
//设置业务标识key
message.setKeys("keys-" + i);
SendResult result = producer.send(message);
System.out.println(result);
}
//4.关闭生产
producer.shutdown();
}
/**
* 异步发送消息
*/
public static void main(String[] args) throws Exception {
//创建生产者
DefaultMQProducer producer = new DefaultMQProducer("pg1");
//设置nameserver地址
producer.setNamesrvAddr("192.168.18.128:9876");
//设置重试
producer.setRetryTimesWhenSendAsyncFailed(0);
//设置该topic的默认Queue数量
producer.setDefaultTopicQueueNums(2);
//开启生产者
producer.start();
//生产者发送消息
for (int i = 0; i < 100; i++) {
byte[] body = ("body:" + 1).getBytes();
Message message = new Message("mytopic", "mytag", body);
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
@Override
public void onException(Throwable throwable) {
throwable.printStackTrace();
}
});
}
//睡眠一会,因为采用的是异步发送,Producer无需接收到MQ发送的ACK,所以Producer不能判断当前消息是否发送成功,所以,如果不在此处做睡眠操作,
// 就会造成消息尚未发送成功,生产者就关闭的情况
TimeUnit.SECONDS.sleep(3);
//关闭生产者
producer.shutdown();
}
/**
* 单向发送消息
*/
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("oneWay");
producer.setNamesrvAddr("192.168.18.128:9876");
producer.start();
for (int i = 0; i < 100; i++) {
byte[] body = ("body:" + 1).getBytes();
Message message = new Message("oneWay", "test", body);
producer.sendOneway(message);
}
producer.shutdown();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
Consumer代码示例
/**
* 消费者消费消息
*/
public static void main(String[] args) throws Exception {
//定义一个消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
//设置Nameserver地址
consumer.setNamesrvAddr("192.168.18.128:9876");
//设置从第一条开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//指定消费topic和tag
consumer.subscribe("someTopic", "*");
//指定广播消费,默认为集群模式
consumer.setMessageModel(MessageModel.BROADCASTING);
//注册监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list,
ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt item : list) {
System.out.println(item);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//开始消费
consumer.start();
System.out.println("Consumer Started");
//关闭消费者
//consumer.shutdown();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
13.2顺序消息
13.2.1什么是顺序消息
顺序消息指的是,严格按照消息的发送顺序进行消费的消息。
默认情况下生产者会把消息以Round Robin轮询方式发送到不同的Queue分区队列中;而消费消息时会从多个Queue上拉取消息,这种情况下的发送和消费不能保证顺序的。如果将消息仅发送到同一Queue中,消费时也只从这个Queue上拉取消息,就严格保证了消息的顺序性。
13.2.2为什么要顺序消息
12.2.3有序性分类
根据有序性范围的不同,RockerMQ可以严格地保证两种消息的有序性:分区有序和全局有序
全局有序
当发送和消费参与的Queue只有一个时所保证的有序是整个Topic中消息的顺序,成为全局有序。
在创建Topic时指定Queue的数量,有三种指定方式:
1)在代码中创建Producer时,可以指定自动创建Topic的Queue数量
2)在RockerMQ可视化控制田中手动创建Topic时指定Queue的数量
3)使用mqadmin命令手动创建Topic时指定Queue的数量
分区顺序
如果有多个Queue参与,其仅可保证在该Queue分区队列上的消息顺序,则成为分区有序
如何实现Queue的选择?在定义Producer时我们可以指定消息队列选择器,而这个选择其实我们自己实现了MessageQueueSelector接口定义的。
在定义选择器的选择算法时,一般需要使用选择key,这
个选择key可以是消息key也可以是其他数据。但无论谁做选择key,都不能重复,都是唯一的。
一般的选择算法是,让选择key(或者hash值)与该Topic所包含的Queue的数量去模,其结果即为选择出的QUeue的QueueID。
驱模算法存在一个问题:不同选择key与Queue数量去模结果可能是相同的,及不同选择key的消息可能会出现相同的queue,即同一个Consumer可能会消费到不同选择key的消息。这个问题如何解决?一般性的做法是:从消息中获取到选择key,对其进行判断,若是当前Consumer需要消费的消息,则直接消费,否则什么都不做,这种做法要求选择key要能够随着消息一起被Consumer获取到,此时使用消息key作为选择key是最好的做法。
代码实现(分区有序,全局有序需要Producer设置QueueNums)
/**
* 发送顺序消息
*/
public static void main(String[] args) throws Exception {
String orderId = "1";
DefaultMQProducer producer = new DefaultMQProducer("op");
producer.setNamesrvAddr("192.168.18.128:9876");
producer.start();
for (int i = 0; i < 4; i++) {
byte[] body = ("keys:" + i).getBytes();
Message message = new Message("orderTopic", "orderTag", body);
message.setKeys(orderId);
producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
String keys = message.getKeys();
Integer key = Integer.valueOf(keys);
int index = key % list.size();
return list.get(index);
}
}, orderId);
}
producer.shutdown();
System.out.println("执行完成");
}
/**
* 消费顺序消息
*/
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("og");
consumer.setNamesrvAddr("192.168.18.128:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("orderTopic", "*");
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
list.stream().filter(item -> item.getKeys().equals("1"))
.forEach(item -> {
System.out.println("当前消息的内容为:" + new String(item.getBody()));
});
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("消费完成");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
12.3 延时消息
12.3.1什么是延时消息
当消息写入到Broker后,在指定的市场后才可被消费处理的消息,成为延时消息。
采用RockerMQ的延时消息可以实现定时任务的功能,而无需使用定时器。典型的应用场景是,电商交易中超时未支付关闭订单的场景,12306平台超时未支付取消订票的场景。
12.3.2延时等级
延时消息的延迟时长不支持随意市场的延迟,是通过特定的延迟等级来制定的。延迟等级定义在RockerMQ服务端的MessageStoreConfig类中的如下变量:
即,若指定的延时等级为3,则表示延迟时长为10S,即延迟等级是从1开始计数的。
当然,如果需要自定义的延时等级,可以通过在Broker加载的配置中新增如下配置(例如下面增加了1天这个等级1d)。配置文件在ROckerMQ安装目录下的Conf目录中。
12.3.3延时消息实现原理
Producer将消息发送到Broker后,Broker会首先将消息写入到commitLog文件,然后需要将其分发到相应的consumeQueue。不过,在这之前,系统会先判断消息中是否带有延时等级。若没有,则正常分发;否则需要经历一个复杂的过程:
- 修改消息的Topic为SCHEDULE_TOPIC_XXXX
- 根据延时等级,在consumequeue目录中SCHEDULE_TOPIC_XXXX主题下创建出相应的queueid目录与consumequeue文件。
- 修改消息索引单元的内容。索引单元中的Message Tag HashCode部分原本存放的是消息的Tag的Hash值。先修改为消息的投递时间。投递时间是指该消息被重新修改为原Topic后再次写入到commitlog中的时间。投递时间 = 消息存储时间 + 延时时间。消息存储时间指的是消息被发送到Broker的时间戳。
- 将消息索引写入到SCHEDULE_TOPIC_XXXXX主题下相应的consumequeue中。
SCHEDULE_TOPIC_XXXXX目录中各个延迟等级Queue中的消息是如何排序的?
是按照消息投递时间排序的。一个Broker中同一等级的所有延时消息会被写入到Consumequeue目录下SCHEDULE_TOPIC_XXXX目录下相同的queue中。即一个queue中消息投递时间的延迟登记时间是相同的。那么投递时间就取决于消息存储时间了,即按照消息被发送到Broker的时间进行排序的。
投递延迟消息
Broker内部有一个延迟消息服务类ScheduleMessageService,其会消费SCHEDULE_TOPIC_XXXXX中的消息,即按照每条消息的投递时间,将延时消息投递到目标Topic中。不过,在投递之前会从commitlog将原来的消息再次读出,并将其原来的延时等级设置为0,即原消息变为一条不延时的普通消息。然后再次将消息投递到目标Topic中。
将消息重新写入到commitlog
延迟消息服务类将延迟消息再次发送给commitlog,并再次形成新的消息索引条目,分发到相应的Queue
代码演示
/**
* 生产延时消息
*/
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("dpg");
producer.setNamesrvAddr("192.168.18.128:9876");
producer.start();
byte[] body = ("延时消息").getBytes();
Message message = new Message("delayTopic", "test", body);
message.setDelayTimeLevel(3);
SendResult result = producer.send(message);
System.out.println("当前时间为:" + LocalDateTime.now());
System.out.println(result);
producer.shutdown();
}
/**
* 消费延时消息
*/
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("dcg");
consumer.setNamesrvAddr("192.168.18.128:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.subscribe("delayTopic", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
System.out.println(new String(list.get(0).getBody()));
System.out.println("消费时间为:" + LocalDateTime.now());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("结束时间为:" + LocalDateTime.now());
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
12.4事务消息
12.4.1问题引入
12.4.2解决思路
解决思路是,让第1、2、3步具有原子性,要么全部成功,要么全部失败,即消息发送成功后,必须要保证扣款成功。如果扣款失败,则回滚发送成功的消息。而该思路就是使用事务消息。
12.4.3基础
分布式事务
对于分布式实物,通俗地说就是,一次操作有若干分支操作组成,这些分支操作分属不同应用,分布在不同服务器上。分布式事务需要保证这些分支操作要么全部成功,要么全部失败。分布式事务与普通事务一样,就是为了保证操作结果的一致性。
事务消息
RockerMQ提供了类似X/Open XA的分布式事务功能,通过事务消息能达到分布式事务的最终一致。XA是一个分布式事务的解决方案,一种分布式事务处理模式。
半事务消息
暂不能投递的消息,发送发已经成功地将消息发送到了Broker,但是Broker未收到最终确认指令,此时该消息被标记成"暂不能投递”装填,即不能被消费者看到。处于这种状态下的消息即半事务消息。
本地事务状态
Produer回调操作执行的结果为本地事务状态,其会发送给TC,而TC会再发送给TM。TM会根据TC发送来的本地事务状态来确定全局事务确认指令。
消息回查
消息回查,即重新查询本地事务的执行状态。本例就是重新到DB中查看预扣款操作是否执行成功。
RocketMQ中消息回查设置:
12.4.4XA模式三剑客
XA协议
TC
Transaction Coordinator,事务协调者。维持全局和分支事物的状态,驱动全局事务提交或回滚。RocketMQ中Broker充当着TC。
TM
Transaction Manager,事务管理器,定义全局事务的范围:开始全局事务、提交或回滚全局事务。它实际是全局事务的发起者。
RocketMQ中事务消息的Producer充当着TM
RM
Resource Manager,资源管理器。管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回归。
12.4.5XA模式架构
12.4.5 注意
- 事务消息不支持延时消息
- 对于事物消息要做好幂等性检查,因为事务消息可能不止一次被消费(因为存在回滚后再提交的情况)
12.5 批量消息
12.5.1 批量发送消息
发送限制
生产者进行消息发送时可以一次发送多条消息,这可以大大提升Producer的发送效率。不过需要注意以下几点:
- 批量发送的消息必须具有相同的Topic
- 批量发送的消息必须具有相同的刷盘策略
- 批量发送的消息不能是延时消息与事务消息
批量发送大小
默认情况下,一批发送的消息总大小不能超过4MB字节,如果想超出该值,有两种解决方案:
- 方案一:将批量消息进行拆分,拆分为若干不大于4M的消息集合分多次批量发送
- 方案二:在Producer端和Broker端修改属性
- producer端需要在发送之前设置Producer的maxMessageSize属性
- Broker端需要修改其加载的配置文件中maxMessageSize属性
生产者发送的消息的大小
生产者通过send()发送的Message,并不是直接将Message序列化后发送到网路上的,而是通过这个Message生成一个字符串发送出去的。这个字符串由四部分组成:Topic、消息Body、消息日志(占20字节),及用于描述消息的一堆属性key-value。这些属性中包含列入生产者地址、生产时间、要发送的QueueId等。最终写入到Broker中消息单元的数据都是来自于这些属性。
12.5.2 批量消费消息
修改批量属性
Consumer的MessageListenerConcurrent监听接口的consume Message方法的第一个参数为消息列表,但默认情况下每次只能消费一条消息,若要使其一次可以消费多条消息,则可以通过修改Consumer的ConsumeMessageBatchMaXSize属性来指定,不过,该值不能超过32.因为默认情况下消费者每次可以拉取的消息最多是32条,若要修改一次拉去的最大值,则可通过修改Consumer的pullBatchSize属性来指定。
存在的问题
Consumer的Pull Batch Sizes属性与ConsumerMessageBatchMaxSize属性是否设置的越大越好?当然不是。
- pullBatchSize值设置的越大,Consumer每次拉取一次需要的时间就会越长,且在网路上传输问题的可能性就越高。若在拉去过程中若出现了问题,那么本批次所有消息都需要重新拉去。
- consumeMessageBatchMaxSize值设置的越大,Consumer的消息并发消费能力越低,且这批被消费的消息就有相同的消费结果,因为consumeMessageBatchMaxSize指定的一批消息只会使用一个线程进行处理,且在处理工程中只要有一个消息处理异常,则这批消息需要全部重新再次消费处理。
12.5.3 代码示例
producer.setMaxMessageSize(); //设置发送消息的大小
consumer.setPullBatchSize(13);//设置每次拉取消息的大小
consumer.setConsumeMessageBatchMaxSize(13);//设置批量消费消息的最大值
- 1
- 2
- 3
由于生产者批量发送消息的大小不能大于4M(默认,可以修改)所以需要对大于4M的批量消息进行分割
class ListSplitter implements Iterator<List<Message>> {
private int sizeLimit = 1000 * 1000;
private final List<Message> messages;
private int currIndex;
public ListSplitter(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);
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; //for log overhead
if (tmpSize > sizeLimit) {
//it is unexpected that single message exceeds the sizeLimit
//here just let it go, otherwise it will block the splitting process
if (nextIndex - currIndex == 0) {
//if the next sublist has no element, add this one and then break, otherwise just break
nextIndex++;
}
break;
}
if (tmpSize + totalSize > sizeLimit) {
break;
} else {
totalSize += tmpSize;
}
}
List<Message> subList = messages.subList(currIndex, nextIndex);
currIndex = nextIndex;
return subList;
}
@Override
public void remove() {
throw new UnsupportedOperationException("Not allowed to remove");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
12.6消息过滤
消费者进行消息订阅时,除了可以指定要订阅消息的Topic外,还可以对指定Topic中的消息根据指定添加进行过滤,即即可以订阅比Topic更加细粒度的消息类型。
对于指定Topic消息的过滤有两种过滤方式:Tag过滤与SQL过滤
12.6.1Tag过滤
通过consumer的subscribe()方法指定要订阅消息的tag。如果订阅多个Tag的消息,Tag间使用或运算符(双竖线||)连接
12.6.2SQL过滤
SQL过滤是一种通过特定表达式对事先埋入到消息中的用户属性进行筛选过滤的方式。通过SQL过滤,可以实现对消息的复杂过滤,不过,只有使用PUSH模式的消费者才能使用SQL过滤。
SQL过滤表达式中支持多种常量类型和运算符。
12.6.3代码示例
首先要将Broker的SQLPropertiesFileter开启
enablePropertyFilter=true
- 1
/**
* 生产者
*/
public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("sqlpg");
producer.setNamesrvAddr("192.168.18.128:9876");
producer.start();
for (int i = 0; i < 100; i++) {
Message message = new Message("topicA", "filter", String.valueOf(i).getBytes());
message.putUserProperty("age", String.valueOf(i));
SendResult result = producer.send(message);
System.out.println(result);
}
producer.shutdown();
}
/**
* 消费者
*/
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("fcg");
consumer.setNamesrvAddr("192.168.18.128:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.subscribe("topicA", MessageSelector.bySql("age > 90"));
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
list.forEach(System.out::println);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("结束");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
12.7消息发送重试机制
12.7.1说明
Producer对发送失败的消息进行重新发送的机制,称为消息发送重试机制,也成为消息重投机制。
对于消息重投,需要注意以下几点:
- 生产者再发送消息时,若采用同步或者异步方式,发送失败会重试,但oneway消息发送方式发送消息失败是没有重试机制的。
- 只用普通消息具有发送重试机制,顺序消息是没有的
- 消息重投机制可以保证消息尽可能发送成、不丢失,但可能会造成消息重复。消息重复在RocketMQ中是无法避免的问题
- 消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会成为大概率事件
- producer主动重发,consumer负载变化也会导致重复消息(Rebalance)
- 避免消息重复消费的解决方案是,为消息添加唯一标识,是消费者对消息进行消费判断来避免重复消费
- 消息发送重试有三种策略可以选择:同步发送失败策略,异步发送失败策略/消息刷盘策略
12.7.2同步发送策略
对于普通消息,消息发送默认采用round-robin策略来选择所发送到的队列。如果发送失败,默认重试2次,但在重试时是不会选择上次发送失败的Broker,而是选择其他Broker,当然,若只有一个Broker其也只能发送到该Broker,但其会尽量发送到该Broker上的其他Queue。
producer.setRetryTimesWhenSendFailed(2);
producer.setSendMsgTimeout(5000);
- 1
- 2
同时,Broker还具有失败隔离功能,使Producer尽量选择未发送过发送失败的Broker作为目标Broker。其可以保证其他消息尽量不发送到问题Broker,为了提高消息发送效率,降低消息发送耗时。
如果超过重试次数,则抛出异常,由Producer去保证消息不丢,当然当生产者出现RemotingException,MQClientEeception和MQBrokerException时,Producer会自动重投消息。
12.7.3异步发送失败策略
异步发送失败重试时,异步重试不会选择其他Broker,仅在同一个Broker上做重试,所以该策略无法保证消息不丢失
12.7.4消息刷盘失败策略
消息刷盘超时(Master或Slave)或slave不可用(slave在做数据同步时向master返回状态不是SEND_OK)时,默认是不会将消息尝试发送到其他Broker的。不过,对于重要消息可以通过在Broker的配置文件设置retryAnotherBrokerWHenNotStoreOK属性为true来开启。
12.8消息消费重试机制
12.8.1顺序消息的消息重试
对于顺序消息,当consumer消费消息失败后,为了保证消息的顺序性,其会自动不断地保证消息重试,知道消费成功。消费成功默认间隔时间为1000毫秒,重试期间应用会出现消息消费被阻塞的情况。
12.8.2无序消息的消息重试
对与无须消息(普通消息、延时消息、事务消息),当Consumer消费消息失败时,可以通过设置返回状态达到重试的效果,不过需要注意,无序消息的重试只对集群消费方式生效,广播消费方式不提供失败重试特性,即对于广播消费,消费失败后,失败消费不再重试,继续消费后续消息。
12.8.3消费重试次数与间隔
对于无序消息集群消费下的重试消费,每条消息默认最多重试16次,但每次重试的间隔时间是不同的,会逐渐变长,每次重试的间隔时间如下表。
![在这里插入图片描述](https://img-blog.csdnimg.cn/73ba385d9717401f9d91b9fb161f024c.png
12.8.4重试队列
对于需要重试消费的消息,并不是Consumer在等待了指定时长后再次去拉去原来的消息进行消费,而是将这些需要重试消费的消息放入到了一个特殊Topic的队列中,而后进行再次消费的,这个特殊的队列就是重试队列。
当出现需要进行重试消费的消息时,Broker会为每个消费组设置一个Topic名称%RETRY%consumerGroup@consumerGroup的重试队列
1)这个重试队列是针对消息才组的,而不是针对每个Topic设置的
2)只有当出现需要重试的队列时,才会为该消费者创建重试队列
Broker对于充实消息的处理是通过延时消息实现的,先将消息保存在SCHEDULE_TOPIC_XXXXX演示队列中,延时时间到后,会将消息投递到%RETRY%consumerGroup%consumerGroup重试队列中。
12.8.5消息重试配置方式
集群消费方式下,消息消费失败后若希望消费重试,则需要在消息监听器接口中明确进行如下三种方式之一的配置:
- 方式一:返回ConsumeConcurrentlyStatus.RECONSUME_LATER(推荐)
- 方式二:返回null
- 方式三:抛出异常
12.8.6消费不重试配置方式
集群消费方式下,消息消费失败后若不希望消息重试,则在捕获异常后同样返回与消费成功后相同的结果,即ConsumeConcurrentlyStatus.CONSUME_SUCCESS,则不进行消费重试。
12.9死信队列
12.9.1什么是死信队列
当一条消息初次消费失败后,消息队列会自动进行消费重试;达到最大重试次数后,若消费依然失败,则表明消费者无法在正常情况下无法正确地消费该消息,此时,消息队列不会立即将消息丢弃,而是将其发送到该消费者对应的特殊队列,这个队列就是死信队列(Dead-Latter Queue,DLQ),而其中的消息则称为死信消息(Dead-Letter Message,DLM)
12.9.2死信队列的特征
死信队列具有如下特征:
-死信队列的消息不会再被消费者正常消费,即DLQ对消费者是不可见的
-死信储存时间和正常消息时间相同,均为3天,3天后会自动删除
-死信队列就是一个特殊的Topic,名称为%DLQ%consumerGroup@consumerGroup,即每个消费者组都会有一个死信队列
-如果一个消费者组未产生死信消息,则不会为其创建相应的死信队列。
12.9.3死信消息的处理
实际上,当一条消息进入到死信队列,就意味着系统中某些地方出现了问题,从而导致消费者无法正常消费该消息,比如代码中原本就存在Bug,因此,对于死信消息,通常需要开发人员进行特殊处理,最关键的步骤是排查可以因素,解决代码中存在的bug,然后将原来的私信消息再次进行投递消费。
13.SpringBoot集成RocketMQ
13.1安装步骤
1.本地安装资源包
2.安装(在资源包的目录下执行)
mvn install -Dmaven.skip.test=true
- 1
13.2具体内容
13.2.1Producer发送消息
添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
- 1
- 2
- 3
- 4
- 5
代码示例(Producer)
@SpringBootTest(classes = Test2RocketMqApplication.class)
@RunWith(SpringRunner.class)
public class ProducerSendMessageTest {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
public void test1() {
SendResult result = rocketMQTemplate.syncSend("myTopic1:tag", "唐含含是狗");
System.out.println(result);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
当然RocketMQTmplate提供了很多方法供我们使用,下面我简单列举几个
syncSend(); 同步发送方法
syncSendOrderly();通过指定haskey有序同步发送
asyncSend();异步发送
asyncSendOrderly();通过指定haskey有序异步发送
sendOneWay();单向发送
sendOneWayOrderly();通过指定haskey有序单向发送
doConvertMessage();发送字符串消息
- 1
- 2
- 3
- 4
- 5
- 6
- 7
代码示例(Consumer)
@Component
@RocketMQMessageListener(
topic = "myTopic1", //主题
consumerGroup = "cg12", //消费者组名
selectorExpression = "*", //选择器
messageModel = MessageModel.CLUSTERING //集群模式
)
public class Consumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("接受到了消息:" + message);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13