RocketMQ的思想来自于Kafka,而Kafka是依赖了Zookeeper的。所以,在RocketMQ的早期版本,即在
MetaQ v1.0与v2.0版本中,也是依赖于Zookeeper的。从MetaQ v3.0,即RocketMQ开始去掉了
Zookeeper依赖,使用了自己的NameServer。
Broker管理:接受Broker集群的注册信息并且保存下来作为路由信息的基本数据;提供心跳检测
机制,检查Broker是否还存活。
路由信息管理:每个NameServer中都保存着Broker集群的整个路由信息和用于客户端查询的队列
信息。Producer和Conumser通过NameServer可以获取整个Broker集群的路由信息,从而进行消
息的投递和消费。
个节点间是无差异的,各节点间相互不进行信息通讯。那各节点中的数据是如何进行数据同步的呢?在
Broker节点启动时,轮询NameServer列表,与每个NameServer节点建立长连接,发起注册请求。在
NameServer内部维护着⼀个Broker列表,用来动态存储Broker的信息。如图:
这与其他的注册中心集群很不同,大多数注册中心的集群都是内部通讯的,当Broker只需要连接一个注册中心就可以了.
方式上报给NameServer,每30秒发送一次心跳。心跳包中包含 BrokerId、Broker地址(IP+Port)、 Broker名称、Broker所属集群名称等等。NameServer在接收到心跳包后,会更新心跳时间戳,记录这 个Broker的最新存活时间。
戳距离当前时间是否超过120秒,如果超过,则会判定Broker失效,然后将其从Broker列表中剔除。
扩展:对于_RocketMQ_日常运维工作,例如_Broker_升级,需要停掉_Broker_的工作。_OP_需要怎么
做?
_OP_需要将_Broker_的读写权限禁掉。一旦_client(Consumer_或_Producer)_向_broker_发送请求,都会收
到_broker_的_NO_PERMISSION(没有权限)_响应,然后_client_会进行对其它_Broker_的重试。
当_OP_观察到这个_Broker_没有流量后,再关闭它,实现_Broker_从_NameServer_的移除。
OP:运维工程师
SRE:Site Reliability Engineer,现场可靠性工程师
客户端,而是客户端定时拉取主题最新的路由。默认客户端每30秒会拉取一次最新的路由。
扩展:
1)Push_模型:推送模型。其实时性较好,是一个“发布-订阅”_模型,需要维护一个长连接。而
长连接的维护是需要资源成本的。该模型适合于的场景:
实时性要求较高
_Client_数量不多(数量多,长链接就多),_Server_数据变化较频繁
2)_Pull_模型:拉取模型。存在的问题是,实时性较差,定时拉取,有可能多个时间段(这几个时间段内路由信息不发生变化)都拉取不到新数据。
3)_Long Polling_模型:长轮询模型。其是对_Push_与_Pull_模型的整合,充分利用了这两种模型的优
势,屏蔽了它们的劣势。
呢?客户端首先会生产一个随机数,然后再与NameServer节点数量取模,此时得到的就是所要连接的
节点索引,然后就会进行连接。如果连接失败,则会采用round-robin策略,逐个尝试着去连接其它节
点。 首先采用的是随机策略进行的选择,失败后采用的是轮询策略。
扩展:Zookeeper Client_是如何选择_Zookeeper Server_的?
简单来说就是,经过两次_Shufæ e,然后选择第一台_Zookeeper Server_。
详细说就是,将配置文件中的_zk server_地址进行第一次_shufæ e_,然后随机选择一个。这个选择出
的一般都是一个_hostname_。然后获取到该_hostname_对应的所有_ip_,再对这些_ip_进行第二次
shufæ e,从_shufæ e_过的结果中取第一个_server_地址进行连接。
生产者发送来的消息,同时为消费者的拉取请求作准备。Broker同时也存储着消息相关的元数据,包括
消费者组消费进度偏移offset、主题、队列等。 _Kafka 0.8_版本之后,_offset_是存放在_Broker_中的,之前版本是存放在_Zookeeper_中的。
建Topic时也会将Topic与Broker的关系写入到NameServer中。不过,这步是可选的,也可以在发送消
息时自动创建Topic。
取路由信息,即当前发送的Topic消息的Queue与Broker的地址(IP+Port)的映射关系。然后根据算 法策略从队选择一个Queue,与队列所在的Broker建立长连接从而向Broker发消息。当然,在获取 到路由 信息后,Producer会首先将路由信息缓存到本地,再每30秒从NameServer更新一次路由信 息。
然后根据算法策略从路由信息中获取到其所要消费的Queue,然后直接跟Broker建立长连接,开始 消费其中的消息。Consumer在获取到路由信息后,同样也会每30秒从NameServer更新一次路由信 息不过不同于Producer的是,Consumer还会向Broker发送心跳,以确保Broker的存活状态。
启动并查看NameServer
nohup sh bin/mqnamesrv &
tail -f ~/logs/rocketmqlogs/namesrv.log
The Name Server boot success…
启动Broker并查看
nohup sh bin/mqbroker -n localhost:9876 &
tail -f ~/logs/rocketmqlogs/broker.log
The broker[%s, 172.30.30.233:10911] boot success…
生产者
export NAMESRV_ADDR=localhost:9876
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
SendResult [sendStatus=SEND_OK, msgId= …
消费者
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
ConsumeMessageThread_%d Receive New Messages: [MessageExt…
sh bin/mqshutdown broker
The mqbroker(36695) is running…
Send shutdown request to mqbroker(36695) OK
sh bin/mqshutdown namesrv
The mqnamesrv(36664) is running…
Send shutdown request to mqnamesrv(36664) OK
在解压目录rocketmq-console的pom.xml中添加如下JAXB依赖。
JAXB,Java Architechture for Xml Binding,用于_XML_绑定的_Java_技术,是一个业界标准,是一
项可以根据_XML Schema_生成_Java_类的技术。
<dependency>
<groupId>javax.xml.bindgroupId>
<artifactId>jaxb-apiartifactId>
<version>2.3.0version>
dependency>
<dependency>
<groupId>com.sun.xml.bindgroupId>
<artifactId>jaxb-implartifactId>
<version>2.3.0version>
dependency>
<dependency>
<groupId>com.sun.xml.bindgroupId>
<artifactId>jaxb-coreartifactId>
<version>2.3.0version>
dependency>
<dependency>
<groupId>javax.activationgroupId>
<artifactId>activationartifactId>
<version>1.1.1version>
dependency>
个master一般配置一个slave即可)。master与slave的关系是主备关系,即master负责处理消息的读写
请求,而slave仅负责消息的备份与master宕机后的角色切换
producer返回成功ACK,无需等待slave同步数据成功。
步具有短暂的延迟(毫秒级),所以当master宕机后,这种异步复制方式可能会存在少量消息的丢失问
题。
master会等待slave同步数据成功后才向producer返回成功ACK,即master与slave都要写入成功后才会
返回成功ACK,也即双写。
高,从而导致性能要略低(大约低10%)。
效、安全性,又解决了可能会影响订阅的问题。
所以_RAID_阵列的搭建成本较高。
多_Master+RAID_阵列,与多_Master_多_Slave_集群的区别是什么?
多_Master+RAID_阵列,其仅仅可以保证数据不丢失,即不影响消息写入,但其可能会影响到
消息的订阅。但其执行效率要远高于多Master多Slave集群
多_Master_多_Slave_集群,其不仅可以保证数据不丢失,也不会影响消息写入。其运行效率要低
于多Master+RAID阵列
JBOD ,Just a Bunch of Disks,磁盘簇。表示一个没有控制软件提供协调控制的磁盘集合,这是 RAID
区别与 JBOD 的主要因素。 JBOD 将多个物理磁盘串联起来,提供一个巨大的逻辑磁盘。
JBOD 的数据存放机制是由第一块磁盘开始按顺序往后存储,当前磁盘存储空间用完后,再依次往后面
的磁盘存储数据。 JBOD 存储性能完全等同于单块磁盘,而且也不提供数据安全保护。
其只是简单提供一种扩展存储空间的机制,_JBOD_可用存储容量等于所有成员磁盘的存储空间之
和
JBOD 常指磁盘柜,而不论其是否提供 RAID 功能。不过,JBOD并非官方术语,官方称为Spanning。
RAID0 是一种简单的、无数据校验的数据条带化技术。实际上不是一种真正的 RAID ,因为它并不提
供任何形式的冗余策略。 RAID0 将所在磁盘条带化后组成大容量的存储空间,将数据分散存储在所有
磁盘中,以独立访问方式实现多块磁盘的并读访问。
理论上讲,一个由 n 块磁盘组成的 RAID0 ,它的读写性能是单个磁盘性能的 n 倍,但由于总线带宽等
多种因素的限制,实际的性能提升低于理论值。由于可以并发执行 I/O 操作,总线带宽得到充分利用。
再加上不需要进行数据校验,RAID0 的性能在所有 RAID 等级中是最高的。
RAID0 具有低成本、高读写性能、 100% 的高存储空间利用率等优点,但是它不提供数据冗余保护,一
旦数据损坏,将无法恢复。
应用场景:对数据的顺序读写要求不高,对数据的安全性和可靠性要求不高,但对系统性能要求很高的
场景。
RAID0_与_JBOD_相同点:
1)存储容量:都是成员磁盘容量总和
2)磁盘利用率,都是_100%,即都没有做任何的数据冗余备份
_RAID0_与_JBOD_不同点:
JBOD:数据是顺序存放的,一个磁盘存满后才会开始存放到下一个磁盘
RAID:各个磁盘中的数据写入是并行的,是通过数据条带技术写入的。其读写性能是_JBOD_的_n _
倍
RAID1 就是一种镜像技术,它将数据完全一致地分别写到工作磁盘和镜像磁盘,它的磁盘空间利用率
为 50% 。 RAID1 在数据写入时,响应时间会有所影响,但是读数据的时候没有影响。 RAID1 提供了
最佳的数据保护,一旦工作磁盘发生故障,系统将自动切换到镜像磁盘,不会影响使用。
RAID1是为了增强数据安全性使两块磁盘数据呈现完全镜像,从而达到安全性好、技术简单、管理方
便。 RAID1 拥有完全容错的能力,但实现成本高。
应用场景:对顺序读写性能要求较高,或对数据安全性要求较高的场景。
RAID10是一个RAID1与RAID0的组合体,所以它继承了RAID0的快速和RAID1的安全。
简单来说就是,先做条带,再做镜像。发即将进来的数据先分散到不同的磁盘,再将磁盘中的数据做
镜像。
RAID01是一个RAID0与RAID1的组合体,所以它继承了RAID0的快速和RAID1的安全。
简单来说就是,先做镜像再做条带。即将进来的数据先做镜像,再将镜像数据写入到与之前数据不同
的磁盘,即再做条带。
_因为RAID10是先进行条带,已经将数据写进去,后进行镜像,等于将数据备份了一个磁盘,所以当其中一个磁盘挂了,但这一条数据也不算丢失,所以RAID10_要比_RAID01_的容错率再高,所以生产环境下一般是不使用_RAID01_的。
![image.png](https://img-blog.csdnimg.cn/img_convert/316e7d36502a2e01c6d9943342fa377f.png#clientId=ue5eeb3f5-f8bd-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=317&id=u8b38be3b&margin=[object Object]&name=image.png&originHeight=633&originWidth=591&originalType=binary&ratio=1&rotation=0&showTitle=false&size=264736&status=done&style=none&taskId=u0847ae40-0bbe-4bbe-b505-5d1d5ebf7fa&title=&width=295.5)
# 指定整个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.188.100:9876;192.168.188.130:9876
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.59.164:9876;192.168.59.165: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
brokerClusterName=DefaultCluster
brokerName=broker-b
brokerId=0
deleteWhen=04
fileReservedTime=48
brokerRole=ASYNC_MASTER
flushDiskType=ASYNC_FLUSH
namesrvAddr=192.168.59.164:9876;192.168.59.165:9876
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=1
deleteWhen=04
fileReservedTime=48
brokerRole=SLAVE
flushDiskType=ASYNC_FLUSH
namesrvAddr=192.168.59.164:9876;192.168.59.165:9876
listenPort=11911
storePathRootDir=~/store-s
storePathCommitLog=~/store-s/commitlog
storePathConsumeQueue=~/store-s/consumequeue
storePathIndex=~/store-s/index
storeCheckpoint=~/store-s/checkpoint
abortFile=~/store-s/abort
#默认为新建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
nohup sh bin/mqnamesrv &
tail -f ~/logs/rocketmqlogs/namesrv.log
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a.properties &
tail -f ~/logs/rocketmqlogs/broker.log
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b.properties &
tail -f ~/logs/rocketmqlogs/broker.log
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b-s.properties &
tail -f ~/logs/rocketmqlogs/broker.log
nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a-s.properties &
tail -f ~/logs/rocketmqlogs/broker.log
该命令在官网中有详细的用法解释。
https://github.com/apache/rocketmq/blob/master/docs/cn/operation.md
路由表:实际是一个_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。
默认选择算法。该算法保证了每个Queue中可以均匀的获取到消息。
该算法存在一个问题:由于某些原因,在某些_Broker_上的_Queue_可能投递延迟较严重。从而导致
_Producer_的缓存队列中出现较大的消息积压,影响消息的投递性能。
该算法会统计每次消息投递的时间延迟,然后根据统计出的结果将消息投递到时间延迟最小的Queue。
如果延迟相同,则采用轮询算法投递。该算法可以有效提升消息的投递性能。
该算法也存在一个问题:消息在_Queue_上的分配不均匀。投递延迟小的_Queue_其可能会存在大量
的消息。而对该_Queue_的消费者压力会增大,降低消息的消费能力,可能会导致_MQ_中消息的堆
积。
被命名为_mappedFile_。
mappedFile文件中的。mappedFile文件大小为1G(小于等于1G),文件名由20位十进制数构成,表示
当前文件的第一条消息的起始位移偏移量。
第一个文件名一定是_20_位_0_构成的。因为第一个文件的第一条消息的偏移量_commitlog offset_为_0 _
当第一个文件放满时,则会自动生成第二个文件继续存放消息。假设第一个文件大小(总大小为1G)是
_1073741820_字节(1G = 1073741824_字节),则第二个文件名就是_00000000001073741820。
以此类推,第_n_个文件名应该是前_n-1_个文件大小之和。
一个_Broker_中所有_mappedFile_文件的_commitlog offset_是连续的
_mappedFile_文件是顺序读写的文件,所有其访问效率很高
无论是_SSD_磁盘还是_SATA_磁盘,通常情况下,顺序存取效率都会高于随机存取。
mappedFile文件内容由一个个的消息单元构成。每个消息单元中包含消息总长度MsgLen、消息的物理
位置physicalOffset、消息体内容Body、消息体长度BodyLength、消息主题Topic、Topic长度
TopicLength、消息生产者BornHost、消息发送时间戳BornTimestamp、消息所在的队列QueueId、消
息在Queue中存储的偏移量QueueOffset等近20余项消息相关属性。
需要注意到,消息单元中是包含_Queue_相关属性的。所以,我们在后续的学习中,就需要十分
留意_commitlog_与_queue_间的关系是什么?一个_mappedFile_文件中第_m+1_个消息单元的_commitlog offset_偏移量 为: L(m+1) = L(m) + MsgLen(m) (m >= 0)
为了提高效率,会为每个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后经历了以下几个过程才最终被持久化。
Broker根据queueId,获取到该消息对应索引条目要在consumequeue目录中的写入偏移量,即
QueueOffset
将queueId、queueOffset等数据,与消息一起封装为消息单元
将消息单元写入到commitlog
同时,形成消息索引条目
将消息索引条目分发到相应的consumequeue
当Consumer来拉取消息时会经历以下几个步骤:
Consumer获取到其要消费消息所在Queue的消费偏移量offset,计算出其要消费消息的
消息offset
消费_offset_即消费进度,consumer_对某个_Queue_的消费_offset,即消费到了该_Queue_的第几
条消息
消息_offset = _消费_offset + 1_Consumer向Broker发送拉取请求,其中会包含其要拉取消息的Queue、消息offset及消息
Tag。
Broker计算在该consumequeue中的queueOffset。
_queueOffset = _消息_offset * 20_字节
从该queueOffset处开始向后查找第一个指定Tag的索引条目。
解析该索引条目的前8个字节,即可定位到该消息在commitlog中的commitlog offset
从对应commitlog offset中读取消息单元,并发送给Consumer
_PageCache_机制,页缓存机制,是_OS_对文件的缓存机制,用于加速对文件的读写操作。一般来
说,程序对文件进行顺序读写的速度几乎接近于内存读写速度,主要原因是由于_OS_使用
PageCache_机制对读写访问操作进行性能优化,将一部分的内存用作_PageCache。
写操作:OS_会先将数据写入到_PageCache_中,随后会以异步方式由_pdæ ush(
_page dirty æ ush) _
内核线程将_Cache_中的数据刷盘到物理磁盘
读操作:若用户要读取数据,其首先会从_PageCache_中读取,若没有命中,则_OS_在从物理磁
盘上加载该数据到_PageCache_的同时,也会顺序对其相邻数据块中的数据进行预读取。
Kafka_中的_Topic_的消息被分割为一个或多个_partition。partition_是一个物理概念,对应到系统上
就是_topic_目录下的一个或多个目录。每个_partition_中包含的文件称为_segment,是具体存放消息
的文件。
_Kafka_中消息存放的目录结构是:_topic_目录下有_partition_目录,_partition_目录下有_segment_文件
_Kafka_中没有二级分类标签_Tag_这个概念
_Kafka_中无需索引文件。因为生产者是将消息直接写在了_partition_中的,消费者也是直接从
_partition_中读取数据的
beginTimestamp:该indexFile中第一条消息的存储时间
endTimestamp:该indexFile中最后一条消息存储时间
beginPhyoffset:该indexFile中第一条消息在commitlog中的偏移量commitlog offset
endPhyoffset:该indexFile中最后一条消息在commitlog中的偏移量commitlog offset
hashSlotCount:已经填充有index的slot数量(并不是每个slot槽下都挂载有index索引单元,这
里统计的是所有挂载了index索引单元的slot槽的数量)
indexCount:该indexFile中包含的索引单元个数(统计出当前indexFile中所有slot槽下挂载的所
有index索引单元的数量之和)
据这个indexNo可以计算出该index单元在indexFile中的位置。不过,该取模结果的重复率是很高的,
为了解决该问题,在每个index索引单元中增加了preIndexNo,用于指定该slot中当前index索引单元的
前一个index索引单元。而slot中始终存放的是其下最新的index索引单元的indexNo,这样的话,只要
找到了slot就可以找到其最新的index索引单元,而通过这个index索引单元就可以找到其之前的所有
index索引单元。
_indexNo_是一个在_indexFile_中的流水号,从_0_开始依次递增。即在一个_indexFile_中所有_indexNo_是
以此递增的。_indexNo_在_index_索引单元中是没有体现的,其是通过_indexes_中依次数出来的。
keyHash:消息中指定的业务key的hash值
phyOffset:当前key对应的消息在commitlog中的偏移量commitlog offset
timeDiff:当前key对应消息的存储时间与当前indexFile创建时间的时间差
preIndexNo:当前slot下当前index索引单元的前一个index索引单元的indexNo
indexFile的文件名为当前文件被创建时的时间戳。这个时间戳有什么用处呢?
根据业务key进行查询时,查询条件除了key之外,还需要指定一个要查询的时间戳,表示要查询不大于
该时间戳的最新的消息,即查询指定时间戳之前存储的最新消息。这个时间戳文件名可以简化查询,提
高查询效率。具体后面会详细讲解。
indexFile文件是何时创建的?其创建的条件(时机)有两个:
当第一条带key的消息发送来后,系统发现没有indexFile,此时会创建第一个indexFile文件
当一个indexFile中挂载的index索引单元数量超出2000w个时,会创建新的indexFile。当带key的
消息发送到来后,系统会找到最新的indexFile,并从其indexHeader的最后4字节中读取到
indexCount。若indexCount >= 2000w时,会创建新的indexFile。
由于可以推算出,一个_indexFile_的最大大小是:_(40[indexHeader] + 500w * 4 [slots]+ 2000w * 20[index])_字节
最多有五百万个slots,每个四个字节;一个slost最多可以挂在四个index,每个index最多占20个字节.
计算指定消息key的slot槽位序号:
slot槽位序号 = key的hash % 500w
计算槽位序号为n的slot在indexFile中的起始位置:(这是起始位置,所以是计算上一个槽位)
slot(n)位置 = 40 + (n - 1) * 4
计算indexNo为m的index在indexFile中的起始位置:
index(m)位置 = 40 + 500w * 4 + (m - 1) * 20
_40_为_indexFile_中_indexHeader_的字节数
_500w * 4 _是所有_slots_所占的字节数
消费所有消息,但它们的消费进度是不同。所以consumer各自保存各自的消费进度。
中的消息,同一条消息只会被消费一次。消费进度会参与到了消费的负载均衡中,故消费进度是
需要共享的。下图是broker中存放的各个Topic的各个Queue的消费进度。
Consumer间进行重新分配的过程。
者的情况下,这个消费者将负责消费这5个队列的消息。如果此时我们增加⼀个消费者,那么就可以给
其中⼀个消费者分配2个队列,给另⼀个分配3个队列,从而提升消息的并行消费能力。
多余的消费者实例将分配不到任何队列。
Rebalance的发生。此时原Consumer就需要暂停部分队列的消费,等到这些队列分配给新的Consumer
后,这些暂停消费的队列才能继续被消费。
同步提交:_consumer_提交了其消费完毕的一批消息的_offset_给_broker_后,需要等待_broker_的成功
ACK。当收到_ACK_后,_consumer_才会继续获取并消费下一批消息。在等待_ACK_期间,_consumer _
是阻塞的。
异步提交:consumer_提交了其消费完毕的一批消息的_offset_给_broker_后,不需要等待_broker_的成
功_ACK。_consumer_可以直接获取并消费下一批消息。
对于一次性读取消息的数量,需要根据具体业务场景选择一个相对均衡的是很有必要的。因为
数量过大,系统性能提升了,但产生重复消费的消息数量可能会增加;数量过小,系统性能会
下降,但被重复消费的消息数量可能会减少。
1)_Queue_数量发生变化的场景:
_Broker_扩容或缩容
_Broker_升级运维
_Broker_与_NameServer_间的网络异常
_Queue_扩容或缩容
2)消费者数量发生变化的场景:
_Consumer Group_扩容或缩容
_Consumer_升级运维
_Consumer_与_NameServer_间网络异常
TopicConå gManager:_key_是_topic_名称,value_是_TopicConå g。_TopicConå g_中维护着该_Topic_中所
有_Queue_的数据。
ConsumerManager:key_是_Consumser Group Id,value_是_ConsumerGroupInfo。
_ConsumerGroupInfo_中维护着该_Group_中所有_Consumer_实例数据。
ConsumerOffsetManager:key_为Topic与订阅该Topic的Group的组合,即topic@group,
value_是一个内层_Map。内层_Map_的_key_为_QueueId,内层_Map_的_value_为该_Queue_的消费进度
offset。
自主进行Rebalance。
Coordinator是Broker中的一个进程。Coordinator会在Consumer Group中选出一个Group Leader。由
这个Leader根据自己本身组情况完成Partition分区的再分配。这个再分配结果会上报给Coordinator,
并由Coordinator同步给Group中的所有Consumer实例。
Kafka中的Rebalance是由Consumer Leader完成的。而RocketMQ中的Rebalance是由每个Consumer自
身完成的,Group中不存在Leader。
则按顺序将avg个Queue逐个分配Consumer;如果不能整除,则将多余出的Queue按照Consumer顺序
逐个分配。
该算法即,先计算好每个_Consumer_应该分得几个_Queue_,然后再依次将这些数量的_Queue_逐个
分配个_Consumer_。
环形平均算法是指,根据消费者的顺序,依次在由queue队列组成的环形图中逐个分配。
该算法不用事先计算每个_Consumer_需要分配几个_Queue_,直接一个一个分即可。
该算法会将consumer的hash值作为Node节点存放到hash环上,然后将queue的hash值也放到hash环
上,通过顺时针方向,距离queue最近的那个consumer就是该queue要分配的consumer。
该算法存在的问题:分配不均。
该算法会根据queue的部署机房位置和consumer的位置,过滤出当前consumer相同机房的queue。然
后按照平均分配策略或环形平均策略对同机房queue进行分配。如果没有同机房queue,则按照平均分
配策略或环形平均策略对所有queue进行分配。
致性hash策略分配的结果也很大可能上存在不平均的情况。但平均分配后的reblance可能会很大.请看下图
扩容后,我们的队列要重新分配一次,之前是每个队列都分配了三个队列,而增加了一个消费者后,2,4,5,6,7,8,9队列都改变了原来消费者的位置,rebalance占比高达70%.
可以看到,如果是缩容,环路上只是位置减少了一个消费者,要重新分配的队列只是该减少的消费者原来分配的队列,rebalance只有20%.
那么什么情况下,消息被消费了呢?
Consumer在消费完消息后会向其消费进度记录器提交其消费消息的offset,
offset被成功记录到记录器中,那么这条消费就被成功消费了。
那什么又是消费进度记录器呢?对于广播模式来说,消费者本身就是消费记录器,而对于集群模式来说,Broker就是消费记录器.
Tag及对消息的处理逻辑必须完全一致。否则,消息消费的逻辑就会混乱,甚至导致消息丢失。
分为两种模式:本地模式和远程模式。
当消费模式为广播消费时,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}为消费者组名称。
当消费模式为集群消费时,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机制。
consumer.setConsumeFromWhere()方法指定的。
CONSUME_FROM_LAST_OFFSET:从_queue_的当前最后一条消息开始消费
CONSUME_FROM_FIRST_OFFSET:从_queue_的第一条消息开始消费
CONSUME_FROM_TIMESTAMP:从指定的具体时间戳位置的消息开始消费。这个具体时间戳
是通过另外一个语句指定的 。
consumer.setConsumeTimestamp(“20210701080000”) yyyyMMddHHmmss
集群消费模式下,Consumer消费完消息后会向Broker提交消费进度offset,其提交方式分为两种:
同步提交:消费者在消费完一批消息后会向broker提交这些消息的offset,然后等待broker的成功响
应。若在等待超时之前收到了成功响应,则继续读取下一批消息进行消费(从ACK中获取
nextBeginOffset)。若没有收到响应,则会重新提交,直到获取到响应。而在这个等待过程中,消费
者是阻塞的。其严重影响了消费者的吞吐量。
异步提交:消费者在消费完一批消息后向broker提交offset,但无需等待Broker的成功响应,可以继续
读取并消费下一批消息。这种方式增加了消费者的吞吐量。但需要注意,broker在收到提交的offset
后,还是会向消费者进行响应的。可能还没有收到ACK,此时Consumer会从Broker中直接获取
nextBeginOffset。
幂等:若某操作执行多次与执行一次对系统产生的影响是相同的,则称该操作是幂等的。
发Rebalance(过程可能有延迟),此时Consumer可能会收到曾经被消费过的消息。
例如,对同一笔订单的多次支付操作,只会成功一次。
操作为重复性操作;若不存在,则进入下一步。
第_1_步已经判断过是否是重复性操作了,为什么第_2_步还要再次判断?能够进入第_2_步,说明已经
不是重复操作了,第_2_次判断是否重复?
当然不重复。一般缓存中的数据是具有有效期的。缓存中数据的有效期一旦过期,就是发生缓
存穿透,使请求直接就到达了_数据库_。
- 当支付请求到达后,首先在Redis缓存中却获取key为支付流水号的缓存value。若value不空,则
说明本次支付是重复操作,业务系统直接返回调用侧重复支付标识;若value为空,则进入下一步
操作- 到DBMS中根据支付流水号查询是否存在相应实例。若存在,则说明本次支付是重复操作,业务
系统直接返回调用侧重复支付标识;若不存在,则说明本次操作是首次操作,进入下一步完成唯
一性处理- 在分布式事务中完成三项操作:
完成支付任务
将当前支付流水号作为key,任意字符串作为value,通过set(key, value, expireTime)将数
据写入到Redis缓存
将当前支付流水号作为主键,与其它相关数据共同写入到DBMS
_RocketMQ_能够保证消息不丢失,但不能保证消息不重复。
消息处理流程中,如果Consumer的消费速度跟不上Producer的发送速度,MQ中未处理的消息会越来
越多(进的多出的少),这部分消息就被称为堆积消息。消息出现堆积进而会造成消息的消费延迟。
以下场景需要重点关注消息堆积和消费延迟问题:
Consumer通过长轮询Pull模式批量拉取的方式从服务端获取消息,将拉取到的消息缓存到本地缓冲队
列中。对于拉取式消费,在内网环境下会有很高的吞吐量,所以这一阶段一般不会成为消息堆积的瓶
颈。
一个单线程单分区的低规格主机_(Consumer_,4C8G),其可达到几万的_TPS_。如果是多个分区多
个线程,则可以轻松达到几十万的_TPS_。
Consumer将本地缓存的消息提交到消费线程中,使用业务消费逻辑对消息进行处理,处理完毕后获取
到一个结果。这是真正的消息消费过程。此时Consumer的消费能力就完全依赖于消息的消费耗时和消
费并发度了。如果由于业务处理逻辑复杂等原因,导致处理单条消息的耗时较长,则整体的消息吞吐
量肯定不会高,此时就会导致Consumer本地缓冲队列达到上限,停止从服务端拉取消息。
消息堆积的主要瓶颈在于客户端的消费能力,而消费能力由消费耗时和消费并发度决定。注意,消费
耗时的优先级要高于消费并发度。即在保证了消费耗时的合理性前提下,再考虑消费并发度问题。
影响消息处理时长的主要因素是代码逻辑。而代码逻辑中可能会影响处理时长代码主要有两种类型:
CPU内部计算型代码和外部I/O操作型代码。
通常情况下代码中如果没有复杂的递归和循环的话,内部计算耗时相对外部I/O操作来说几乎可以忽
略。所以外部IO型代码是影响消息处理时长的主要症结所在。
外部_IO_操作型代码举例:
1.读写外部数据库,例如对远程_MySQL_的访问
2.读写外部缓存系统,例如对远程_Redis_的访问
3.下游系统调用,例如_Dubbo_的_RPC_远程调用,_Spring Cloud_的对下游系统的_Http_接口调用
关于下游系统调用逻辑需要进行提前梳理,掌握每个调用操作预期的耗时,这样做是为了能够
判断消费逻辑中_IO_操作的耗时是否合理。通常消息堆积是由于下游系统出现了服务异常或达到
了DBMS容量限制,导致消费耗时增加。
服务异常,并不仅仅是系统中出现的类似_500_这样的代码错误,而可能是更加隐蔽的问题。例
如,网络带宽问题。
达到了_DBMS_容量限制,其也会引发消息的消费耗时增加。
一般情况下,消费者端的消费并发度由单节点线程数和节点数量共同决定,其值为单节点线程数*节点
数量。不过,通常需要优先调整单节点的线程数,若单机硬件资源达到了上限,则需要通过横向扩展
来提高消费并发度。
单节点线程数,即单个_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_的分区数量。
对于一台主机中线程池中线程数的设置需要谨慎,不能盲目直接调大线程数,设置过大的线程数反而会
带来大量的线程切换的开销。理想环境下单节点的最优线程数计算模型为:C *(
T1 + T2)/ T1。
最优线程数 = C *(
T1 + T2)_/ T1 = C * T1/T1 + C * T2/T1 = C + C * T2/T1 _
注意,该计算出的数值是理想状态下的理论数据,在生产环境中,不建议直接使用。而是根据
当前环境,先设置一个比该值小的数值然后观察其压测效果,然后再根据效果逐步调大线程
数,直至找到在该环境中性能最佳时的值。
为了避免在业务使用时出现非预期的消息堆积和消费延迟问题,需要在前期设计阶段对整个业务逻辑进
行完善的排查和梳理。其中最重要的就是梳理消息的消费耗时和设置消息消费的并发度。
通过压测获取消息的消费耗时,并对耗时较高的操作的代码逻辑进行分析。梳理消息的消费耗时需要关
注以下信息:
逐步调大单个Consumer节点的线程数,并观测节点的系统指标,得到单个节点最优的消费线程数
和消息吞吐量。
根据上下游链路的流量峰值计算出需要设置的节点数
节点数 _= _流量峰值 _/ _单个节点消息吞吐量
消息被消费过后会被清理掉吗?不会的。
消息是被顺序存储在commitlog文件的,且消息大小不定长,所以消息的清理是不可能以消息为单位进
行清理的,而是以commitlog文件为单位进行清理的。否则会急剧下降清理效率,并实现逻辑复杂。
commitlog文件存在一个过期时间,默认为72小时,即三天。除了用户手动清理外,在以下情况下也
会被自动清理,无论文件中的消息是否被消费过:
文件过期,且到达清理时间点(默认为凌晨4点)后,自动清理过期文件
文件过期,且磁盘空间占用率已达过期清理警戒线(默认75%)后,无论是否达到清理时间点,
都会自动清理过期文件
磁盘占用率达到清理警戒线(默认85%)后,开始按照设定好的规则清理文件,无论是否过期。
默认会从最老的文件开始清理
磁盘占用率达到系统危险警戒线(默认90%)后,Broker将拒绝消息写入
需要注意以下几点:
1)对于_RocketMQ_系统来说,删除一个_1G_大小的文件,是一个压力巨大的_IO_操作。在删除过程
中,系统性能会骤然下降。所以,其默认清理时间点为凌晨_4_点,访问量最小的时间。也正因如
果,我们要保障磁盘空间的空闲率,不要使系统出现在其它时间点删除_commitlog_文件的情况。
2)官方建议_RocketMQ_服务的_Linux_文件系统采用_ext4_。因为对于文件删除操作,_ext4_要比_ext3_性
能更好
单向发送消息是指,Producer仅负责发送消息,不等待、不处理MQ的ACK。该发送方式时MQ也不返
回ACK。该方式的消息发送效率最高,但消息可靠性较差。
同步发送消息是指,Producer发出⼀条消息后,会在收到MQ返回的ACK之后才发下⼀条消息。该方式
的消息可靠性最高,但消息发送效率太低。
异步发送消息是指,Producer发出消息后无需等待MQ返回ACK,直接发送下⼀条消息。该方式的消息
可靠性可以得到保障,消息发送效率也可以。
public static void main(String[] args) throws Exception{
DefaultMQProducer singleSend = new DefaultMQProducer("singleSend");
singleSend.setNamesrvAddr("hadoop4:9876");
singleSend.start();
for (int i = 0; i < 10; i++) {
String body=i+"";
Message message=new Message("SingleSendTopic","s1",body.getBytes(StandardCharsets.UTF_8));
singleSend.sendOneway(message);
}
singleSend.shutdown();
}
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-clientartifactId>
<version>4.9.2version>
dependency>
public static void main(String[] args) throws Exception{
// 创建一个生产者组
DefaultMQProducer p1 = new DefaultMQProducer("p1");
// 指定NameServer地址
p1.setNamesrvAddr("hadoop4:9876");
// 设置发送失败时重置次数,默认两次
p1.setRetryTimesWhenSendFailed(3);
// 设置发送超时时间为,默认为三千毫秒
p1.setSendMsgTimeout(5000);
// 开启生产者
p1.start();
// 发送消息
for (int i = 0; i < 10; i++) {
String body=i+"";
// 消息类,1.topic 2.tag 3.消息体
Message message=new Message("topic","tag",body.getBytes(StandardCharsets.UTF_8));
SendResult send = p1.send(message);
System.out.println(send);
}
p1.shutdown();
}
点击 SendResult 类可看见,该类里有一个重要的枚举类属性 SendStatus
// 消息发送的状态
public enum SendStatus {
SEND_OK, // 发送成功
FLUSH_DISK_TIMEOUT, // 刷盘超时。当Broker设置的刷盘策略为同步刷盘时才可能出 现这种异常状态。异步刷盘不会出现
FLUSH_SLAVE_TIMEOUT, // Slave同步超时。当Broker集群设置的Master-Slave的复 制方式为同步复制时才可能出现这种异常状态。异步复制不会出现
SLAVE_NOT_AVAILABLE, // 没有可用的Slave。当Broker集群设置为Master-Slave的 复制方式为同步复制时才可能出现这种异常状态。异步复制不会出现
}
成功结果截图
在/root/store/consumequeue 路径下会发现有该名字为topic的目录
在前段控制台页面的主题导航栏中,新增了一个名为topic的主题
在前端控制台页面的消息导航栏,可搜索该主题的消息
public static void main(String[] args) throws RemotingException, InterruptedException, MQClientException {
DefaultMQProducer p2 = new DefaultMQProducer("p2");
p2.setNamesrvAddr("hadoop4:9876");
// 指定异步失败后不进行重试发送
p2.setRetryTimesWhenSendAsyncFailed(0);
// 指定默认的队列个数
p2.setDefaultTopicQueueNums(2);
p2.start();
for (int i = 0; i < 10; i++) {
String body=i+"";
Message message=new Message("topic1","tag1",body.getBytes(StandardCharsets.UTF_8));
// 异步发送,有成功回调和异常回调内部类
p2.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
@Override
public void onException(Throwable throwable) {
}
});
}
// 因为是异步发送,所以一定要等待几秒,不然还没发送,p2.shutdown()已经执行将生产者关闭
Thread.sleep(50000);
p2.shutdown();
}
下面位置可查看初始化读写队列数
public static void main(String[] args) throws Exception{
// 设置一个push策略得到消息的消费者
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("p1Consumer");
defaultMQPushConsumer.setNamesrvAddr("hadoop4:9876");
// 指定消费策略,从第一个消息开始消费
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 指定订阅主题和其tag
defaultMQPushConsumer.subscribe("topic","*");
// 指定消费模式为广播模式,默认为集群模式
defaultMQPushConsumer.setMessageModel(MessageModel.BROADCASTING);
// 创建消息监听器
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
// 遍历得到的消息
for (MessageExt m :
list) {
System.out.println(m);
}
// 返回消息状态为成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
defaultMQPushConsumer.start();
}
在这种情况下,我们希望消费者每次收到的消息都是正确的,可以采用容错机制,即是将所有的错误列举出来,但这样子消耗的时间太长,显然不是合适的.
当发送和消费参与的Queue只有一个时所保证的有序是整个Topic中消息的顺序, 称为全局有序。
在创建_Topic_时指定_Queue_的数量。有三种指定方式:
1)在代码中创建_Producer_时,可以指定其自动创建的_Topic_的_Queue_数量
2)在_RocketMQ_可视化控制台中手动创建_Topic_时指定_Queue_数量
3)使用_mqadmin_命令手动创建_Topic_时指定_Queue_数量
如果有多个Queue参与,其仅可保证在单独每个队列上的消息顺序,则称为分区有序。
但是这里会出现一个问题,不同的order如何进行队列的选择?
在定义_Producer_时我们可以指定消息队列选择器,而这个选择器是我们
自己实现了_MessageQueueSelector_接口定义的。
在实现选择算法时,一般要使用一个选择key,这个选择key是唯一的,一般是消息的key.然后用该选择key与该topic所包含的队列的数量取模,其结果即为选出的队列id.
但新的问题的是:即使这个选择key是唯一的,但是由于队列的数量是不定的,取模的结果是会相同的,也就是不同的选择key的消息可能会选择同一个队列.这个时候,消费该队列的消费者将面临两个不同选择key的消息,这个问题又该如何解决?
其实这个并不是问题,只要我们用其选择key进行一下判断,看这个选择key的消息是否是该消费者要消费的消息便可以了.
以上做法会不会出现如下新的问题呢?不属于那个_Consumer_的消息被拉取走了,那么应该消费
该消息的_Consumer_是否还能再消费该消息呢?答案肯定是可以的,同一个_Queue_中的消息不可能被同一个_Group_中的不同_Consumer_同时消费。所以,消费同一个_Queue_的不同选择_key_的消息的_Consumer_一定属于不同的_Group_。而不同的_Group_中的_Consumer_间的消费是相互隔离的,互不影响的。
public static void main(String[] args) throws Exception{
DefaultMQProducer ordered = new DefaultMQProducer("Ordered");
ordered.setNamesrvAddr("hadoop4:9876");
//注意,这里我们演示的是分区有序,所以队列不止一个,设置成一个就成全局有序了
//这里默认的队列个数是四个的
ordered.start();
for (int i = 0; i < 10; i++) {
String body=i+"";
Message message=new Message("orderedTopic","o1",body.getBytes(StandardCharsets.UTF_8));
// 指定消息key
message.setKeys(i+"");
// 发送同步消息,如果需要异步消息,直接在后面加上参数匿名内部类即可
SendResult send = ordered.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
// 使用消息key作为选择key,在指定消息的时候设置key,这里虽然key都相同,但类型是不同的
String keys = message.getKeys();
int i = Integer.parseInt(keys);
// 使用其他数据key作为选择key,这个数据从send函数的第三个参数传入,然后会被select函数的第三个参数Object o接收
// Integer i = (Integer) o;
int queueIndex = i % list.size();
return list.get(queueIndex);
}
}, i);
System.out.println(send);
}
ordered.shutdown();
}
当消息写入到Broker后,在指定的时长后才可被消费处理的消息,称为延时消息。
采用RocketMQ的延时消息可以实现定时任务的功能,而无需使用定时器。典型的应用场景是,电商交
易中超时未支付关闭订单的场景,12306平台订票超时未支付取消订票的场景。
延时消息的延迟时长不支持随意时长的延迟,是通过特定的延迟等级来指定的。延时等级定义在
RocketMQ服务端的MessageStoreConfig类中的如下变量中:
即,若指定的延时等级为3,则表示延迟时长为10s,即延迟等级是从1开始计数的。
当然,如果需要自定义的延时等级,可以通过在broker加载的配置中新增如下配置(例如下面增加了1
天这个等级1d)。配置文件在RocketMQ安装目录下的conf目录中。
messageDelayLevel = 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 1d
Producer将消息发送到Broker后,Broker会首先将消息写入到commitlog文件,然后需要将其分发到相
应的consumequeue。不过,在分发之前,系统会先判断消息中是否带有延时等级。若没有,则直接正
常分发;若有则需要经历一个复杂的过程:
目录与consumequeue文件(如果没有这些目录与文件的话)。
延迟等级_delayLevel_与_queueId_的对应关系为_queueId = delayLevel -1 _
需要注意,在创建_queueId_目录时,并不是一次性地将所有延迟等级对应的目录全部创建完毕,
而是用到哪个延迟等级创建哪个目录
Hash值。现修改为消息的投递时间。投递时间是指该消息被重新修改为原Topic后再次被写入到
commitlog中的时间。投递时间 = 消息存储时间 + 延时等级时间。消息存储时间指的是消息
被发送到Broker时的时间戳。
_SCHEDULE_TOPIC_XXXX_目录中各个延时等级_Queue_中的消息是如何排序的?
是按照消息投递时间排序的。一个_Broker_中同一等级的所有延时消息会被写入到_consumequeue _
目录中_SCHEDULE_TOPIC_XXXX_目录下相同_Queue_中。即一个_Queue_中消息投递时间的延迟等
级时间是相同的。那么投递时间就取决于于消息存储时间了。即按照消息被发送到_Broker_的时
间进行排序的。
Broker内部有⼀个延迟消息服务类ScheuleMessageService,其会消费SCHEDULE_TOPIC_XXXX中的消
息,即按照每条消息的投递时间,将延时消息投递到⽬标Topic中。不过,在投递之前会从commitlog
中将原来写入的消息再次读出,并将其原来的延时等级设置为0,即原消息变为了一条不延迟的普通消
息。然后再次将消息投递到目标Topic中。
ScheuleMessageService_在_Broker_启动时,会创建并启动一个定时器_TImer,用于执行相应的定时
任务。系统会根据延时等级的个数,定义相应数量的_TimerTask_,每个_TimerTask_负责一个延迟
等级消息的消费与投递。每个_TimerTask_都会检测相应_Queue_队列的第一条消息是否到期。若第
一条消息未到期,则后面的所有消息更不会到期(消息是按照投递时间排序的);若第一条消
息到期了,则将该消息投递到目标_Topic_,即消费该消息。
延迟消息服务类ScheuleMessageService将延迟消息再次发送给了commitlog,并再次形成新的消息索
引条目,分发到相应Queue。
这其实就是一次普通消息发送。只不过这次的消息_Producer_是延迟消息服务类
ScheuleMessageService。
只要在消息发送之前指定消息的延迟等级即可
// 设置消息水平延迟登记为3,即10s
message.setDelayTimeLevel(3);
消费者如往常一样接收,但写好消费者后,消费者要比生产者优先启动,然后会发现,消费者需要等待十秒才能接收到消息.
事务消息为了保证操作的原子性,最典型的问题是银行扣款的问题.
对于分布式事务,通俗地说就是,一次操作由若干分支操作组成,这些分支操作分属不同应用,分布在
不同服务器上。分布式事务需要保证这些分支操作要么全部成功,要么全部失败。分布式事务与普通事
务一样,就是为了保证操作结果的一致性。
RocketMQ提供了类似X/Open XA的分布式事务功能,通过事务消息能达到分布式事务的最终一致。XA
是一种分布式事务解决方案,一种分布式事务处理模式。
暂不能投递的消息,发送方已经成功地将消息发送到了Broker,但是Broker未收到最终确认指令,此时
该消息被标记成“暂不能投递”状态,即不能被消费者看到。处于该种状态下的消息即半事务消息。
Producer回调操作执行的结果为本地事务状态,其会发送给TC,而TC会再发送给TM。TM会根据TC发
送来的本地事务状态来决定全局事务确认指令。
消息回查,即重新查询本地事务的执行状态。本例就是重新到DB中查看预扣款操作是否执行成功。
注意,消息回查不是重新执行回调操作。回调操作是进行预扣款操作,而消息回查则是查看预
扣款操作执行的结果。
引发消息回查的原因最常见的有两个:
_1)_回调操作返回_UNKNWON _
_2)TC_没有接收到_TM_的最终全局事务确认指令
关于消息回查,有三个常见的属性设置。它们都在broker加载的配置文件中设置,例如:
transactionTimeout=20,指定TM在20秒内应将最终确认状态发送给TC,否则引发消息回查。默
认为60秒
transactionCheckMax=5,指定最多回查5次,超过后将丢弃消息并记录错误日志。默认15次。
transactionCheckInterval=10,指定设置的多次消息回查的时间间隔为10秒。默认为60秒。
XA(Unix Transaction)是一种分布式事务解决方案,一种分布式事务处理模式,是基于XA协议的。
XA协议由Tuxedo(Transaction for Unix has been Extended for Distributed Operation,分布式操作扩
展之后的Unix事务系统)首先提出的,并交给X/Open组织,作为资源管理器与事务管理器的接口标
准。
XA模式中有三个重要组件:TC、TM、RM。
Transaction Coordinator,事务协调者。维护全局和分支事务的状态,驱动全局事务提交或回滚。
RocketMQ_中_Broker_充当着_TC。
Transaction Manager,事务管理器。定义全局事务的范围:开始全局事务、提交或回滚全局事务。它
实际是全局事务的发起者。
RocketMQ_中事务消息的_Producer_充当着_TM。
Resource Manager,资源管理器。管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事
务的状态,并驱动分支事务提交或回滚。
RocketMQ_中事务消息的_Producer_及_Broker_均是_RM。
注意:上面的转账例子并不是一个典型的XA模式例子,因为典型的XA模式各步骤之间是异步的,而刚才的例子是同步进行的
情况)
事务监听器用于监听消息的发送,也方便我们测试,这里我们模拟一些消息出现了异常,并进行回查.
package com.hyb.demo1;
import org.apache.commons.lang3.StringUtils;
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;
public class ICBCTransactionListener implements TransactionListener {
/*
* 成功的回调函数,消息预提交成功会触发,用来处理本地事务
* */
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object o) {
System.out.println("预提交消息成功:" + msg);
// 假设接收到TAGA的消息就表示扣款操作成功,TAGB的消息表示扣款失败,
// TAGC表示扣款结果不清楚,需要执行消息回查
if (StringUtils.equals("TAGA", msg.getTags())) {
return LocalTransactionState.COMMIT_MESSAGE;
} else if (StringUtils.equals("TAGB", msg.getTags())) {
return LocalTransactionState.ROLLBACK_MESSAGE;
} else if (StringUtils.equals("TAGC", msg.getTags())) {
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.UNKNOW;
}
/*
* 回查
* */
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
System.out.println("执行回查"+messageExt.getTags());
return LocalTransactionState.COMMIT_MESSAGE;
}
}
package com.hyb.demo1;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
public class TransactionProducer {
public static void main(String[] args) throws Exception{
TransactionMQProducer transactionMQProducer=new TransactionMQProducer("transactionMQ");
transactionMQProducer.setNamesrvAddr("hadoop4:9876");
/*
* 定义一个线程池
* 1.线程池中核心线程的数量
* 2.最大的线程的数量
* 3.这是一个时间,当一个线程池中的线程数量大于核心线程数量时,剩余的空闲线程存活时间
* 4.时间单位
* 5.临时存放任务的队列,其参数就是队列的长度
* 6.线程工厂
* */
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;
}
});
// 为生产者设置一个线程池
transactionMQProducer.setExecutorService(executorService);
// 为生产者设置一个事务监听器
transactionMQProducer.setTransactionListener(new ICBCTransactionListener());
transactionMQProducer.start();
String[] tags = {"TAGA","TAGB","TAGC"};
for (int i = 0; i < 3; i++) {
String body=i+"";
Message message = new Message("transaction",tags[i],body.getBytes(StandardCharsets.UTF_8));
// 第二个参数用于指定本地事务的业务逻辑
TransactionSendResult transactionSendResult = transactionMQProducer.sendMessageInTransaction(message, null);
System.out.println("发送结果为:" + transactionSendResult.getSendStatus());
}
}
}
生产者最主要的方法是设置事务监听器
消费者为最基本的消费者代码即可.
启动生产者会发现,消息都发送了,但是又一个消息走了事务监听器的回查函数,这是因为我们让其返回了UNKNOW
else if (StringUtils.equals("TAGC", msg.getTags())) {
return LocalTransactionState.UNKNOW;
}
生产者启动后,我们的设置,虽然会回查,但是消息是一直发送不出去的.所以消费一旦启动了,只能接收到正常的两条消息.
生产者进行消息发送时可以一次发送多条消息,这可以大大提升Producer的发送效率。不过需要注意以
下几点:
默认情况下,一批发送的消息总大小不能超过4MB字节。如果想超出该值,有两种解决方案:
** Producer端需要在发送之前设置Producer的maxMessageSize属性
** Broker端需要修改其加载的配置文件中的maxMessageSize属性
生产者通过send()方法发送的Message,并不是直接将Message序列化后发送到网络上的,而是通过这
个Message生成了一个字符串发送出去的。这个字符串由四部分构成:Topic、消息Body、消息日志
(占20字节),及用于描述消息的一堆属性key-value。这些属性中包含例如生产者地址、生产时间、
要发送的QueueId等。最终写入到Broker中消息单元中的数据都是来自于这些属性。
Consumer的MessageListenerConcurrently监听接口的consumeMessage()方法的第一个参数为消息列
表,但默认情况下每次只能消费一条消息。若要使其一次可以消费多条消息,则可以通过修改
Consumer的consumeMessageBatchMaxSize属性来指定。不过,该值不能超过32。因为默认情况下消
费者每次可以拉取的消息最多是32条。若要修改一次拉取的最大值,则可通过修改Consumer的
pullBatchSize属性来指定。
Consumer的pullBatchSize属性与consumeMessageBatchMaxSize属性是否设置的越大越好?当然不
是。
pullBatchSize值设置的越大,Consumer每拉取一次需要的时间就会越长,且在网络上传输出现
问题的可能性就越高。若在拉取过程中若出现了问题,那么本批次所有消息都需要全部重新拉
取。
consumeMessageBatchMaxSize值设置的越大,Consumer的消息并发消费能力越低,且这批被消
费的消息具有相同的消费结果。因为consumeMessageBatchMaxSize指定的一批消息只会使用一
个线程进行处理,且在处理过程中只要有一个消息处理异常,则这批消息需要全部重新再次消费
处理。
该批量发送的需求是,不修改最大发送4M的默认值,但要防止发送的批量消息超出4M的限制。
package com.learn.rocketmq.batch;
import org.apache.rocketmq.common.message.Message;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* 消息列表分割器:其只会处理每条消息的大小不超4M的情况。
* 若存在某条消息,其本身大小大于4M,这个分割器无法处理,
* 其直接将这条消息构成一个子列表返回。并没有再进行分割
*/
public class MessageListSplitter implements Iterator<List<Message>> {
// 指定极限值为4M
private final int SIZE_LIMIT = 4 * 1024 * 1024;
// 存放所有要发送的消息
private final List<Message> messages;
// 要进行批量发送消息的小集合起始索引
private int currIndex;
public MessageListSplitter(List<Message> messages) {
this.messages = messages;
}
@Override
public boolean hasNext() {
// 判断当前开始遍历的消息索引要小于消息总数
return currIndex < messages.size();
}
@Override
public List<Message> next() {
int nextIndex = currIndex;
// 记录当前要发送的这一小批次消息列表的大小
int totalSize = 0;
for (; nextIndex < messages.size(); nextIndex++) {
// 获取当前遍历的消息
Message message = messages.get(nextIndex);
// 统计当前遍历的message的大小
int tmpSize = message.getTopic().length() + message.getBody().length;
Map<String, String> properties = message.getProperties();
for (Map.Entry<String, String> entry : properties.entrySet()) {
tmpSize += entry.getKey().length() + entry.getValue().length();
}
tmpSize = tmpSize + 20;
// 判断当前消息本身是否大于4M
if (tmpSize > SIZE_LIMIT) {
if (nextIndex - currIndex == 0) {
nextIndex++;
}
break;
}
if (tmpSize + totalSize > SIZE_LIMIT) {
break;
} else {
totalSize += tmpSize;
}
} // end-for
// 获取当前messages列表的子集合[currIndex, nextIndex)
List<Message> subList = messages.subList(currIndex, nextIndex);
// 下次遍历的开始索引
currIndex = nextIndex;
return subList;
}
}
(本人不理解)
package com.learn.rocketmq.batch;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class BatchProducer {
public static void main(String[] args) throws Exception{
DefaultMQProducer bath = new DefaultMQProducer("bath");
bath.setNamesrvAddr("hadoop4:9876");
bath.start();
// 指定要发送的消息的最大大小,默认是4M
// 不过,仅修改该属性是不行的,还需要同时修改broker加载的配置文件中的
// maxMessageSize属性
// producer.setMaxMessageSize(8 * 1024 * 1024);
List<Message> messages=new ArrayList<>();
for (int i = 0; i < 10; i++) {
String body=i+"";
Message message=new Message("bathTopic","b1",body.getBytes(StandardCharsets.UTF_8));
messages.add(message);
}
MessageListSplitter splitter = new MessageListSplitter(messages);
while (splitter.hasNext()) {
try {
List<Message> listItem = splitter.next();
bath.send(listItem);
} catch (Exception e) {
e.printStackTrace();
}
}
bath.shutdown();
}
}
package com.learn.rocketmq.batch;
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.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class BathConsumer {
public static void main(String[] args) throws Exception{
DefaultMQPushConsumer bathConsumer = new DefaultMQPushConsumer("bathConsumer");
bathConsumer.setNamesrvAddr("hadoop4:9876");
bathConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
bathConsumer.subscribe("bathTopic","*");
// 指定一次性可以消费十条消息
bathConsumer.setConsumeMessageBatchMaxSize(10);
// 指定每次拉取四十条消息,默认是32
bathConsumer.setPullBatchSize(40);
bathConsumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
for (MessageExt m :
list) {
System.out.println(m);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
bathConsumer.start();
}
}
消息者在进行消息订阅时,除了可以指定要订阅消息的Topic外,还可以对指定Topic中的消息根据指定
条件进行过滤,即可以订阅比Topic更加细粒度的消息类型。
对于指定Topic消息的过滤有两种过滤方式:Tag过滤与SQL过滤。
通过consumer的subscribe()方法指定要订阅消息的Tag。如果订阅多个Tag的消息,Tag间使用或运算
符(双竖线||)连接。
SQL过滤是一种通过特定表达式对事先埋入到消息中的用户属性进行筛选过滤的方式。通过SQL过滤,
可以实现对消息的复杂过滤。不过,只有使用PUSH模式的消费者才能使用SQL过滤。
SQL过滤表达式中支持多种常量类型与运算符。
支持的常量类型:
支持的运算符有:
默认情况下Broker没有开启消息的SQL过滤功能,需要在Broker加载的配置文件中添加如下属性,以开
启该功能:
enablePropertyFilter = true
在启动Broker时需要指定这个修改过的配置文件。例如对于单机Broker的启动,其修改的配置文件是
conf/broker.conf,启动时使用如下命令:
sh bin/mqbroker -n localhost:9876 -c conf/broker.conf &
# sh bin/mqbroker -n localhost:9876 & 启动Broker
代码比较简单:
消费者:
consumer.subscribe("myTopic", MessageSelector.bySql("age between 0 and 6"));
生产者:
msg.putUserProperty("age", i + "");
Producer对发送失败的消息进行重新发送的机制,称为消息发送重试机制,也称为消息重投机制。
对于消息重投,需要注意以下几点:
生产者在发送消息时,若采用同步或异步发送方式,发送失败会重试,但oneway消息发送方式
发送失败是没有重试机制的
只有普通消息具有发送重试机制,顺序消息是没有的
消息重投机制可以保证消息尽可能发送成功、不丢失,但可能会造成消息重复。消息重复在
RocketMQ中是无法避免的问题
消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会成为大概率事件
producer主动重发、consumer负载变化(发生Rebalance,不会导致消息重复,但可能出现重复
消费)也会导致重复消息
消息重复无法避免,但要避免消息的重复消费。
避免消息重复消费的解决方案是,为消息添加唯一标识(例如消息key),使消费者对消息进行消
费判断来避免重复消费
消息发送重试有三种策略可以选择:同步发送失败策略、异步发送失败策略、消息刷盘失败策略
对于普通消息,消息发送默认采用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会自动重投消息。
异步发送失败重试时,异步重试不会选择其他broker,仅在同一个broker上做重试,所以该策略无法保
证消息不丢。
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketmqOS:9876");
// 指定异步发送失败后不进行重试发送
producer.setRetryTimesWhenSendAsyncFailed(0);
消息刷盘超时(Master或Slave)或slave不可用(
slave在做数据同步时向master返回状态不是
SEND_OK)时,默认是不会将消息尝试发送到其他Broker的。不过,对于重要消息可以通过在Broker
的配置文件设置retryAnotherBrokerWhenNotStoreOK属性为true来开启。
对于顺序消息,当Consumer消费消息失败后,为了保证消息的顺序性,其会自动不断地进行消息重
试,直到消费成功。消费重试默认间隔时间为1000毫秒。重试期间应用会出现消息消费被阻塞的情
况
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
// 顺序消息消费失败的消费重试时间间隔,单位毫秒,默认为1000,其取值范围为[10, 30000]
consumer.setSuspendCurrentQueueTimeMillis(100);
由于对顺序消息的重试是无休止的,不间断的,直至消费成功,所以,对于顺序消息的消费,
务必要保证应用能够及时监控并处理消费失败的情况,避免消费被永久性阻塞。
注意,顺序消息没有发送失败重试机制,但具有消费失败重试机制
对于无序消息(普通消息、延时消息、事务消息),当Consumer消费消息失败时,可以通过设置返回
状态达到消息重试的效果。不过需要注意,无序消息的重试只对集群消费方式生效,广播消费方式不
提供失败重试特性。即对于广播消费,消费失败后,失败消息不再重试,继续消费后续消息。
对于无序消息集群消费下的重试消费,每条消息默认最多重试16次,但每次重试的间隔时间是不同
的,会逐渐变长。每次重试的间隔时间如下表。
若一条消息在一直消费失败的前提下,将会在正常消费后的第4小时46分后进行第_16_次重试。
若仍然失败,则将消息投递到死信队列
修改消息重试次数:
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
// 修改消费重试次数
consumer.setMaxReconsumeTimes(10);
对于修改过的重试次数,将按照以下策略执行:
对于_Consumer Group_,若仅修改了一个_Consumer_的消费重试次数,则会应用到该_Group_中所有
其它_Consumer_实例。若出现多个_Consumer_均做了修改的情况,则采用覆盖方式生效。即最后被
修改的值会覆盖前面设置的值。
对于需要重试消费的消息,并不是Consumer在等待了指定时长后再次去拉取原来的消息进行消费,而
是将这些需要重试消费的消息放入到了一个特殊Topic的队列中,而后进行再次消费的。这个特殊的队
列就是重试队列。
当出现需要进行重试消费的消息时,Broker会为每个消费组都设置一个Topic名称
为%RETRY%consumerGroup@consumerGroup 的重试队列。
因为一个_Topic_的消息可以让多个消费者组进行消费,只有当出现重试消费时,仅会为该消费者组创建重试队列,每个消费者组的消费不一样,所以会为这些消费者组各创建一个重试队列
前面看到消费重试的间隔与延迟消费的时间和等级非常相似,除了没有延迟等级的前两个时间1s和5s之外,其它时间都是相同的,这是因为我们的消费重试的原理也是通过延迟队列进行的
messageDelayLevel = 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 1d
先将消息保存到SCHEDULE_TOPIC_XXXX延迟队
列中,延迟时间到后,会将消息投递到%RETRY%consumerGroup@consumerGroup重试队列中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aBWDfo2k-1653453399712)(https://cdn.nlark.com/yuque/0/2022/png/25796860/1642494119236-efaa9abd-6ebf-4133-990d-ad5ad957afbe.png#clientId=u4957b822-ff0d-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=364&id=u946dd757&margin=%5Bobject%20Object%5D&name=image.png&originHeight=727&originWidth=933&originalType=binary&ratio=1&rotation=0&showTitle=false&size=303480&status=done&style=none&taskId=uf5be55ae-56e4-47d2-a3e8-e67487b0496&title=&width=466.5)]
集群消费方式下,消息消费失败后若不希望消费重试,则在捕获到异常后同样也返回与消费成功后的相
同的结果,即ConsumeConcurrentlyStatus.CONSUME_SUCCESS,则不进行消费重试。
**1 什么是死信队列 **
当一条消息初次消费失败,消息队列会自动进行消费重试;达到最大重试次数后,若消费依然失败,则
表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送
到该消费者对应的特殊队列中。这个队列就是死信队列(Dead-Letter Queue,DLQ),而其中的消息
则称为死信消息(Dead-Letter Message,DLM)。
死信队列是用于处理无法被正常消费的消息的。
**2 死信队列的特征 **
死信队列具有如下特征:
死信队列中的消息不会再被消费者正常消费,即DLQ对于消费者是不可见的
死信存储有效期与正常消息相同,均为 3 天(
commitlog文件的过期时间),3 天后会被自动删除
死信队列就是一个特殊的Topic,名称为%DLQ%consumerGroup@consumerGroup ,即每个消
费者组都有一个死信队列
如果⼀个消费者组未产生死信消息,则不会为其创建相应的死信队列
**3 死信消息的处理 **
实际上,当⼀条消息进入死信队列,就意味着系统中某些地方出现了问题,从而导致消费者无法正常消
费该消息,比如代码中原本就存在Bug。因此,对于死信消息,通常需要开发人员进行特殊处理。最关
键的步骤是要排查可疑因素,解决代码中可能存在的Bug,然后再将原来的死信消息再次进行投递消
费。