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工作流程
- 启动NameServer, NameServer启动后开始监听端口,等待Broker、Producer、Consumer连接。
- 启动Broker时,Broker会与所有NameServer建立并保持长连接,然后每30秒向NameServer定时发送心跳包。
3)发送消息前,先通过RocketMQ控制台创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
- Producer发送消息,启动时先跟NameServer集群中的一台建立长连接,并从NameServer中获取到Topic路由信息 (该Topic路由表及Broker列表) ,然后以某种Queue选择算法从队列列表中选择一个Queue,并与Queue所在的Broker (Master) 建立长连接,且定时向Master发送心跳,从而向Broker发消息。
Producer在获取到路由信息后,会首先将路由信息缓存到本地,再每30秒从NameServer更新一次路由信息。
- 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);
}
}
}