RokectMQ 2万字 详解 认真看完必有收获

一、作用

限流消峰

暂存、延后处理超量流量,避免请求丢失,减缓系统压力。

image-20231017152618140

异步解耦

上游服务对下游服务同步调用会大大降低系统吞吐量与并发度,且系统耦合度太高。RokectMQ可以实现两层之间同步到异步的转化。

数据收集

分布式系统会产生海量级数据流,如:业务日志、监控数据、用户行为等。针对这些数据流进行实时或批量采集汇总。

二、基本概念

1、消息(Message)

生产喝消费数据的最小单位,每条消息必须属于一个主题

2、主题(Topic)

一个主题包含n条消息,订阅消息的最小单位。
3、标签(Tag)

当相于主题为一级分类,标签为二级分类。

4、队列(Queue,也称为分区Partition)

存储消息的物理实体。一个主题内有多个Queue,Queue存储在不同Broker(分片),相同主题下通常Queue大小默认为相同,不同大小可通过配置修改。

5、分片(Sharding、Broker)

可以理解为一台服务器为一个Broker,一个Broker包含多个Topic的Queue。

image-20231017153731938

6、消息标识(msgId,key,offsetMsgId)

可理解为分辨消息的唯一主键,producer生成msgId,broker生成offsetMsgId,用户指定key。

msgId = producerIp + 进程pid + MessageClientIDSetter类的ClassLoader的hashCode + 当前时间 + AutomicInteger自增计数器

offsetMsgId = brokerIp + 物理分区的offset(Queue中的偏移量)

三、系统架构

image-20231017154820155

1、Producer(生产者)

Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。

RocketMQ中的消息生产者都是以生产者组(Producer Group)的形式出现的。生产者组是同一类生产者的集合,这类Producer发送相同Topic类型的消息。一个生产者组可以同时发送多个主题的消息。

例举场景(业务调用时产生的日志记录、秒杀业务数据等存入MQ)

2、Consumer(消费者)

消息消费者,负责消费消息。一个消息消费者会从Broker服务器中获取到消息,并对消息进行相关业务处理。

RocketMQ中的消息消费者都是以消费者组(Consumer Group)的形式出现的。Consumer Group消费的是同一个Topic类型的消息。消费者组实现依据响应消费策略实现消费负载均衡(一个Topic中不同的Queue平均分配给同一个Consumer Group的不同的Consumer,注意,并不是将消息负载均衡)和容错(一个Consmer挂了,该Consumer Group中的其它Consumer可以接着消费原Consumer消费的Queue)的目标变得非常容易。

例举场景(从MQ取出数据,实现具体的日志记录、秒杀等业务)

注意:消费者组只能消费一个且相同的Topic的消息

3、Name Server(注册中心)

NameServer是一个Broker与Topic路由的注册中心,支持Broker的动态注册与发现。

1)功能

Broker管理:接受Broker集群的注册信息并且保存下来作为路由信息的基本数据;提供心跳检测机制,检查Broker是否还存活。

路由信息管理:每个NameServer中都保存着Broker集群的整个路由信息和用于客户端查询的队列信息。Producer和Conumser通过NameServer可以获取整个Broker集群的路由信息,从而进行消息的投递和消费。

2)路由注册

NameServer是无状态的,即NameServer集群中的各个节点间是无差异的,各节点间不进行信息通讯。

2.1)各节点如何保持数据同步?

Broker启动后,与每个NameServer建立长连接,发起注册,维护Broker列表存储Broker信息。

Broker节点为了维护与NameServer间的长连接,通过每30秒发送一次心跳包上报NameServer,NameServer在接收到心跳包后,会更新心跳时间戳,记录这

个Broker的最新存活时间。

心跳包中包含 BrokerId、Broker地址(IP+Port)、Broker名称、Broker所属集群名称等等。

3)路由剔除

由于Broker关机、宕机或网络抖动等原因,NameServer没有收到Broker的心跳,NameServer通过定时任务,10秒检查一次最新心跳时间戳距离当前时间是否超过120秒,如果超过,则会判定Broker失效,然后将其从Broker列表中剔除。

RocketMQ的路由发现采用的是Pull模型。客户端每30秒主动拉取一次最新路由。

4)客户端NameServer选择策略(首先采用的是随机策略进行的选择,失败后采用的是轮询策略)

客户端在配置时必须要写上NameServer集群的地址,客户端首先会生产一个随机数,然后再与NameServer节点数量取模,此时得到的就是所要连接的节点索引,然后就会进行连接。如果连接失败,则会采用round-robin策略,逐个尝试着去连接其它节点。

5、Broker模块

image-20231017164520068

Remoting Module:Broker实体,处理来自clients端的请求。

Client Manager:客户端管理器。负责接收、解析客户端请求,管理客户端。例如,维护Consumer的Topic订阅信息

Store Service:存储服务。提供API接口,处理消息存储到物理硬盘和消息查询功能。

HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。

Index Service:索引服务。提供根据key,对消息进行索引查询服务功能。

扩展)

如果某Broker节点宕机,如何保证数据不丢失呢?其解决

将每个Broker集群节点进行横向扩展,即将Broker节点再建为一个HA集群,解决单点问题。

1、Broker节点集群

主从集群,即集群中具有Master与Slave两种角色。Master负责处理读写操作请求,Slave负责对Master中的数据进行备份。

当Master挂了,Slave则会自动切换为Master。所以这个Broker集群是主备集群。一个Master可以包含多个Slave,但一个Slave只能隶属于一个Master。

Master与Slave 的对应关系是通过指定相同的BrokerName、不同的BrokerId 来确定的。BrokerId为0表示Master,非0表示Slave。

每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。

2、工作流程

1)NameServer启动后监听端口,等待Broker、Producer、Consumer连接。

2)启动Broker,Broker会与所有的NameServer建立并保持长连接,每30秒向NameServer定时发送心跳包。

3)发送消息前,可以先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,在创建Topic时也会将Topic与Broker的关系写入到NameServer中。也可以在发送消息时自动创建Topic。

4)Producer发送消息,从NameServer中获取路由信息,即当前发送的Topic消息的Queue与Broker的地址(IP+Port)的映射关系。根据算法策略从队列选择一个Queue,与Queue所在的Broker建立长连接从而向Broker发消息。获取到路由信息后,Producer会首先将路由信息缓存到本地,再每30秒从NameServer更新一次路由信息。

5)Consumer跟Producer类似,获取所订阅Topic的路由信息,然后根据算法策略从路由信息中获取到其所要消费的Queue,跟Broker建立长连接,开始消费。Consumer在获取到路由信息后,同样也会每30秒从NameServer更新一次路由信息。不同于Producer的是,Consumer还会向Broker发送心跳,以确保Broker的存活状态。

3、Topic的创建模式

1)手动创建Topic:

集群模式:该模式下创建的Topic在该集群中,所有Broker中的Queue数量是相同的。

Broker模式:该模式下创建的Topic在该集群中,每个Broker中的Queue数量可以不同。

2)自动创建Topic:

默认采用的是Broker模式,默认每个Broker创建4个Queue。

4、读/写队列

从物理上来讲,读/写队列是同一个队列。所以,不存在读/写队列数据同步问题。

一般情况下,读/写队列数量是相同的。例如,创建Topic时设置的写队列数量为8,读队列数量为4,Consumer只会消费0 1 2 3这4个队列中的消息。

1)为什么要这样设计呢?

方便Topic的Queue的缩容。

例如,原来创建的Topic中包含16个Queue,为了能够使其Queue缩容为8个,还不会丢失消息?可以动态修改写队列数量为8,读队列数量不变。此时新的消息只能写入到前8个队列,而消费都消费的却是16个队列中的数据。当后8个Queue中的消息消费完毕后,就可以再将读队列数量动态设置为8。整个缩容过程,没有丢失任何消息。

2)perm用于设置对当前创建Topic的操作权限:2表示只写,4表示只读,6表示读写。

四、数据复制与刷盘策略

image-20231018165822932

1)复制策略

复制策略是Broker的Master与Slave间的数据同步方式。

同步复制:消息写入master后,master会等待slave同步数据成功后才向producer返回成功ACK

异步复制:消息写入master后,master立即向producer返回成功ACK,无需等待slave同步数据成功

异步复制策略延迟更低,RT更小,更高吞吐量

2)刷盘策略

刷盘策略指的是broker中消息的落盘方式,即消息发送到broker内存后消息持久化到磁盘的方式。

同步刷盘:当消息持久化到broker的磁盘后才算是消息写入成功。

异步刷盘:当消息写入到broker的内存后即表示消息写入成功,无需等待消息持久化到磁盘。

1)异步刷盘策略延迟更低,RT更小,更高吞吐量

2)消息写入到Broker的内存,一般是写入到了PageCache

3)对于异步刷盘策略,消息会写入到PageCache后立即返回成功ACK。不会立即做落盘操作,而是当PageCache到达一定量时会自动进行落盘。

五、Broker集群模式

1)单Master

一个broker(其本质上就不能称为集群)。存在单点问题生产环境不能使用。

2)多Master

多个master构成,不存在Slave。
优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高;
缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅(不可消费),消息实时性会受到影响。
以上优点的前提是,这些Master都配置了RAID磁盘阵列。如果没有配置,一旦出现某Master宕机,则会发生大量消息丢失的情况。

3)多Master多Slave模式-异步复制

多个master、多个slave构成异步复制,(在配置了RAID磁盘阵列的情况下,一个master一般配置一个slave即可)。

master负责处理消息的读写请求,而slave仅负责消息的备份与master宕机后的角色切换。
异步复制即前面所讲的复制策略中的异步复制策略,即消息写入master成功后,master立即向producer返回成功ACK,无需等待slave同步数据成功。
该模式的最大特点之一是,当master宕机后slave能够自动切换为master。不过由于slave从master的同步具有短暂的延迟(毫秒级),所以当master宕机后,这种异步复制方式可能会存在少量消息的丢失问题。
Slave从Master同步的延迟越短,其可能丢失的消息就越少。

对于Master的RAID磁盘阵列,若使用的也是异步复制策略,同样也存在延迟问题,同样也可能会丢失消息。但RAID阵列的秘诀是微秒级的(因为是由硬盘支持的),所以其丢失的数据量会更少。

4)多Master多Slave模式-同步双写

多Master多Slave模式的同步复制。

所谓同步双写,指的是消息写入master成功后,master会等待slave同步数据成功后才向producer返回成功ACK,即master与slave都要写入成功后才会返回成功ACK,也即双写。
该模式与异步复制模式相比,优点是消息的安全性更高,不存在消息丢失的情况。但单个消息的RT略高,从而导致性能要略低(大约低10%)。
该模式存在一个大的问题:对于目前的版本,Master宕机后,Slave不会自动切换到Master。

最佳实践一般会为Master配置RAID10磁盘阵列,然后再为其配置一个Slave。即利用了RAID10磁盘阵列的高效、安全性,又解决了可能会影响订阅的问题。
1)RAID磁盘阵列的效率要高于Master-Slave集群。因为RAID是硬件支持的。也正因为如此,所以RAID阵列的搭建成本较高。
2)多Master+RAID阵列,与多Master多Slave集群的区别是什么?
多Master+RAID阵列,其仅仅可以保证数据不丢失,即不影响消息写入,但其可能会影响到消息的订阅。但其执行效率要远高于多Master多Slave集群多Master多Slave集群,其不仅可以保证数据不丢失,也不会影响消息写入。其运行效率要低于多Master+RAID阵列。

六、工作原理

1)消息生产

1.1)生产过程

Producer将消息写入某Broker中的某Queue中,其经历了如下过程:

Producer先向NameServer发出获取消息Topic的路由信息的请求,NameServer返回该Topic的路由表及Broker列表,Producer根据代码中指定的Queue选择策略,从Queue列表中选出一个队列,用于后续存储消息,同时Produer对消息做一些特殊处理,例如,消息本身超过4M,则会对其进行压缩,Producer向选择出的Queue所在的Broker发出RPC请求,将消息发送到选择出的Queue。

路由表:{TopicName:[TopicBrokerName]}

Broker列表:{brokerName:{brokerId:broker地址}(brokerId为0表示该broker为Master,非0表示Slave)

1.2)Queue选择算法

对于无序消息,其Queue选择算法,也称为消息投递算法。
轮询算法:默认选择算法。该算法保证了每个Queue中可以均匀的获取到消息。问题:由于某些原因,在某些Broker上的Queue可能投递延迟较严重。从而导致Producer的缓存队列中出现较大的消息积压,影响消息的投递性能。
最小投递延迟算法:该算法会统计每次消息投递的时间延迟,然后根据统计出的结果将消息投递到时间延迟最小的Queue。如果延迟相同,则采用轮询算法投递。该算法可以有效提升消息的投递性能。问题:消息在Queue上的分配不均匀。投递延迟小的Queue其可能会存在大量的消息。而对该Queue的消费者压力会增大,降低消息的消费能力,可能会导致MQ中消息的堆积。

2)消息存储

image-20231018173236105

2.1)文件简要

abort:Broker启动后会自动创建,正常关闭Broker该文件会自动消失。若在没有启动Broker的情况下,文件存在则说明之前Broker是非正常关闭。

checkpoint:存储commitlog、consumequeue、index文件最后刷盘时间戳

commitlog(文件夹):存放commitlog(文件),消息是写在commitlog(文件)中

conæg:Broker运行期间的一些配置数据

consumequeue(文件夹):存放consumequeue文件,队列消费信息写在consumequeue(文件)

index:存放消息索引文件indexFile

lock:运行期间使用到的全局资源锁

2.2)commitlog文件(又称mappedFile)

当前Broker中的所有消息都是落盘到commitlog目录的mappedFile文件中。

mappedFile文件大小小于等于1G,文件名由20位十进制数构成,表示当前文件的第一条消息的起始位移偏移量。

第一个文件名一定是200构成的。因为偏移量commitlog offset为0,当第一个文件放满时,则会自动生成第二个文件继续存放消息。假设第一个文件大小是1073741820字节(1G = 1073741824字节),则第二个文件名就是00000000001073741824。

一个Broker中所有mappedFile文件的commitlog offset是连续的

一个Broker中仅包含一个commitlog目录,无论当前Broker中存放着多少Topic的消息,这些消息都是被顺序写入到了mappedFile文件中的。这些消息在Broker中存放时并没有被按照Topic进行分类存放。

mappedFile文件是顺序读写的文件,所有其访问效率很高。

2.2.1)消息单元

image-20231018174653182

mappedFile文件内容由消息单元构成。

每个消息单元中包含消息总长度MsgLen、消息的物理位置physicalOffset、消息体内容Body、消息体长度BodyLength、消息主题Topic、Topic长度TopicLength、消息生产者BornHost、消息发送时间戳BornTimestamp、消息所在的队列QueueId、消息在Queue中存储的偏移量QueueOffset等近20余项消息相关属性。

2.3)consumequeue

image-20231018175138902

为了提高效率,会为每个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可能是不同的

2.4)消息读写

一条消息进入到Broker后经历了以下几个过程才最终被持久化。

Broker根据queueId,获取到该消息对应索引条目要在consumequeue目录中的写入偏移量,即QueueOffset将queueId、queueOffset等数据,与消息一起封装为消息单元将消息单元写入到commitlog同时,形成消息索引条目将消息索引条目分发到相应的consumequeue。

2.5)消息拉取

当Consumer来拉取消息时会经历以下几个步骤:

Consumer获取到其要消费消息所在Queue的消费偏移量offset,计算出其要消费消息的消息offset

Consumer向Broker发送拉取请求,其中会包含其要拉取消息的Queue、消息offset及消息Tag。Broker计算在该consumequeue中的queueOffset。从该queueOffset处开始向后查找第一个指定Tag的索引条目。解析该索引条目的前8个字节,即可定位到该消息在commitlog中的commitlog offset,从对应commitlog offset中读取消息单元,并发送给Consumer。

消息offset = 消费offset + 1

消费offset即消费进度,consumer对某个Queue的消费offset,即消费到了该Queue的第几条消息。

queueOffset = 消息offset * 20字节

2.6)性能提升

RocketMQ中,无论是消息本身还是消息索引,都是存储在磁盘上。

RocketMQ对文件的读写操作是通过mmap零拷贝进行的,将对文件的操作转化为直接对内存地址进行操作,极大提高了文件读写效率。

其次,consumequeue中的数据是顺序存放的,还引入了PageCache的预读取机制,使得对consumequeue文件的读取几乎接近于内存读取,即使在有消息堆积情况下也不会影响性能。

PageCache机制,页缓存机制,是OS对文件的缓存机制,用于加速对文件的读写操作。一般来说,程序对文件进行顺序读写的速度几乎接近于内存读写速度,主要原因是由于OS使用。PageCache机制对读写访问操作进行性能优化,将一部分的内存用作PageCache

写操作:OS会先将数据写入到PageCache中,随后会以异步方式由pdfush(page dirty fush)内核线程将Cache中的数据刷盘到物理磁盘

读操作:若用户要读取数据,其首先会从PageCache中读取,若没有命中,则OS在从物理磁盘上加载该数据到PageCache的同时,也会顺序对其相邻数据块中的数据进行预读取。

RocketMQ中可能会影响性能的是对commitlog文件的读取。因为对commitlog文件来说,读取消息时会产生大量的随机访问,而随机访问会严重影响性能。不过,如果选择合适的系统IO调度算法,比如设置调度算法为Deadline(采用SSD固态硬盘的话),随机读的性能也会有所提升。

3)indexFile

image-20231018180915440

RocketMQ还提供了根据key进行消息查询的功能。该查询是通过store目录中的index子目录中的indexFile进行索引实现的快速查询。当拥有key的消息被发送到Broker时写入,如果消息中没有包含key,则不会写入。

3.1)索引条目结构

每个Broker中会包含一组indexFile,每个indexFile都是以一个时间戳命名的(indexFile创建时的时间戳)。每个indexFile文件由三部分构成:indexHeader,slots槽位,indexes索引数据。每个indexFile文件中包含500w个slot槽。每个slot槽又会挂载多个index索引单元。

image-20231018180932190

indexHeader固定40个字节,结构如下:

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索引单元的数量之和)

image-20231018181102553

slot和indexes关系(结构简单理解为hash链表,但指向方向不同):

key的hash值 % 500w的结果即为slot槽位,然后将该slot值修改为该index索引单元的indexNo,根据这个indexNo可以计算出该index单元在indexFile中的位置。每个index索引单元中preIndexNo用于指定该slot中当前index索引单元的前一个index索引单元。而slot中始终存放的是其下最新的index索引单元的indexNo,只要找到了slot就可以找到其最新的index索引单元,而通过这个index索引单元就可以找到其之前的所有index索引单元。

indexNo是一个在indexFile中的流水号,从0开始依次递增。即在一个indexFile中所有indexNo是以此递增的。indexNoindex索引单元中是没有体现的,其是通过indexes中依次数出来的。

image-20231018181344818

index索引单元默写20个字节,其中存放着以下四个属性:

keyHash:消息中指定的业务key的hash值

phyOffset:当前key对应的消息在commitlog中的偏移量commitlog offset

timeDiff:当前key对应消息的存储时间与当前indexFile创建时间的时间差

preIndexNo:当前slot下当前index索引单元的前一个index索引单元的indexNo

3.2)indexFile的创建

indexFile的文件名为当前文件被创建时的时间戳。这个时间戳有什么用处呢?

根据业务key进行查询时,查询条件除了key之外,还需要指定一个要查询的时间戳,表示要查询不大于该时间戳的最新的消息,即查询指定时间戳之前存储的最新消息。这个时间戳文件名可以简化查询,提高查询效率。

indexFile文件是何时创建的?其创建的条件(时机)有两个:

当第一条带key的消息发送来后,系统发现没有indexFile,此时会创建第一个indexFile文件,当一个indexFile中挂载的index索引单元数量超出2000w个时,会创建新的indexFile。当带key的消息发送到来后,系统会找到最新的indexFile,并从其indexHeader的最后4字节中读取到。indexCount。若indexCount >= 2000w时,会创建新的indexFile。

由于可以推算出,一个indexFile的最大大小是:(40 + 500w * 4 + 2000w* 20)字节

3.3)索引查询流程

image-20231018181749790

为indexFile中indexHeader的字节数 = 40

所有slots所占的字节数 = 500w * 4

具体查询流程如下:

计算指定消息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

4)消息的消费

4.1)消费方式
4.1.1)pull拉取

Consumer主动从Broker中拉取消息,主动权由Consumer控制。一旦获取了批量消息,就会启动消费过程。

问题:实时性低,不能及时发现并消费新消息,拉去间隔过长频繁空转。

4.1.2)push推送

Broker收到数据后会主动推送给Consumer,典型的发布-订阅模式,即Consumer向其关联的Queue注册了监听器,一旦发现有新的消息到来就会触发回调的执行,回调方法是Consumer去Queue中拉取消息。而这些都是基于Consumer与broker长连接。

问题:长连接,耗资源。

4.1.3)对比

pull:需要应用去实现对关联Queue的遍历,实时性差;但便于应用控制消息的拉取

push:封装了对关联Queue的遍历,实时性强,但长连接,耗系统资源

4.2)消费模式
4.2.1)广播消费

即每条消息都会被发送到Consumer Group中的每个Consumer。消费进度保存在consumer端。

4.2.2)集群消费

相同Consumer Group的每个Consumer实例平均分摊同一个Topic的消息。即每条消息只会被发送到Consumer Group中的某个Consumer。消费进度保存在broker中。消费进度会参与到了消费的负载均衡中,故消费进度是需要共享的。

4.3)Rebalance机制(前提:集群消费)

Rebalance即再均衡,指的是将⼀个Topic下的多个Queue在同⼀个Consumer Group中的多个Consumer间进行重新分配的过程。Rebalance机制的本意是为了提升消息的并行消费能力。

4.3.1)限制

由于⼀个队列最多分配给⼀个消费者,因此当某个消费者组下的消费者实例数量大于队列的数量时,多余的消费者实例将分配不到任何队列。

4.3.2)危害

消费暂停:在只有一个Consumer时,其负责消费所有队列;在新增了一个Consumer后会触发Rebalance。此时原Consumer就需要暂停部分队列的消费,等到这些队列分配给新的Consumer后,这些暂停消费的队列才能继续被消费。

消费重复:Consumer 在消费新分配给自己的队列时,必须接着之前Consumer 提交的消费进度的offset继续消费。然而默认情况下,offset是异步提交的,这个异步性导致提交到Broker的offset与Consumer实际消费的消息并不一致。这个不一致的差值就是可能会重复消费的消息。

消费突刺:由于Rebalance可能导致重复消费,如果需要重复消费的消息过多,或者因为Rebalance暂停时间过长从而导致积压了部分消息。那么有可能会导致在Rebalance结束之后瞬间需要消费很多消息。

4.3.3)扩展

同步提交:consumer提交了其消费完毕的一批消息的offsetbroker后,需要等待broker的成功ACK。当收到ACK后,consumer才会继续获取并消费下一批消息。在等待ACK期间,consumer是阻塞的。

异步提交:consumer提交了其消费完毕的一批消息的offsetbroker后,不需要等待broker的成功ACKconsumer可以直接获取并消费下一批消息。

对于一次性读取消息的数量,需要根据具体业务场景选择一个相对均衡的是很有必要的。因为数量过大,系统性能提升了,但产生重复消费的消息数量可能会增加;数量过小,系统性能会下降,但被重复消费的消息数量可能会减少。

4.3.4)原因

消费者所订阅Topic的Queue数量发生变化,或消费者组中消费者的数量发生变化。

1Queue数量发生变化的场景:

Broker扩容或缩容、Broker升级运维、BrokerNameServer间的网络异常

Queue扩容或缩容

2)消费者数量发生变化的场景:

Consumer Group扩容或缩容、Consumer升级运维、ConsumerNameServer间网络异常

4.3.5)过程

在Broker中维护着多个Map集合,这些集合中动态存放着当前Topic中Queue的信息、Consumer Group中Consumer实例的信息。一旦发现消费者所订阅的Queue数量发生变化,或消费者组中消费者的数量发生变化,立即向Consumer Group中的每个实例发出Rebalance通知。

TopicConfigManager:keytopic名称,value是TopicConfig。TopicConfig中维护着该Topic中所有Queue的数据。

ConsumerManagerkeyConsumser Group IdvalueConsumerGroupInfo

ConsumerGroupInfo中维护着该Group中所有Consumer实例数据。

ConsumerOffsetManagerkey为Topic与订阅该Topic的Group的组合,即topic@group,value是一个内层Map。内层MapkeyQueueId,内层Mapvalue为该Queue的消费进度offset。

Consumer实例在接收到通知后会采用Queue分配算法自己获取到相应的Queue,即由Consumer实例自主进行Rebalance。

4.4)queue分配算法

一个Topic中的Queue只能由Consumer Group中的一个Consumer进行消费,而一个Consumer可以同时

消费多个Queue中的消息。

queue分配策略通过在创建Consumer时的构造器传进去。

平均分配策略

根据avg = QueueCount / ConsumerCount 的计算结果进行分配的。即,先计算好每个Consumer应该分得几个Queue,然后再依次将这些数量的Queue逐个分配个Consumer

image-20231019093846490

环形平均策略

环形平均算法是指,根据消费者的顺序,依次在由queue队列组成的环形图中逐个分配。该算法不用事先计算每个Consumer需要分配几个Queue,直接一个一个分即可。

image-20231019093910133

一致性hash策略

将consumer的hash值作为Node节点存放到hash环上,然后将queue的hash值也放到hash环

上,通过顺时针方向,距离queue最近的那个consumer就是该queue要分配的consumer。

问题:分配不均。

同机房策略

该算法会根据queue的部署机房位置和consumer的位置,过滤出当前consumer相同机房的queue。然后按照平均分配策略或环形平均策略对同机房queue进行分配。如果没有同机房queue,则按照平均分配策略或环形平均策略对所有queue进行分配。

对比

问题:两种平均分配策略的分配效率较高,一致性hash策略的较低。因为一致性hash算法较复杂。另外,一

致性hash策略分配的结果也很大可能上存在不平均的情况。

一致性hash算法存在的意义:其可以有效减少由于消费者组扩容或缩容所带来的大量的Rebalance。

一致性hash算法的应用场景:Consumer数量变化较频繁场景。

5)至少一次原则

RocketMQ有一个原则:每条消息必须要被成功消费一次。

那么什么是成功消费呢?Consumer在消费完消息后会向其消费进度记录器提交其消费消息的offset,offset被成功记录到记录器中,那么这条消费就被成功消费了。

什么是消费进度记录器?

对于广播消费模式来说,Consumer本身就是消费进度记录器。

对于集群消费模式来说,Broker是消费进度记录器。

6)订阅关系的一致性

订阅关系的一致性指的是,同一个消费者组(Group ID相同)下所有Consumer实例所订阅的Topic与Tag及对消息的处理逻辑必须完全一致。否则,消息消费的逻辑就会混乱,甚至导致消息丢失。

6.1)正确订阅关系

多个消费者组订阅了多个Topic,并且每个消费者组里的多个消费者实例的订阅关系保持了一致。

6.2)错误订阅关系

订阅了不同Topic、订阅了不同Tag、订阅了不同数量的Topic

7)消费offset管理

消费进度offset是用来记录每个Queue的不同消费组的消费进度的。根据消费进度记录器的不同,可以分为两种模式:本地模式和远程模式。

7.1)本地管理模式

当消费模式为广播消费时,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}为消费者组名称。

7.2)远程管理模式

当消费模式为集群消费时,offset使用远程模式管理。

Consumer在集群消费模式下offset相关数据以json的形式持久化到Broker磁盘文件中,文件路径为当前

用户主目录下的store/config/consumerOffset.json 。Broker启动时会加载这个文件,并写入到一个双层Map(ConsumerOffsetManager)。结构为{topic@group:{queueId:offset}}。

集群模式下offset采用远程管理模式,主要是为了保证Rebalance机制,当发生Rebalance时,新的Consumer会从该Map中获取到相应的数据来继续消费。

7.3)用途

消费者是如何从最开始持续消费消息的?

消费者要消费的第一条消息的起始位置是用户自己通过consumer.setConsumeFromWhere()方法指定的。在Consumer启动后,其要消费的第一条消息的起始位置常用的有三种。

ConsumeFromWhere:

CONSUME_FROM_LAST_OFFSET:从queue的当前最后一条消息开始消费

CONSUME_FROM_FIRST_OFFSET:从queue的第一条消息开始消费

CONSUME_FROM_TIMESTAMP:从指定的具体时间戳位置的消息开始消费。这个具体时间戳是通过另外一个语句指定的 。(consumer.setConsumeTimestamp(“20210701080000”) yyyyMMddHHmmss)

当消费完一批消息后,Consumer会提交其消费进度offset给Broker,Broker在收到消费进度后会将其更新到ConsumerOffsetManager及consumerOffset.json文件中,然后向该Consumer进行ACK,而ACK内容中包含三项数据:当前消费队列的最小offset(minOffset)、最大offset(maxOffset)、及下次消费的起始offset(nextBeginOffset)。

7.4)重试队列

消费出现异常时,会将发生异常的消息的offset提交到Broker中的重试队列。系统在发生消息消费异常时会为当前的topic@group创建一个重试队列,该队列以%RETRY%开头,到达重试时间后进行消费重试。

7.5)offset的同步提交与异步提交

集群消费模式下,Consumer消费完消息后会向Broker提交消费进度offset,其提交方式分为两种:

7.5.1)同步提交

消费者在消费完一批消息后会向broker提交这些消息的offset,然后等待broker的成功响应。若在等待超时之前收到了成功响应,则继续读取下一批消息进行消费(从ACK中获取nextBeginOffset)。若没有收到响应,则会重新提交,直到获取到响应。而在这个等待过程中,消费者是阻塞的。其严重影响了消费者的吞吐量。

7.5.2)异步提交

消费者在消费完一批消息后向broker提交offset,但无需等待Broker的成功响应,可以继续读取并消费下一批消息。这种方式增加了消费者的吞吐量。但需要注意,broker在收到提交的offset后,还是会向消费者进行响应的。可能还没有收到ACK,此时Consumer会从Broker中直接获取nextBeginOffset。

七、消费幂等

1)概念

当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响,那么这个消费过程就是消费幂等的。

幂等:若某操作执行多次与执行一次对系统产生的影响是相同的,则称该操作是幂等的。

重复发送或重复消费可能会影响业务处理,那么应该对消息做幂等处理。

2)消息重复的场景分析

2.1)发送时消息重复

当一条消息已被成功发送Broker并完成持久化,出现网络闪断,导致Broker对Producer应答失败。 如果此时Producer意识到消息发送失败并尝试再次发送消息,此时Broker中就可能会出现两条内容相同并且Message ID也相同的消息,那么后续Consumer就一定会消费两次该消息。

2.2)消费时消息重复

消息已投递到Consumer并完成业处理,当Consumer给Broker反馈应答时网络闪断,Broker没有接收到消费成功响应。为了保证消息至少被消费一次的原则,Broker将在网络恢复后再次尝试投递之前已被处理过的消息。此时消费者就会收到与之前处理过的内容相同、Message ID也相同的消息。

2.3)Rebalance时消息重复

当Consumer Group中的Consumer数量发生变化时,或其订阅的Topic的Queue数量发生变化时,会触发Rebalance,此时Consumer可能会收到曾经被消费过的消息。

3)通用解决方案

3.1)两要素
3.1.1)幂等令牌(简单理解:能够辨认消息唯一的标识)

生产者和消费者两者中的既定协议,通常指具备唯⼀业务标识的字符串。例如,订单号、流水号。一般由Producer随着消息一同发送来的。

3.1.1.1)幂等令牌实现流程(在同一事务中完成三项操作)
  1. 首先缓存去重。在缓存中如果已经存在了某幂等令牌,则说明本次操作是重复性操作;若缓存没有命中,则进入下一步。

  2. 在唯一性处理之前,先在数据库中查询幂等令牌作为索引的数据是否存在。若不存在,则进入下一步。

  3. 唯一性处理后,将幂等令牌写入到缓存,并将幂等令牌作为唯一索引的数据写入到DB中。

分成三部原因:一般缓存中的数据是具有有效期的。缓存中数据的有效期一旦过期,就是发生缓存穿透,使请求直接就到达了DBMS

3.1.2)唯一性处理(简单理解:同样的只会执行一次)

服务端通过采用⼀定的算法策略,保证同⼀个业务逻辑不会被重复执行成功多次。

3.2)消费幂等的实现总结

为消息指定不会重复的唯一标识。因为Message ID有可能出现重复的情况,所以真正安全的幂等处理,不建议以Message ID作为处理依据。最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息Key设置。RocketMQ能够保证消息不丢失,但不能保证消息不重复。

八、消息堆积与消费延迟

1)概念

消息处理流程中,如果Consumer的消费速度跟不上Producer的发送速度(生产 > 消费),MQ中未处理的消息会越来越多,这部分消息就被称为堆积消息。消息出现堆积进而会造成消息的消费延迟。

以下场景需要重点关注消息堆积和消费延迟问题:

业务系统上下游能力不匹配造成的持续堆积,且无法自行恢复。

业务系统对消息的消费实时性要求较高,即使是短暂的堆积造成的消费延迟也无法接受。

2)原因

Consumer使用长轮询Pull模式消费消息时,分为以下两个阶段:

2.1)消息拉取

Consumer通过长轮询Pull模式批量拉取的方式从服务端获取消息,将拉取到的消息缓存到本地缓冲队列中。对于拉取式消费,在内网环境下会有很高的吞吐量,所以这一阶段一般不会成为消息堆积的瓶颈。

一个单线程单分区的低规格主机*(Consumer*,4C8G),其可达到几万的TPS。如果是多个分区多个线程,则可以轻松达到几十万的TPS

2.2)消息消费

Consumer将本地缓存的消息提交到消费线程中,使用业务消费逻辑对消息进行处理,处理完毕后获取到一个结果。这是真正的消息消费过程。此时Consumer的消费能力就完全依赖于消息的消费耗时和消费并发度了。如果由于业务处理逻辑复杂等原因,导致处理单条消息的耗时较长,则整体的消息吞吐量肯定不会高,此时就会导致Consumer本地缓冲队列达到上限,停止从服务端拉取消息。

(简单理解:消费缓存队列满了,停止拉取造成堆积)

2.3)结论

消息堆积的主要瓶颈在于客户端的消费能力,而消费能力由消费耗时和消费并发度决定。注意,消费耗时的优先级要高于消费并发度。即在保证了消费耗时的合理性前提下,再考虑消费并发度问题。

3)消费耗时

影响消息处理时长的主要因素是代码逻辑。而代码逻辑中可能会影响处理时长代码主要有两种类型:CPU内部计算型代码和外部I/O操作型代码。

通常情况下没有复杂的递归和循环的话,内部计算耗时相对外部I/O操作来说几乎可以忽略。所以外部IO型代码是影响消息处理时长的主要症结所在。

外部IO操作型代码举例:

读写外部数据库,例如对远程MySQL的访问

读写外部缓存系统,例如对远程Redis的访问

下游系统调用,例如DubboRPC远程调用,Spring Cloud的对下游系统的Http接口调用

关于下游系统调用逻辑需要进行提前梳理,掌握每个调用操作预期的耗时,这样做是为了能够判断消费逻辑中IO操作的耗时是否合理。通常消息堆积是由于下游系统出现了服务异常或达到了DBMS容量限制,导致消费耗时增加。

服务异常,并不仅仅是系统中出现的类似500这样的代码错误,而可能是更加隐蔽的问题。例如,网络带宽问题。

达到了DBMS容量限制,其也会引发消息的消费耗时增加。

4)消费并发度

单节点线程数,即单个Consumer所包含的线程数量

节点数量,即Consumer Group所包含的Consumer数量

一般情况下,消费者端的消费并发度由单节点线程数和节点数量共同决定,其值为单节点线程数节点数量。不过,通常需要优先调整单节点的线程数,若单机硬件资源达到了上限,则需要通过横向扩展来提高消费并发度。

对于普通消息、延时消息及事务消息,并发度计算都是单节点线程数 * 节点数量。

而顺序消息的消费并发度等于Topic的Queue分区数量。

4.1)全局顺序消息

该类型消息的Topic只有一个Queue分区。其可以保证该Topic的所有消息被顺序消费。为了保证这个全局顺序性,Consumer Group中在同一时刻只能有一个Consumer的一个线程进行消费。所以其并发度为1

4.2)分区顺序消息

该类型消息的Topic有多个Queue分区。其仅可以保证该Topic的每个Queue分区中的消息被顺序消费,不能保证整个Topic中消息的顺序消费。为了保证这个分区顺序性,每个Queue分区中的消息在Consumer Group中的同一时刻只能有一个Consumer的一个线程进行消费。即,在同一时刻最多会出现多个Queue分蘖有多个Consumer的多个线程并行消费。所以其并发度为Topic的分区数量。

4.3)单机线程数计算

不能盲目直接调大线程数,设置过大的线程数反而会带来大量的线程切换的开销。理想环境下单节点的最优线程数计算模型为:C *(T1 + T2)/ T1。

C:CPU内核数

T1:CPU内部逻辑计算耗时

T2:外部IO操作耗时

最优线程数 = C **(T1 + T2/ T1 = C * T1/T1 + C * T2/T1 = C + C * T2/T1*

注意,该计算出的数值是理想状态下的理论数据,在生产环境中,不建议直接使用。而是根据当前环境,先设置一个比该值小的数值然后观察其压测效果,然后再根据效果逐步调大线程数,直至找到在该环境中性能最佳时的值。

4.4)避免消息堆积和消费延迟

需要在前期设计阶段对整个业务逻辑进行完善的排查和梳理。其中最重要的就是梳理消息的消费耗时和设置消息消费的并发度。

4.4.1)梳理消息的消费耗时

通过压测获取消息的消费耗时,并对耗时较高的操作的代码逻辑进行分析。梳理消息的消费耗时需要关注以下信息:

消息消费逻辑的计算复杂度是否过高,代码是否存在无限循环和递归等缺陷。消息消费逻辑中的I/O操作是否是必须的,能否用本地缓存等方案规避。

消费逻辑中的复杂耗时的操作是否可以做异步化处理。如果可以,是否会造成逻辑错乱。

4.4.2)设置消费并发度

对于消息消费并发度的计算,可以通过以下两步实施:

逐步调大单个Consumer节点的线程数,并观测节点的系统指标,得到单个节点最优的消费线程数和消息吞吐量。

根据上下游链路的流量峰值计算出需要设置的节点数

节点数 = 流量峰值 / 单个节点消息吞吐量

九、消息的清理

注意:消息被消费过后不会被立即清理掉!

消息是被顺序存储在commitlog文件的,且消息大小不定长,所以消息的清理是不可能以消息为单位进行清理的,而是以commitlog文件为单位进行清理的。否则会急剧下降清理效率,并实现逻辑复杂。

commitlog文件存在一个过期时间,默认为72小时,即三天。除了用户手动清理外,在以下情况下也会被自动清理,无论文件中的消息是否被消费过:

文件过期,且到达清理时间点(默认为凌晨4点)后,自动清理过期文件

文件过期,且磁盘空间占用率已达过期清理警戒线(默认75%)后,无论是否达到清理时间点,都会自动清理过期文件

磁盘占用率达到清理警戒线(默认85%)后,开始按照设定好的规则清理文件,无论是否过期。默认会从最老的文件开始清理

磁盘占用率达到系统危险警戒线(默认90%)后,Broker将拒绝消息写入

需要注意以下几点:

1)对于RocketMQ系统来说,删除一个1G大小的文件,是一个压力巨大的IO操作。在删除过程中,系统性能会骤然下降。所以,其默认清理时间点为凌晨4点,访问量最小的时间。也正因如果,我们要保障磁盘空间的空闲率,不要使系统出现在其它时间点删除commitlog文件的情况。

2)官方建议RocketMQ服务的Linux文件系统采用ext4。因为对于文件删除操作,ext4要比ext3性能更好

十、应用

1)普通消息

1.1)同步发送消息

Producer发出⼀条消息后,会在收到MQ返回的ACK之后才发下⼀条消息。该方式的消息可靠性最高,但消息发送效率太低。

DefaultMQProducer defaultMQProducer = new DefaultMQProducer("product-group");
defaultMQProducer.setNamesrvAddr("0.0.0.0:9876");
defaultMQProducer.start();
for (int i = 0; i < 50; i++) {
    String str = "message:" + i;
    byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
    Message message = new Message("topic","tag",bytes);
    defaultMQProducer.send(message);
    log.info("发送消息:{}",i);
}
defaultMQProducer.shutdown();
1.2)异步发送消息

Producer发出消息后无需等待MQ返回ACK,直接发送下⼀条消息。该方式的消息可靠性可以得到保障,消息发送效率也可以。

DefaultMQProducer defaultMQProducer = new DefaultMQProducer("product-group");
defaultMQProducer.setNamesrvAddr("0.0.0.0:9876");
defaultMQProducer.start();
for (int i = 50; i < 100; i++) {
    String msg = "异步发送消息:" + i;
    Message message = new Message("topic", "tag", msg.getBytes(StandardCharsets.UTF_8));
    defaultMQProducer.send(message, new SendCallback() {
        @Override
        public void onSuccess(SendResult sendResult) {
            System.out.println("生产者消息发送,msg:" + sendResult);
        }
        @Override
        public void onException(Throwable throwable) {
            throwable.printStackTrace();
        }
    });
}
// 因为异步,所以需要睡眠或设置线程池
Thread.sleep(5000);
defaultMQProducer.shutdown();
1.3)单向发送消息

Producer仅负责发送消息,不等待、不处理MQ的ACK。该发送方式时MQ也不返回ACK。该方式的消息发送效率最高,但消息可靠性较差。

DefaultMQProducer defaultMQProducer = new DefaultMQProducer("product-group");
defaultMQProducer.setNamesrvAddr("0.0.0.0:9876");
defaultMQProducer.start();
for (int i = 50; i < 100; i++) {
    String msg = "异步发送消息:" + i;
    Message message = new Message("topic", "tag", msg.getBytes(StandardCharsets.UTF_8));
    defaultMQProducer.sendOneway(message);
    log.info("生产者消息发送,msg:{}",msg);
}
defaultMQProducer.shutdown();

2)顺序消息

2.1)概念

顺序消息指的是,严格按照消息的发送顺序进行消费的消息(FIFO)。

默认情况下生产者会把消息以Round Robin轮询方式发送到不同的Queue分区队列;而消费消息时会从多个Queue上拉取消息,这种情况下的发送和消费是不能保证顺序的。如果将消息仅发送到同一个Queue中,消费时也只从这个Queue上拉取消息,就严格保证了消息的顺序性。

2.2)为什么需要顺序消息(例举应用场景)

(业务需要顺序执行):订单T0000001:未支付 --> 订单T0000001:已支付 --> 订单T0000001:发货中 --> 订单T0000001:发货失败

2.3)有序性分类
2.3.1)全局有序

image-20231019105248584

当发送和消费参与的Queue只有一个时所保证的有序是整个Topic中消息的顺序, 称为全局有序。

扩展,创建Topic时指定Queue的数量。有三种指定方式:

1)在代码中创建Producer时,可以指定其自动创建的TopicQueue数量

2)在RocketMQ可视化控制台中手动创建Topic时指定Queue数量

3)使用mqadmin命令手动创建Topic时指定Queue数量

2.3.2)分区有序

image-20231019105306209

如果有多个Queue参与,其仅可保证在该Queue分区队列上的消息顺序,则称为分区有序。

如何实现Queue的选择?

在定义Producer时我们可以实现MessageQueueSelector接口定义。一般性的选择算法是,让选择key(或其hash值)与该Topic所包含的Queue的数量取模,其结果即为选择出的Queue的QueueId。

问题:不同选择keyQueue数量取模结果可能会是相同的,即不同选择key的消息可能会出现在相同的Queue,即同一个Consuemr可能会消费到不同选择key的消息。

如何解决?

一般性的作法是,从消息中获取到选择key,对其进行判断。若是当前Consumer需要消费的消息,则直接消费,否则,什么也不做。这种做法要求选择key要能够随着消息一起被Consumer获取到。此时使用消息key作为选择key是比较好的做法。

不属于那个Consumer的消息被拉取走,那么应该消费该消息的Consumer是否还能再消费该消息呢?同一个Queue中的消息不可能被同一个Group中的不同Consumer同时消费。所以,消费现一个Queue的不同选择key的消息的Consumer一定属于不同的Group。而不同的Group中的Consumer间的消费是相互隔离的,互不影响的。

3)延时消息

3.1)概念

当消息写入到Broker后,在指定的时长后才可被消费处理的消息,称为延时消息。

采用RocketMQ的延时消息可以实现定时任务的功能,而无需使用定时器。

典型的应用场景是,电商交易中超时未支付关闭订单的场景,12306平台订票超时未支付取消订票的场景。在电商平台中,订单创建时会发送一条延迟消息。这条消息将会在30分钟后投递给后台业务系统(Consumer),后台业务系统收到该消息后会判断对应的订单是否已经完成支付。如果未完成,则取消订单,将商品再次放回到库存;如果完成支付,则忽略。

3.2)延时等级

延时消息的延迟时长不支持随意时长的延迟,是通过特定的延迟等级来指定的。延时等级定义在RocketMQ服务端的MessageStoreConfig类中的如下变量中:

image-20231019105532889

即,若指定的延时等级为3,则表示延迟时长为10s,即延迟等级是从1开始计数的。如需其他时间,需到配置文件自行新增相应时间配置。

3.3)流程原理
3.3.1)修改消息

Producer将消息发送到Broker后,Broker会首先将消息写入到commitlog文件,然后需要将其分发到相应的consumequeue。不过,在分发之前,系统会先判断消息中是否带有延时等级。若没有,则直接正常分发;若有则需要经历一个复杂的过程:修改消息的Topic为SCHEDULE_TOPIC_XXXX根据延时等级,在consumequeue目录中SCHEDULE_TOPIC_XXXX主题下创建出相应的queueId目录与consumequeue文件(如果没有这些目录与文件的话)。延迟等级delayLevelqueueId的对应关系为queueId = delayLevel -1

需要注意,在创建queueId目录时,并不是一次性地将所有延迟等级对应的目录全部创建完毕,而是用到哪个延迟等级创建哪个目录

修改消息索引单元内容。索引单元中的Message Tag HashCode部分原本存放的是消息的Tag的Hash值。现修改为消息的投递时间。投递时间是指该消息被重新修改为原Topic后再次被写入到commitlog中的时间。投递时间 = 消息存储时间 + 延时等级时间。消息存储时间指的是消息被发送到Broker时的时间戳。将消息索引写入到SCHEDULE_TOPIC_XXXX主题下相应的consumequeue中

3.3.2)投递延时消息

Broker内部有⼀个延迟消息服务类ScheuleMessageService,其会消费SCHEDULE_TOPIC_XXXX中的消息,即按照每条消息的投递时间,将延时消息投递到⽬标Topic中。不过,在投递之前会从commitlog中将原来写入的消息再次读出,并将其原来的延时等级设置为0,即原消息变为了一条不延迟的普通消息。然后再次将消息投递到目标Topic中。ScheuleMessageService在Broker启动时,会创建并启动一个定时器TImer,用于执行相应的定时任务。系统会根据延时等级的个数,定义相应数量的TimerTask,每个TimerTask负责一个延迟等级消息的消费与投递。每个TimerTask都会检测相应Queue队列的第一条消息是否到期。若第一条消息未到期,则后面的所有消息更不会到期(消息是按照投递时间排序的);若第一条消息到期了,则将该消息投递到目标Topic,即消费该消息。

3.3.3)将消息重新写入commitlog

延迟消息服务类ScheuleMessageService将延迟消息再次发送给了commitlog,并再次形成新的消息索引条目,分发到相应Queue。这其实就是一次普通消息发送。只不过这次的消息Producer是延迟消息服务类ScheuleMessageService。

SCHEDULE_TOPIC_XXXX目录中各个延时等级Queue中的消息是如何排序的?

是按照消息投递时间排序的。一个Broker中同一等级的所有延时消息会被写入到consumequeue目录中SCHEDULE_TOPIC_XXXX目录下相同Queue中。即一个Queue中消息投递时间的延迟等级时间是相同的。那么投递时间就取决于于消息存储时间了。即按照消息被发送到Broker的时间进行排序的。

4)事务消息

4.1)场景

image-20231019110202376

工行用户A向建行用户B转账1万元。若第3步中的扣款操作失败,但消息已经成功发送到了Broker。对于MQ来说,只要消息写入成功,那么这个消息就可以被消费。此时建行系统中用户B增加了1万元。出现了数据不一致问题。

4.2)解决思路

image-20231019110350717

解决思路是,让第1、2、3步具有原子性,要么全部成功,要么全部失败。即消息发送成功后,必须要保证扣款成功。如果扣款失败,则回滚发送成功的消息。而该思路即使用事务消息。这里要使用分布式事务解决方案。使用事务消息来处理该需求场景:

  1. 事务管理器TM向事务协调器TC发起指令,开启全局事务

  2. 工行系统发一个给B增款1万元的事务消息M给TC

  3. TC会向Broker发送半事务消息prepareHalf,将消息M预提交到Broker。此时的建行系统是看不到Broker中的消息M的

  4. Broker会将预提交执行结果Report给TC。

  5. 如果预提交失败,则TC会向TM上报预提交失败的响应,全局事务结束;如果预提交成功,TC会调用工行系统的回调操作,去完成工行用户A的预扣款1万元的操作

  6. 工行系统会向TC发送预扣款执行结果,即本地事务的执行状态

  7. TC收到预扣款执行结果后,会将结果上报给TM。

  8. 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

  1. TC在接收到指令后会向Broker与工行系统发出确认指令TC接收的若是Global Commit指令,则向Broker与工行系统发送Branch Commit指令。此时Broker中的消息M才可被建行系统看到;此时的工行用户A中的扣款操作才真正被确认TC接收到的若是Global Rollback指令,则向Broker与工行系统发送Branch Rollback指令。此时Broker中的消息M将被撤销;工行用户A中的扣款操作将被回滚

以上方案就是为了确保消息投递与扣款操作能够在一个事务中,要成功都成功,有一个失败,则全部回滚。

以上方案并不是一个典型的XA模式。因为XA模式中的分支事务是异步的,而事务消息方案中的消息预提交与预扣款操作间是同步的。

4.3)基础
4.3.1)分布式事务

一次操作由若干分支操作组成,这些分支操作分属不同应用,分布在不同服务器上。分布式事务需要保证这些分支操作要么全部成功,要么全部失败。分布式事务与普通事务一样,就是为了保证操作结果的一致性。

(简单理解:在不同项目(进程)上)

4.3.2)事务消息

RocketMQ提供了类似X/Open XA的分布式事务功能,通过事务消息能达到分布式事务的最终一致。XA是一种分布式事务解决方案,一种分布式事务处理模式。

4.3.3)半事务消息

暂不能投递的消息,发送方已经成功地将消息发送到了Broker,但是Broker未收到最终确认指令,此时该消息被标记成“暂不能投递”状态,即不能被消费者看到。处于该种状态下的消息即半事务消息。

(粗暴理解:提交审核,审核中,审核通过)

4.3.4)本地事务状态

Producer回调操作执行的结果为本地事务状态,其会发送给TC,而TC会再发送给TM。TM会根据TC发送来的本地事务状态来决定全局事务确认指令。

4.3.5)消息回查

image-20231019111200666

即重新查询本地事务的执行状态。本例就是重新到DB中查看预扣款操作是否执行成功。

注意,消息回查不是重新执行回调操作。回调操作是进行预扣款操作,而消息回查则是查看预扣款操作执行的结果。

引发消息回查的原因最常见的有两个:

1)回调操作返回UNKNWON

2)TC没有接收到TM的最终全局事务确认指令

4.4)RocketMQ中的消息回查设置

broker加载的配置文件中设置,例如:

6transactionTimeout=20,指定TM在20秒内应将最终确认状态发送给TC,否则引发消息回查。默认为60秒

transactionCheckMax=5,指定最多回查5次,超过后将丢弃消息并记录错误日志。默认15次。

transactionCheckInterval=10,指定设置的多次消息回查的时间间隔为10秒。默认为60秒。

4.5)XA协议

XA(Unix Transaction)是一种分布式事务解决方案,一种分布式事务处理模式,是基于XA协议的。XA协议由Tuxedo(Transaction for Unix has been Extended for Distributed Operation,分布式操作扩展之后的Unix事务系统)首先提出的,并交给X/Open组织,作为资源管理器与事务管理器的接口标准。

XA模式中有三个重要组件:TC、TM、RM。

4.5.1)TC(Broker充当TC角色)

Transaction Coordinator,事务协调者。维护全局和分支事务的状态,驱动全局事务提交或回滚。

4.5.2)TM(Producer充当TM角色)

Transaction Manager,事务管理器。定义全局事务的范围:开始全局事务、提交或回滚全局事务。它实际是全局事务的发起者。

4.5.3)RM(Broker,Producer充当RM角色)

Resource Manager,资源管理器。管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

4.6)模式架构

image-20231019111800655

  1. TM向TC发起指令,开启一个全局事务。

  2. 根据业务要求,各个RM会逐个向TC注册分支事务,然后TC会逐个向RM发出预执行指令。

  3. 各个RM在接收到指令后会在进行本地事务预执行。

  4. RM将预执行结果Report给TC。当然,这个结果可能是成功,也可能是失败。

  5. TC在接收到各个RM的Report后会将汇总结果上报给TM,根据汇总结果TM会向TC发出确认指令。若所有结果都是成功响应,则向TC发送Global Commit指令。只要有结果是失败响应,则向TC发送Global Rollback指令。

  6. TC在接收到指令后再次向RM发送确认指令。

事务消息方案并不是一个典型的XA模式。因为XA模式中的分支事务是异步的,而事务消息方案中的消息预提交与预扣款操作间是同步的。

4.7)注意

事务消息不支持延时消息

对于事务消息要做好幂等性检查,因为事务消息可能不止一次被消费(因为存在回滚后再提交的情况)

4.8)代码

4.8.1)定义事物消息生产者

TransactionMQProducer transactionMQProducer = new TransactionMQProducer("producer-group");
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(2000), new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setName(System.currentTimeMillis() + "-Thread");
        return thread;
    }
});
transactionMQProducer.setNamesrvAddr("0.0.0.0:9876");
transactionMQProducer.setTransactionListener(new ICBCTranscation());
transactionMQProducer.setExecutorService(threadPoolExecutor);
transactionMQProducer.start();
for (int i = 0; i < 100; i++) {
    String msg;
    if (i == 99){
        msg = "false";
    }else{
        msg = "true";
    }
    Message message = new Message("topic", "tag", msg.getBytes(StandardCharsets.UTF_8));
    transactionMQProducer.sendMessageInTransaction(message,null);
    log.info("发送消息,msg:{}",msg + i);
}
log.info("生产者消息发送结束");

4.8.2)定义消费者

DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("consumer-group");
defaultMQPushConsumer.setNamesrvAddr("0.0.0.0:9876");
defaultMQPushConsumer.subscribe("topic","tag");
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        list.forEach(item -> log.info("消费者消费消息:{}",new String(item.getBody())));
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});
defaultMQPushConsumer.start();
log.info("consumer starting");

4.8.3)定义事务监听器

public class ICBCTranscation implements TransactionListener {
    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        String msg = new String(message.getBody());
        log.info("开始执行事务,msg:{}",msg);
        if ("true".equals(msg)){
            log.info("成功");
            return LocalTransactionState.COMMIT_MESSAGE;
        }else{
            log.info("失败");
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        log.info("执行消息回查");
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

5)批量消息

5.1)批量发送消息
5.1.1)发送限制

生产者进行消息发送时可以一次发送多条消息,这可以大大提升Producer的发送效率。不过需要注意以下几点:

批量发送的消息必须具有相同的Topic

批量发送的消息必须具有相同的刷盘策略

批量发送的消息不能是延时消息与事务消息

5.1.2)批量发送大小

默认情况下,一批发送的消息总大小不能超过4MB字节。如果想超出该值,有两种解决方案:

方案一:将批量消息进行拆分,拆分为若干不大于4M的消息集合分多次批量发送

方案二:在Producer端与Broker端修改属性

** Producer端需要在发送之前设置Producer的maxMessageSize属性

** Broker端需要修改其加载的配置文件中的maxMessageSize属性

5.1.3)生产者发送的消息大小

image-20231019112520339

生产者通过send()方法发送的Message,并不是直接将Message序列化后发送到网络上的,而是通过这个Message生成了一个字符串发送出去的。这个字符串由四部分构成:Topic、消息Body、消息日志(占20字节),及用于描述消息的一堆属性key-value。这些属性中包含例如生产者地址、生产时间、要发送的QueueId等。最终写入到Broker中消息单元中的数据都是来自于这些属性。

5.2)批量消费消息
5.2.1)修改批量属性

Consumer的MessageListenerConcurrently监听接口的consumeMessage()方法的第一个参数为消息列表,但默认情况下每次只能消费一条消息。若要使其一次可以消费多条消息,则可以通过修改Consumer的consumeMessageBatchMaxSize属性来指定。不过,该值不能超过32。因为默认情况下消费者每次可以拉取的消息最多是32条。若要修改一次拉取的最大值,则可通过修改Consumer的pullBatchSize属性来指定。

5.2.2)问题

pullBatchSize值设置的越大,Consumer每拉取一次需要的时间就会越长,且在网络上传输出现问题的可能性就越高。若在拉取过程中若出现了问题,那么本批次所有消息都需要全部重新拉取。consumeMessageBatchMaxSize值设置的越大,Consumer的消息并发消费能力越低,且这批被消费的消息具有相同的消费结果。因为consumeMessageBatchMaxSize指定的一批消息只会使用一个线程进行处理,且在处理过程中只要有一个消息处理异常,则这批消息需要全部重新再次消费处理。

6)消息过滤

消息者在进行消息订阅时,除了可以指定要订阅消息的Topic外,还可以对指定Topic中的消息根据指定条件进行过滤,即可以订阅比Topic更加细粒度的消息类型。

6.1)Tag过滤

通过consumer的subscribe()方法指定要订阅消息的Tag。如果订阅多个Tag的消息,Tag间使用或运算符(双竖线||)连接。

consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");
6.2)SQL过滤

SQL过滤是一种通过特定表达式对事先埋入到消息中的用户属性进行筛选过滤的方式。通过SQL过滤,可以实现对消息的复杂过滤。不过,只有使用PUSH模式的消费者才能使用SQL过滤。SQL过滤表达式中支持多种常量类型与运算符。

msg.putUserProperty("age", i + "");
consumer.subscribe("myTopic", MessageSelector.bySql("age between0 and 6"));

支持的常量类型:

数值:比如:123,3.1415

字符:必须用单引号包裹起来,比如:‘abc’

布尔:TRUE 或 FALSE

NULL:特殊的常量,表示空

支持的运算符有:

数值比较:>,>=,<,<=,BETWEEN,=

字符比较:=,<>,IN

逻辑运算 :AND,OR,NOT

NULL判断:IS NULL 或者 IS NOT NULL

默认情况下Broker没有开启消息的SQL过滤功能,需要在Broker加载的配置文件修改。

7)消息发送重试机制

7.1)说明

Producer对发送失败的消息进行重新发送的机制,称为消息发送重试机制,也称为消息重投机制。

对于消息重投,需要注意以下几点:

生产者在发送消息时,若采用同步或异步发送方式,发送失败会重试,但oneway消息发送方式发送失败是没有重试机制的

只有普通消息具有发送重试机制,顺序消息是没有的

消息重投机制可以保证消息尽可能发送成功、不丢失,但可能会造成消息重复。消息重复在RocketMQ中是无法避免的问题

消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会成为大概率事件producer主动重发、consumer负载变化(发生Rebalance,不会导致消息重复,但可能出现重复消费)也会导致重复消息

消息重复无法避免,但要避免消息的重复消费。

避免消息重复消费的解决方案是,为消息添加唯一标识(例如消息key),使消费者对消息进行消费判断来避免重复消费

消息发送重试有三种策略可以选择:同步发送失败策略、异步发送失败策略、消息刷盘失败策略

7.2)同步发送失败策略

对于普通消息,消息发送默认采用round-robin策略来选择所发送到的队列。如果发送失败,默认重试2次。但在重试时是不会选择上次发送失败的Broker,而是选择其它Broker。当然,若只有一个Broker其也只能发送到该Broker,但其会尽量发送到该Broker上的其它Queue。同时,Broker还具有失败隔离功能,使Producer尽量选择未发生过发送失败的Broker作为目标Broker。其可以保证其它消息尽量不发送到问题Broker,为了提升消息发送效率,降低消息发送耗时。

如果超过重试次数,则抛出异常,由Producer去保证消息不丢。当然当生产者出现RemotingException、MQClientException和MQBrokerException时,Producer会自动重投消息。

思考:让我们自己实现失败隔离功能,如何来做?

1)方案一:Producer中维护某JUCMap集合,其key是发生失败的时间戳,valueBroker实例。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*。若该值相同,采用轮询方式选择。

7.3)异步发送失败策略

异步发送失败重试时,异步重试不会选择其他broker,仅在同一个broker上做重试,所以该策略无法保证消息不丢。

7.4)消息刷盘失败策略

消息刷盘超时(Master或Slave)或slave不可用(slave在做数据同步时向master返回状态不是SEND_OK)时,默认是不会将消息尝试发送到其他Broker的。不过,对于重要消息可以通过在Broker的配置文件设置retryAnotherBrokerWhenNotStoreOK属性为true来开启。

8)消息消费重试机制

8.1)顺序消息的消费重试

对于顺序消息,当Consumer消费消息失败后,为了保证消息的顺序性,其会自动不断地进行消息重试,直到消费成功。消费重试默认间隔时间为1000毫秒。重试期间应用会出现消息消费被阻塞的情况。
由于对顺序消息的重试是无休止的,不间断的,直至消费成功,所以,对于顺序消息的消费,务必要保证应用能够及时监控并处理消费失败的情况,避免消费被永久性阻塞。
注意,顺序消息没有发送失败重试机制,但具有消费失败重试机制。

8.2)无序消息的消费重试

对于无序消息(普通消息、延时消息、事务消息),当Consumer消费消息失败时,可以通过设置返回状态达到消息重试的效果。不过需要注意,无序消息的重试只对集群消费方式生效,广播消费方式不提供失败重试特性。即对于广播消费,消费失败后,失败消息不再重试,继续消费后续消息。

8.3)消费重试次数与间隔

对于无序消息集群消费下的重试消费,每条消息默认最多重试16次,但每次重试的间隔时间是不同的,会逐渐变长。每次重试的间隔时间如下表。

image-20231019113729822

若一条消息在一直消费失败的前提下,将会在正常消费后的第4小时46分后进行第16次重试。若仍然失败,则将消息投递到死信队列

对于修改过的重试次数,将按照以下策略执行:

若修改值小于16,则按照指定间隔进行重试

若修改值大于16,则超过16次的重试时间间隔均为2小时

对于Consumer Group,若仅修改了一个Consumer的消费重试次数,则会应用到该Group中所有其它Consumer实例。若出现多个Consumer均做了修改的情况,则采用覆盖方式生效。即最后被修改的值会覆盖前面设置的值。

8.4)重试队列

对于需要重试消费的消息,并不是Consumer在等待了指定时长后再次去拉取原来的消息进行消费,而是将这些需要重试消费的消息放入到了一个特殊Topic的队列中,而后进行再次消费的。这个特殊的队列就是重试队列。

当出现需要进行重试消费的消息时,Broker会为每个消费组都设置一个Topic名称为%RETRY%consumerGroup@consumerGroup 的重试队列。

1)这个重试队列是针对消息才组的,而不是针对每个Topic设置的(一个Topic的消息可以让多个消费者组进行消费,所以会为这些消费者组各创建一个重试队列)

2)只有当出现需要进行重试消费的消息时,才会为该消费者组创建重试队列

注意,消费重试的时间间隔与延时消费的延时等级十分相似,除了没有延时等级的前两个时间外,其它的时间都是相同的

Broker对于重试消息的处理是通过延时消息实现的。先将消息保存到SCHEDULE_TOPIC_XXXX延迟队列中,延迟时间到后,会将消息投递到%RETRY%consumerGroup@consumerGroup重试队列中。

8.5)消费重试配置方式

集群消费方式下,消息消费失败后若希望消费重试,则需要在消息监听器接口的实现中明确进行如下三种方式之一的配置:

方式1:返回ConsumeConcurrentlyStatus.RECONSUME_LATER(推荐)

方式2:返回Null

方式3:抛出异常

8.6)消费不重试配置方式

集群消费方式下,消息消费失败后若不希望消费重试,则在捕获到异常后同样也返回与消费成功后的相

同的结果,即ConsumeConcurrentlyStatus.CONSUME_SUCCESS,则不进行消费重试。

9)死信队列

9.1)概念

当一条消息初次消费失败,消息队列会自动进行消费重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。这个队列就是死信队列(Dead-Letter Queue,DLQ),而其中的消息则称为死信消息(Dead-Letter Message,DLM)。死信队列是用于处理无法被正常消费的消息的。

9.2)特征

死信队列中的消息不会再被消费者正常消费,即DLQ对于消费者是不可见的死信存储有效期与正常消息相同,均为 3 天(commitlog文件的过期时间),3 天后会被自动删除死信队列就是一个特殊的Topic,名称为%DLQ%consumerGroup@consumerGroup ,即每个消费者组都有一个死信队列如果⼀个消费者组未产生死信消息,则不会为其创建相应的死信队列

9.3)处理

实际上,当⼀条消息进入死信队列,就意味着系统中某些地方出现了问题,从而导致消费者无法正常消费该消息,比如代码中原本就存在Bug。因此,对于死信消息,通常需要开发人员进行特殊处理。最关键的步骤是要排查可疑因素,解决代码中可能存在的Bug,然后再将原来的死信消息再次进行投递消费。
于需要重试消费的消息,并不是Consumer在等待了指定时长后再次去拉取原来的消息进行消费,而是将这些需要重试消费的消息放入到了一个特殊Topic的队列中,而后进行再次消费的。这个特殊的队列就是重试队列。

当出现需要进行重试消费的消息时,Broker会为每个消费组都设置一个Topic名称为%RETRY%consumerGroup@consumerGroup 的重试队列。

1)这个重试队列是针对消息才组的,而不是针对每个Topic设置的(一个Topic的消息可以让多个消费者组进行消费,所以会为这些消费者组各创建一个重试队列)

2)只有当出现需要进行重试消费的消息时,才会为该消费者组创建重试队列

注意,消费重试的时间间隔与延时消费的延时等级十分相似,除了没有延时等级的前两个时间外,其它的时间都是相同的

Broker对于重试消息的处理是通过延时消息实现的。先将消息保存到SCHEDULE_TOPIC_XXXX延迟队列中,延迟时间到后,会将消息投递到%RETRY%consumerGroup@consumerGroup重试队列中。

8.5)消费重试配置方式

集群消费方式下,消息消费失败后若希望消费重试,则需要在消息监听器接口的实现中明确进行如下三种方式之一的配置:

方式1:返回ConsumeConcurrentlyStatus.RECONSUME_LATER(推荐)

方式2:返回Null

方式3:抛出异常

8.6)消费不重试配置方式

集群消费方式下,消息消费失败后若不希望消费重试,则在捕获到异常后同样也返回与消费成功后的相

同的结果,即ConsumeConcurrentlyStatus.CONSUME_SUCCESS,则不进行消费重试。

9)死信队列

9.1)概念

当一条消息初次消费失败,消息队列会自动进行消费重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。这个队列就是死信队列(Dead-Letter Queue,DLQ),而其中的消息则称为死信消息(Dead-Letter Message,DLM)。死信队列是用于处理无法被正常消费的消息的。

9.2)特征

死信队列中的消息不会再被消费者正常消费,即DLQ对于消费者是不可见的死信存储有效期与正常消息相同,均为 3 天(commitlog文件的过期时间),3 天后会被自动删除死信队列就是一个特殊的Topic,名称为%DLQ%consumerGroup@consumerGroup ,即每个消费者组都有一个死信队列如果⼀个消费者组未产生死信消息,则不会为其创建相应的死信队列

9.3)处理

实际上,当⼀条消息进入死信队列,就意味着系统中某些地方出现了问题,从而导致消费者无法正常消费该消息,比如代码中原本就存在Bug。因此,对于死信消息,通常需要开发人员进行特殊处理。最关键的步骤是要排查可疑因素,解决代码中可能存在的Bug,然后再将原来的死信消息再次进行投递消费。

  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值