初始RocketMQ

164 阅读27分钟

RocketMQ是阿里巴巴采用Java语言开发的开源分布式消息中间件,现在是Apache的一个顶级项目。在阿里内部,RocketMQ承接了例如“双11”等高并发场景的消息流转,能够处理万亿级别的消息。

跟其它中间件相比,RocketMQ的特点是:

  • 纯JAVA实现
  • 集群和HA (高可用性) 实现相对简单
  • 在发生岩机和其它故障时消息丢失率更低

基本概念

消息 (Message):消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。

RocketMQ中的每个消息拥有唯一的MessagelD,且可携带由用户指定的业务相关的唯一标识Key,系统提供通过MessagelD和Key查询消息的功能

主题 (Topic) :表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。

topic:message=1:n

一个生产者可以同时发送多种Topic的消息;

而一个消费者只对某种特定的Topic感兴趣,即只可以订阅和消费一种Topic的消息。producer:topic=1:n

consumer:topic=1:1

标签(Tag):为消息设置的标签,用于同一主题下区分不同类型的消息。

Topic是消息的一级分类,Tag是消息的二级分类。

消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。

队列(Queue):在RocketMQ中存在两种队列:

消息队列 (Message Queue) : 是真正存放消息单元的物理实体,即系统中的CommitLogFiles

采用混合型存储结构,不分Topic按消息接收顺序依次将消息写入日志文件

消费队列 (Consume Queue) : 是存放指向消息队列中消息单元的索引的物理实体(逻辑实体),可据此来查找待消费的消息。

按Topic分类将消息单元索引写入队列文件

一个Consume Queue也被称为一个分区 (Partition)

一个分区中的消息只能被一个消费者组中的一个消费者消费。

存放相应Topic的Broker也被称为分片(Sharding)

每个分片中会创建出相应数量的分区,即ConsumeQueues,每个Consume Queue的大小都是相同的。

RocketMQ系统架构

Producer

消息生产者,负责生产和发送消息

RocketMQ提供多种消息发送方式,如同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需Broker返回确认信息,单向发送则不需要。

消息生产者都是以生产者组 (ProducerGroup) 的形式出现.

生产者组是同一类生产者的集合,这类Producer发送相同Topic类型的消息。

一个生产者组可以同时发送多个主题的消息

Consumer

消息消费者,负责消费消息

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

消息消费者都是以消费者组 (Consumer Group)的形式出现的。

消费者组是同一类消费者的集合,这类Consumer消费的是同一个且只能一个Topic类型的消息,但一个Topic类型的消息可以被多个消费者组同时消费。

消费者组使得在消息消费方面,实现负载均衡和容错的目标变得容易

负载均衡:将一个Topic中的不同的Queue平均分配给同一个ConsumerGroup的不同的Consumer (注意:并不是将消息负载均衡)

容错:一个Consmer挂了,该Consumer Group中的其它Consumer可以接着消费原Consumer消费的Queue

超出Queue数量的Consumer将不能消费消息;且在集群消费模式下,每条同Topic消息只会被Consumer Group中的某个Consumer消费

NameServer

NameServer是一个Broker和Topic路由注册中心,支持Broker的动态注册与发现。NameServer内部维护着一个Topic路由表和个Broker列表;并提供心跳检测机制,检查Broker是否还存活。

NameServer通常也是以集群的方式部署,但各节点间相互不进行信息通讯。

在Broker节点启动时,轮询NameServer列表,与每个NameServer节点建立长连接,定时注册Topic路由信息到所有NameServer。

Topic路由表:

Broker列表:

具有相同BrokerName的Broker构成一个Broker主备集群;

不同Broker主备集群又可构成一个大Broker集群

路由剔除

Broker节点为了证明自己是活着的,会每30秒可NameServer发送一次心跳包。

心跳包中包含Brokerld、Broker地址(IP+Port)Broker名称、Broker所属集群名称、存储的所有Topic信息等等。

NameServer在接收到心跳包后,会更新心跳时间戳记录这个Broker的最新存活时间。

NameServer中有一个定时任务,每隔10秒就会扫描一次Broker列表,查看每-个Broker的最新心跳时间戳距离当前时间是否超过120秒,如果超过,则会判定Broker失效,然后将其从Broker列表中剔除。

路由发现

RocketMQ的路由发现采用的是Pull模型

当Topic路由信息出现变化时,NameServer不会主动推送(Push模型)给客户端,而是客户端定时取 (Pull模型)主题最新的路由。

默认客户端每30秒会拉取一次最新的路由。

三种模型比较

Push模型:实时性较好,需要维护一个长连接

Pull模型:不需要维护一个长连接,但实时性较差

Long Polling模型:长轮询模型。

是对Push与Pull模型的整合,充分利用了这两种模型的优势,屏蔽了它们的劣势。

客户端选择NameServer策略

首先采用随机策略,失败后采用轮询策略。

客户端首先会生产一个随机数,然后再与NameServer集群中的节点数量取模,结果即为所要连接的节点索引,然后进行连接。

如果连接失败,则会采用round-robin策略,逐个尝试着去连接其它节点

Broker

Broker充当着消息中转角色,负责消息的存储和转发,同时负责消息的查询和服务高可用性保证。

接收并存储从生产者发送来的消息,同时为消费者的拉取请求作准备。

Broker同时也存储着消息相关的元数据,包括消费者组消费进度偏移offset、主题、队列等

Remoting Module是整个Broker的实体,负责处理来自clients端的请求。

Broker实体由以下模块构成

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

Store Service: 存储服务。提供方便简单API接口,处理消息存储到物理硬盘和消息查询功能HA Service:高可用服务。提供Master Broker 和 Slave Broker之间的数据同步功能

Index Service: 索引服务。根据特定的Message key,对投递到Broker的消息进行索引服务,同时也提供根据Message Key对消息进行快速查询的功能。

Broker中数据的复制与刷盘策略

复制策略是Broker的Master与Slave间的数据同步方式,分为:

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

刷盘策略

刷盘策略指的是broker中消息的落盘方式即消息发送到broker内存 (一般是PageCache) 后消息持久化到磁盘的方式分为:

同步刷盘:当消息持久化到broker的磁盘后才返回成功ACK。

异步刷盘:当消息写入到broker的内存后立即返回成功ACK,无需等待消息持久化到磁盘。当写入PageCache到达一定量时会自动进行落盘。

RocketMQ工作流程

  1. 启动NameServer, NameServer启动后开始监听端口,等待Broker、Producer、Consumer连接。
  2. 启动Broker时,Broker会与所有NameServer建立并保持长连接,然后每30秒向NameServer定时发送心跳包。

3)发送消息前,先通过RocketMQ控制台创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。

  1. Producer发送消息,启动时先跟NameServer集群中的一台建立长连接,并从NameServer中获取到Topic路由信息 (该Topic路由表及Broker列表) ,然后以某种Queue选择算法从队列列表中选择一个Queue,并与Queue所在的Broker (Master) 建立长连接,且定时向Master发送心跳,从而向Broker发消息。

Producer在获取到路由信息后,会首先将路由信息缓存到本地,再每30秒从NameServer更新一次路由信息。

  1. Consumer跟Producer类似,与其中一台NameServer建立长连接后,从NameServer中获取到Topic路由信息,然后按某种Queue分配策略从路由信息中获取到其所要消费的Queue并与Queue所在的Broker (Master和Slave) 建立长连接,且定时向Master和Slave发送心跳从而开始消费消息。

Consumer在获取到路由信息后,同样也会将路由信息缓存到本地,再每30秒从NameServer更新一次路由信息。

RocketMQ相关原理

Topic的创建

自动创建Topic时,系统会为每个Broker默认创建4个Queue (writeQueueNums=4, eadQueueNums=4 , perm=6)

  • 物理上只会创建4个队列。
  • 读写队列数量设置机制,其设计目的是为了方便Topic的Queue的缩容,以致不会造成任何消息的丢失。
  • 其中perm用于设置对当前创建Topic的操作权限2表示只写,4表示只读,6表示读写

Queue的选择

Queue选择算法,也称为消息投递算法,常见的有两种:

  • 轮询 (round-robin) 算法
  • 最小投递延迟算法
轮询算法(默认选择算法)

该算法可保证每个Queue中均匀获取到消息。

该算法存在一个问题: 由于某些原因,在某些Broker上的Queue可能投递延迟较严重,从而导致Producer的缓存队列中出现较大的消息积压,影响消息的投递性能。

最小投递延迟算法

该算法会统计每次消息投递的时间延迟,然后根据统计出的结果将消息投递到时间延迟

最小的Queue。

如果延迟相同,则采用轮询算法投递。

该算法可以有效提升消息的投递性能

该算法也存在一个问题: 消息在Queue上的分配不均匀。

投递延迟小的Queue其可能会存在大量的消息,而对该Queue的消费者压力会增大,降低消息的消费能力,可能会导致MQ中消息的堆积。

Queue的分配

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

Queue的分配常见有四种策略,这些策略会通过创建Consumer时的构造方法传进去。

  • 平均分配策略
  • 环形平均策略
  • 一致性hash策略
  • 同机房策略
平均分配策略

该算法根据avg = QueueCount !ConsumerCount的计算结果进行分配。

如果能够整除,则按顺序将avg个Queue逐个分配Consumer;

如果不能整除,则将多余出的Queue按照Consumer顺序逐个分配。

该算法即先计算好每个Consumer应该分得几个然后再依次将这些数量的Queue逐个Queue,分配各Consumer。

该算法存在的问题:如果消费者组扩容或缩容会带来大量Consumer的Rebalance (再均衡)

环形平均策略

环形平均算法是根据消费者的顺序,依次在由Queue队列组成的环形图中逐个分配。

该算法不用事先计算每个Consumer需要分配几个Queue,直接一个一个分即可。

该算法存在的问题:如果消费者组扩容或缩容也会带来大量的Rebalance (重分配)

一致性hash策略

该算法会将consumer的hash值作为Node节点存放到hash环上,然后将queue的hash值也放到hash环上,通过顺时针方向,距离queue最近的那个consumer就是该queue要分配的consumer。

该算法存在的问题:分配不均。

同机房策略

该算法会根据queue的部署机房位置和consumer的位置,过滤出当前consumer相司机房的queue。

然后按照平均分配策略或环形平均策略对同机房queue进行分配。

如果没有同机房queue,则按照平均分配策略或环形平均策略对所有queue进行分配

Queue的再均衡(Rebalance)

Rebalance即再均衡,指将一个Topic下的多个Queue在同一个Consumer Group中的多个Consumer间进行重新分配的过程。

Rebalance产生的原因:消费者所订阅Topic的Queue数量发生变化;

或消费者组中消费者的数量发生变化。

Rebalance过程:

Broker中有3种Manager的Map,据此,Broker一旦发现有引起Rebalance因素,就会立即向Consumer Group中的每个实例发出Rebalance通知。

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

3种Manager中的Map

1.TopicConfigManager

key是topic名称,value是TopicConfig

TopicConfig中维护着该Topic中所有Queue的数据

2.ConsumerManager

key是Consumser Groupld,value是ConsumerGrouplnfo.

ConsumerGrouplnfo中维护着该Group中所有Consumer实例数据。3.ConsumerOffsetManager

key为Topic与订阅该Topic的Group的组合,即topic@group,value是一个内层Map。

内层Map的key为Queueld,内层Map的value为该Queue的消费进度offset。

Rebalance的危害

Rebalance在提升消费能力的同时,也带来一些问题

  • 消费暂停

当发生Rebalance时,原Consumer需要暂停部分队列的消费,等到这些队列分配给新的Consumer后,这些暂停消费的队列才能继续被消费。

  • 消费重复

Consumer在消费新分配给自己的队列时必须接着之前Consumer提交的消费进度的offset继续消费

然而默认情况下,消费进度的offset是异步提交的,这个异步性导致提交到Broker的offset与Consumer实际消费的消息的offset并不一致,这个不一致就可能导致重复消费消息。

  • 消费突刺

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

消息的存储

RocketMQ中的消息存储在Broker本地文件系统中,这些相关文件默认在BrokerOS当前用户主目录下的store目录里。

store目录下的目录和文件

abort:该文件在Broker启动后会自动创建,正常关闭Broker,该文件会自动消失。

· 若在没有启动Broker的情况下,发现这个文件是存在的,则说明之前Broker的关闭是非正常关闭。

checkpoint:其中存储着commitlog、consumequeue,index文件的最后刷盘时间戳commitlog: 其中存放着很多mappedFile文件,而消息就写在mappedFile文件中

config: 存放着Broker运行期间的一些配置数据

consumequeue: 其中存放着consumequeue文件,队列就存放在这个目录中

index: 其中存放着消息索引文件indexFile

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

消息的消费

获取消费类型

拉取式 (Pull) 消费

Consumer主动从Broker中拉取消息到本地缓冲队列中,主动权由Consumer控制。

需要应用去实现对关联Queue的遍历,实时性差,即Broker中有了新的消息时消费者并不能及时发现并消费。

推送式 (Push) 消费

该模式下Broker收到数据后会主动推送给Consumer。

是典型的发布-订阅模式,即Consumer向其关联的Queue注册了监听器,一旦发现有新的消息到来就会触发回调的执行,回调方法是Consumer去Queue中拉取消息。

封装了对关联Queue的遍历,实时性强

消费模式

广播消费

该模式下,相同Consumer Group的每个Consumer实例都接收同一个Topic的全量消息。即每条消息都会被发送到Consumer Group中的每个Consumer.

集群消费

该模式下,相同Consumer Group的所有Consumer共同消费同一个Topic中的消息,且同一条消息只会被消费一次。

即每条消息只会被发送到Consumer Group中的某个0Consumer.

订阅和消费原则

订阅关系一致性原则

订阅关系的一致性指的是,同一个消费者组 (GroupID相同)下所有Consumer实例所订阅的Topic与Tag的类型和数量及对消息的处理逻辑必须完全一致

至少一次原则

即每条消息必须要被成功消费一次

所谓成功消费是指Consumer在消费完消息后会向其消费进度记录器提交其消费消息的offset,offset被成功记录到记录器中

  • 对于广播消费模式,Consumer本身就是消费进度记录器
  • 对于集群消费模式,Broker是消费进度记录器
消费进度管理

消费进度管理通过消费进度offset来记录每个Queue的不同消费组的消费进度。

而消费者要消费的第一条消息的起始位置是用户自己通过consumer.setConsumeFromWhere()方法指定可制定的起始位置有:

  • CONSUME FROM LAST OFFSET: 从queue的当前最后一条消息开始消费
  • CONSUME FROM FIRST_OFFSET: 从queue的第一条消息开始消费
  • CONSUME FROM TIMESTAMP:从指定的具体时间戳位置的消息开始消费。

这个具体时间戳通过另外一个语句指定:

consumer.setConsumeTimestamp("20210701080000")【yyyyMMddHHmmss】

消费进度管理模式:

根据消费进度记录器的不同,可以分为两种管理模式:本地管理模式远程管理模式

本地管理模式:

当消费模式为广播消费时,每个消费者各自管理各自的消费进度,消费进度offset相关数据以json的形式持久化到Consumer本地磁盘文件中。

默认文件路径为当前用户主目录下的

rocketmq_offsets/{clientld}/{group}/Offsets.json ,其中

${clientld}为当前消费者id,默认为ip@DEFAULT

${group}为消费者组名称

远程管理模式:

当消费模式为集群消费时,所有Consumer共享Queue的消费进度,消费进度offset相关数据以json的形式持久化到Broker磁盘文件中。

文件路径为当前用户主目录下的store/config/consumerOffset.json 。

Broker启动时会加载这个文件,并写入到ConsumerOffsetManager的双层Map里

消费进度offset的提交

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

同步提交:等待返回成功ACK后才读取下一批消息进行消费

异步提交:不需等待返回成功ACK就读取下一批消息进行消费

  • Consumer可从Broker中直接获取nextBeginOffset

Broker在收到消费进度后会将其更新到ConsumerOffsetManager的双层Map及consumerOffset.ison文件中,然后向该Consumer进行ACK,而ACK内容中包含三项数据:

当前消费队列的最小offset (minOffset)

最大offset(maxOffset)

下次消费的起始(offsetnextBeginOffset)

消费幂等

消费幂等是指当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响。引起重复消费最常见的三种情况:

发送时消息重复

消费时消息重复

Rebalance时重复消费

发送时消息重复

当一条消息已被成功发送到Broker并完成持久化,此时出现网络闪断,Producer得不到应答就会尝试再次发送消息,Broker中就可能出现两条内容相同并且MessagelD也相同的消息。

消费时消息重复

当消息已投递到Consumer并完成业务处理此时出现网络闪断, Broker得不到应答就会尝试再次发送消息 (至少一次性原则)Consumer就会收到与之前处理过的内容相同、MessagelD也相同的消息。

Rebalance时重复消费

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

实现幂等通用方案:

  • 首先在缓存中查询幂等令牌是否存在。若存在则说明本次操作是重复性操作;若不存在,则进入下一步;
  • 唯一性处理前,在DB中查询幂等令牌作为索引的数据是否存在。若存在,则说明本次操作为重复性操作;若不存在,则进入下一步。
  • 唯一性处理后,将幂等令牌写入到缓存,并将幂等令牌作为唯一索引的数据写入到DB中
消费延迟

消息处理流程中,如果Consumer的消费速度跟不上Producer的发送速度,Consumer本地缓冲队列达到上限,就会停止从服务端拉取消息,MQ中未处理的消息就会越来越多(进多出少) ,从而出现消息堆积,进而会造成消息的消费延迟。

如何避免消费延迟

引起消息堆积的主要原因在于客户端的消费能力,而消费能力由消费耗时和消费并发度决定。

所以,要避免消费延迟主要从以下两方面进行完善:

  • 梳理消息的消费耗时
  • 设置消费并发度

梳理消息的消费耗时

梳理消息的消费耗时需要关注以下信息:

消息消费逻辑的计算复杂度是否过高,代码是否存在无限循环和递归等缺陷。

消息消费逻辑中的I/O操作是否是必须的,能否用本地缓存等方案规避

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

设置消费并发度

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

  • 逐步调大单个Consumer节点的线程数,并观测节点的系统指标,得到单个节点最优的消费线程数和消息吞吐量。
  • 理想环境下单节点的最优线程数计算模型为: C*(T1+ T2)/T1

其中C: CPU内核数; T1: CPU内部逻辑计算耗时;T2:外部IO操作耗时

  • 根据上下游链路的流量峰值计算出需要设置的节点数
  • 节点数 = 流量峰值 /单个节点消息吞吐量

RocketMQ应用模式

消息范型

普通消息

Producer对于消息的发送方式有多种选择,不同的方式会产生不同的系统效果。

同步发送消息: 指Producer发出一条消息后,会在收到MQ返回的ACK之后才发下一条消息。

该方式的消息可靠性最高,但消息发送效率太低

**异步发送消息:**指Producer发出消息后无需等待MQ返回ACK,置接发送下一条消息

该方式的消息可靠性可以得到保障,消息发送效率也可以

单向发送消息: 指Producer仅负责发送消息,不等待、不处理MQ的ACK。该发送方式时MQ也不返回ACK。

该方式的消息发送效率最高,但消息可靠性较差

顺序消息

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

根据有序范围的不同,有序性可分:

**分区有序:**有多个Queue参与,仅可保证在该Queue分区队列上的消息有序

全局有序: 只有一个Queue参与,保证整个Topic中的消息有序

延时消息

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

再投时间 = 消息存储时间 + 延时等级时间

延时等级,可以通过在broker加载的配置文件broker.conf (在RocketMQ安装目录下的conf目录下) 中新增如下配置(例如下面增加了1天这个等级1d) :

messageDelayLevel = 1s 5s 10s 30s 1m 2m 3m 4m 5n6m 7m 8m 9m 10m 20m 30m 1h 2h 1d

典型用例:超时费支付取消订单

事务消息

问题引入: 如何保证分布式系统中的数据致性?

解决思路: 让第1、2、3步具有原子性。

批量消息

生产者进行消息发送时可以一次发送多条消息,这可以大大提升Producer的发送效率。

不过需要注意以下几点:

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

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

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

消息过滤

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

对于指定Topic消息的过滤有两种过滤方式

1) Tag过滤

如果订阅多个Tag的消息,Tag间使用或运算符(双竖线||)连接

2)SQL过滤

春需慧得葛务奏达式对事先埋入到消息中的用户属性进

只有使用PUSH模式的消费者才能使用SQL过滤

消息重试

消息发送重试

消息发送重试可以保证消息尽可能发送成功不丢失,但可能会造成消息重复。

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

只有普消息 (同步或异步) 具有发送重试机制,单向消息、顺序消息是没有的。

消息发送重试有三种策略:

同步发送失败策略

如果发送失败,默认重试2次。

但在重试时会尽量发送到未发生过发送失败的Broker或其它Queue。

如果超过重试次数,则抛出异常,由Producer去保证消息不丢失

但当Producer出现RemotingException、MQClientException 和MQBrokerException时,Producer会自动重投消息

异步发送失败策略

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

消息刷盘失败策略

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

retryAnotherBrokerWhenNotStoreOK=true

消息消费重试
顺序消息的消费重试

对于顺序消息,当Consumer消费消息失败后,为了保证消息的顺序性,其会自动不断地进行消息重试,直到消费成功。

消费重试默认间隔时间为1000毫秒

重试期间应用会出现消息消费被阻塞的情况。

无序消息的消费重试

对于无序消息 (普通消息、延时消息、事务消息),在集群消费方式下,当Consumer消费消息失败时,每条消息默认最多重试16次,但每次重试的时间间隔不同,会逐渐变长

  • 若第16次重试仍然失败,则将消息索引到死信队列。
  • 广播消费方式不提供失败重试特性

消费重试实现原理

Broker对于重试消息的处理是通过延时消息实现的。

当且只有当一个消费者组出现需要进行重试消费的消息时,Broker会为该消费者组设置一个Topic名称为

%RETRY%consumerGroup@consumerGroup的重试队列。

然后,Broker先将消息索引到

SCHEDULE_TOPIC_XXXX延迟队列,

延迟时间到后,会将消息索引到消费者组的重试队列中,而后由消费者进行再次消费。

死信消息处理

当一条消息被消费时达到最大重试次数后,若依然失败,则该消息被称为死信消息 (Dead-Letter Message,DLM)

当且只有当一个消费者组出现死信消息时,Broker会为该消费者组设置一个Topic名称为

%DLQ%consumerGroup@consumerGroup的死信队列

死信队列中的消息不会再被消费者正常消费,3天后会被自动删除或需由人工处理

RocketMQ与springboot的整合

相关注解

@RocketMQTransactionListener(rocketMQTemplateBeanName) :声明一个本地事务监听器

rocketMQTemplateBeanName为消息生产者发送消息的"RocketMQTemplate"Bean实例名

@RocketMQMessageListener(nameServer, topic,consumerGroup,messageModel) :

声明一个消息消费者

  • nameServer为RocketMQ NameServer;
  • topic为消费的消息主题;
  • consumerGroup为消费者所在组名;
  • messageModel为消息模式,默认为CLUSTERING
  • consumeMode为消费模式,默认为CONCURRENTLY

@Slf4j: 取代代码【private static final Logger logger = LoggerFactory.getLogger(this.XXX.class);】

RocketMQ依赖启动器

org.apache.rocketmq rocketmq-spring-boot-starter 2.1.1

producer

#RocketMQ NameServer地址,多个nameserver地址采用“;”隔开
rocketmq.name-server=127.0.0.1:9876
#rocketmq.name-server=127.0.0.1:9876;192.168.31.173:9876
#消息生产者组名
rocketmq.producer.group=producer-group

RocketMQTemplate 实例对象

发送普通消息(convertAndSend)
//发送一条普通消息
//普通消息无返回值,只负责发送消息⽽不等待服务器回应且没有回调函数触发。
rocketMQTemplate.convertAndSend(String, object);
发送同步消息(syncSend)
SendResult sendResult = rocketMQTemplate.syncSend(String, object);
log.info("【同步发送结果】{}", sendResult);
发送异步消息(asyncSend)
rocketMQTemplate.asyncSend(this.destination, this.payload, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        log.info("【异步发送结果】{}", sendResult);
    }
    @Override
    public void onException(Throwable throwable) {
        log.error("【异步发送异常】{}", throwable.getMessage());
    }
});
发送顺序消息(syncSendOrderly)
for (int i = 0; i < 10; i++) {
    //hashkey用于选择消息队列,只有在相同队列的消息能保持顺序
    rocketMQTemplate.syncSendOrderly(this.destination, new Order("O0" + i, "U01", "P01", 2, false, new Date()), "hashKey");
}

发送批量消息(syncSend)

List<Message> messages = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    this.payload = new Order("O0" + i, "U01", "P01", 2, false, new Date());
    Message<Object> message = MessageBuilder.withPayload(this.payload).build();
    messages.add(message);
}
SendResult sendResult = rocketMQTemplate.syncSend(this.destination, messages, 1000);
log.info("【批量发送结果】{}", sendResult);

发送延迟消息(syncSend(String,Message,int(timeout),int(delayLevel)))

Message<Object> message = MessageBuilder.withPayload(this.payload).build();
//设置延迟时间为10s(1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h)
SendResult sendResult = rocketMQTemplate.syncSend(this.destination, message, 100000, 3);
log.info("【延迟发送结果】{}", sendResult);

发送事务消息(sendMessageInTransaction)

Message<Object> message = MessageBuilder.withPayload(this.payload).build();
log.info("【发送半消息】{}", message.getPayload());
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(this.destination, message, this.payload);
log.info("【本地事务状态】{}", result.getLocalTransactionState());

声明本地监听器

@RocketMQTransactionListener(rocketMQTemplateBeanName = "rocketMQTemplate")
    class ProducerLocalTransactionListener implements RocketMQLocalTransactionListener {
        private ConcurrentHashMap<String, Object> localTrans = new ConcurrentHashMap<>();
        //半消息投递成功后执行的逻辑
        @Override
        public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
            try {
                log.info("【收到半消息响应ACK,执行本地事务:】");
                log.info("Message:{}", message);
                log.info("Object:{}", o);
                localTrans.put(message.getHeaders().getId() + "", message.getPayload());
                return RocketMQLocalTransactionState.UNKNOWN;
                //return RocketMQLocalTransactionState.COMMIT;
            } catch (Exception e) {
                e.printStackTrace();
                log.error("【执行本地事务异常】 Exception:{}", e.getMessage());
                return RocketMQLocalTransactionState.ROLLBACK;
            }
        }
        //回查本地事务执行状态
        @Override
        public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
            log.info("【检查本地事务状态】");
            return RocketMQLocalTransactionState.COMMIT;
        }
    }

实例

业务类ProduceService实现单向消息、同步消息、异步消息、顺序消息、批量消息、延迟消息、事务消息的生产和发送。

package com.scst.service;

import com.scst.domain.Order;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
    @Service
    //消息生产服务
    public class ProduceService {

        @Autowired
        private RocketMQTemplate rocketMQTemplate;

        private String destination;
        private Object payload;

        //1.发送单向消息
        public void sendOneWayMessage(String topic, String tag) {
            this.destination = topic;
            if (tag != null) {
                this.destination = topic + ":" + tag;
            }
            this.payload = new Order("O01", "U01", "P01", 2, false, new Date());
            rocketMQTemplate.convertAndSend(this.destination, this.payload);
        }

        //2.发送同步消息
        public void sendSyncMessage(String topic, String tag) {
            this.destination = topic;
            if (tag != null) {
                this.destination = topic + ":" + tag;
            }
            this.payload = new Order("O01", "U01", "P01", 2, false, new Date());
            SendResult sendResult = rocketMQTemplate.syncSend(this.destination, this.payload);
            log.info("【同步发送结果】{}", sendResult);
        }

        //3.发送异步消息
        public void sendAsyncMessage(String topic, String tag) {
            this.destination = topic;
            if (tag != null) {
                this.destination = topic + ":" + tag;
            }
            this.payload = new Order("O01", "U01", "P01", 2, false, new Date());
            rocketMQTemplate.asyncSend(this.destination, this.payload, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    log.info("【异步发送结果】{}", sendResult);
                }

                @Override
                public void onException(Throwable throwable) {
                    log.error("【异步发送异常】{}", throwable.getMessage());
                }
            });
        }

        //4.发送顺序消息
        public void sendOrderedMessages(String topic, String tag) {
            this.destination = topic;
            if (tag != null) {
                this.destination = topic + ":" + tag;
            }
            for (int i = 0; i < 10; i++) {
                //hashkey用于选择消息队列,只有在相同队列的消息能保持顺序
                rocketMQTemplate.syncSendOrderly(this.destination, new Order("O0" + i, "U01", "P01", 2, false, new Date()), "hashKey");
            }
        }

        //5.发送批量消息
        public void sendBatchMessages(String topic, String tag) {
            this.destination = topic;
            if (tag != null) {
                this.destination = topic + ":" + tag;
            }
            List<Message> messages = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
                this.payload = new Order("O0" + i, "U01", "P01", 2, false, new Date());
                Message<Object> message = MessageBuilder.withPayload(this.payload).build();
                messages.add(message);
            }
            SendResult sendResult = rocketMQTemplate.syncSend(this.destination, messages, 1000);
            log.info("【批量发送结果】{}", sendResult);
        }

        //6.发送延迟消息
        public void sendDelayMessage(String topic, String tag) {
            this.destination = topic;
            if (tag != null) {
                this.destination = topic + ":" + tag;
            }
            this.payload = new Order("O01", "U01", "P01", 2, false, new Date());
            Message<Object> message = MessageBuilder.withPayload(this.payload).build();
            //设置延迟时间为10s(1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h)
            SendResult sendResult = rocketMQTemplate.syncSend(this.destination, message, 100000, 3);
            log.info("【延迟发送结果】{}", sendResult);
        }

        //7.发送事务消息
        public void sendTransactionMessage(String topic, String tag) {
            this.destination = topic;
            if (tag != null) {
                this.destination = topic + ":" + tag;
            }
            this.payload = new Order("O01", "U01", "P01", 2, false, new Date());
            Message<Object> message = MessageBuilder.withPayload(this.payload).build();
            log.info("【发送半消息】{}", message.getPayload());
            TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(this.destination, message, this.payload);
            log.info("【本地事务状态】{}", result.getLocalTransactionState());
        }

        //声明本地事务监听器
        @RocketMQTransactionListener(rocketMQTemplateBeanName = "rocketMQTemplate")
            class ProducerLocalTransactionListener implements RocketMQLocalTransactionListener {
                private ConcurrentHashMap<String, Object> localTrans = new ConcurrentHashMap<>();

                //半消息投递成功后执行的逻辑
                @Override
                public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
                    try {
                        log.info("【收到半消息响应ACK,执行本地事务:】");
                        log.info("Message:{}", message);
                        log.info("Object:{}", o);
                        localTrans.put(message.getHeaders().getId() + "", message.getPayload());
                        return RocketMQLocalTransactionState.UNKNOWN;
                        //return RocketMQLocalTransactionState.COMMIT;
                    } catch (Exception e) {
                        e.printStackTrace();
                        log.error("【执行本地事务异常】 Exception:{}", e.getMessage());
                        return RocketMQLocalTransactionState.ROLLBACK;
                    }
                }

                //回查本地事务执行状态
                @Override
                public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
                    log.info("【检查本地事务状态】");
                    return RocketMQLocalTransactionState.COMMIT;
                }
            }
    }

consumer

#RocketMQ NameServer地址,多个nameserver地址采用";"分隔
rocketmq.name-server=127.0.0.1:9876
#rocketmq.name-server=127.0.0.1:9876;192.168.31.173:9876
#消息消费者组名
rocketmq.consumer.group0=consumer-group0
rocketmq.consumer.group1=consumer-group1
rocketmq.consumer.group2=consumer-group2
rocketmq.consumer.group=order-group

声明消费者

package com.scst.service;

import com.scst.domain.Order;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;


//消息消费服务
@Slf4j
    @Service
    public class ConsumeService {

            @Component
            //topic需要和生产者的topic一致;consumerGroup属性必须指定,内容可随意;messageModel默认为CLUSTERING
            @RocketMQMessageListener(nameServer = "${rocketmq.name-server}", topic = "msg-topic", consumerGroup = "${rocketmq.consumer.group0}", messageModel = MessageModel.BROADCASTING)
            class ConsumerInGroup0 implements RocketMQListener<Object> {
                @Override
                public void onMessage(Object o) {
                    //消息体(Payload)o为JSON对象(字符串)
                    log.info("C0开始消费消息:{}", o);
                }
            }

            @Component
            //topic需要和生产者的topic一致;consumerGroup属性必须指定,内容可随意;consumeMode默认为CONCURRENTLY
            @RocketMQMessageListener(nameServer = "${rocketmq.name-server}", topic = "msg-topic", consumerGroup = "${rocketmq.consumer.group1}", consumeMode = ConsumeMode.ORDERLY)
            class ConsumerInGroup1 implements RocketMQListener<Order> {
                @Override
                public void onMessage(Order o) {
                    //消息体(Payload)o为Java对象
                    log.info("C1开始消费消息:{}", o);
                }
            }

            @Component
            //topic需要和生产者的topic一致;consumerGroup属性必须指定,内容可随意;consumeMode默认为CONCURRENTLY;messageModel默认为CLUSTERING
            @RocketMQMessageListener(nameServer = "${rocketmq.name-server}", topic = "msg-topic", consumerGroup = "${rocketmq.consumer.group2}")
            class ConsumerInGroup2 implements RocketMQListener<Order> {
                @Override
                public void onMessage(Order o) {
                    //消息体(Payload)o为Java对象
                    log.info("C2开始消费消息:{}", o);
                }
            }
    }