RocketMQ分布式消息队列
课程内容
- RocketMQ认识
- RocketMQ安装
- RocketMQ原理(重要)
- RocketMQ应用(重要)
一.RocketMQ的认识
1.MQ概述
1.1.为什么用MQ(重要)
为什么要使用RocketMQ,我们先来看一个天府通刷地铁出站的业务场景

【注意】假如天府通出站API 和 支付系统 是不同的子系统 (两个Tomcat远程通信)
上下班高峰期使用天府通刷码的人非常多,以为做并发量很高,一个出站请求到后台需要做费用结算,或者积分赠送等业务。由于并发很高,并且费用结算和积分等业务本来就耗时,况且支付服务也不一定能承担那么大的请求量。
当服务器线程耗尽,后续请求会等待变慢,再加上高并发请求就会导致后续请求越来越慢,请求长时间等待,导致大量请求超时。并发太高,可能会导致服务器的内存上升,CPU使用率急速上升,甚至导致服务器宕掉。
解决方案:使用MQ消峰,效果如下

加入MQ后的效果 削峰限流
- 高并发请求在MQ中排队,达到了消除峰值的目的,不会有大量的请求同时怼到支付系统
- 服务异步调用,“天府通出站API” 把结算消息放入MQ就可以返回“出站成功,费用稍后结算”给用户,响应时间很快
- 服务彻底解耦,即使支付服务挂掉,也不影响“天府通出站API”正常工作,当支付系统再启动仍然可以继续消费MQ中的消息。
1.2.MQ是什么
MQ全称为Message Queue,即消息队列 ,是一种提供消息队列服务的中间件,也称为消息中间件,是一套提供了消息生 产、存储、消费全过程的软件系统,遵循FIFO原则。
1.3.MQ的使用场景(重要) 必问 -3
限流削峰
MQ可以将系统的超量请求暂存其中,以便系统后期可以慢慢进行处理,从而避免了请求的丢失或系统 被压垮。
异步&解耦
上游系统对下游系统的调用若为同步调用,则会大大降低系统的吞吐量与并发度,且系统耦合度太高。 而异步调用则会解决这些问题。所以两层之间若要实现由同步到异步的转化,一般性做法就是,在这两层间添加一个MQ层。 即使消费者挂掉也不影响生产者工作,只要把消息放入队列即可,消费者重启后自己消费即可。
数据收集
分布式系统会产生海量级数据流,如:业务日志、监控数据、用户行为等。针对这些数据流进行实时或 批量采集汇总,然后对这些数据流进行大数据分析,这是当前互联网平台的必备技术。通过MQ完成此 类数据收集是最好的选择。
大数据处理
比如我们的平台向“三方平台”获取数据,一次请求了大量数据回来要进行处理,由于数据较多处理不过来,那么就可以放入MQ,再创建一些消费者进行数据处理即可。
【注意】如下情况不太适合MQ
- 小项目,体量不大,并发量低的使用MQ会太过笨重 - 你可以考虑使用Redis做一个消息队列 – 不擅长。 list
- 对数据的一致性有要求(强一致性)的的场景不适合使用MQ,因为MQ是异步的。
1.4.使用MQ的好处(了解)
提高系统响应速度
任务异步处理。 将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
提高系统稳定性
一是并发被消峰后,系统不容易被高并发打垮,二是系统挂了也没关系,操作内容放到消息队列不丢失,后续重新消费者一样能消费做业务处理。
排序保证 FIFO
遵循队列先进先出的特点,能够保证消息按照添加的数据被消费。
1.5.常见MQ产品(了解)
ActiveMQ
ActiveMQ是使用Java语言开发一款MQ产品。早期很多公司与项目中都在使用。但现在的社区活跃度已 经很低。现在的项目中已经很少使用了。
RabbitMQ
RabbitMQ是使用ErLang语言开发的一款MQ产品。其吞吐量较Kafka与RocketMQ要低,且由于其不是 Java语言开发,所以公司内部对其实现定制化开发难度较大。
Kafka
Kafka是使用Scala/Java语言开发的一款MQ产品。其最大的特点就是高吞吐率,常用于大数据领域的实 时计算、日志采集等场景。其没有遵循任何常见的MQ协议,而是使用自研协议。对于Spring Cloud Netflix,其仅支持RabbitMQ与Kafka。
RocketMQ
RocketMQ是使用Java语言开发的一款MQ产品。经过数年阿里双11的考验,性能与稳定性非常高。其 没有遵循任何常见的MQ协议,而是使用自研协议。对于Spring Cloud Alibaba,其支持RabbitMQ、 Kafka,但提倡使用RocketMQ
下面是MQ的对比图:

技术选型建议:
- 业务场景简单,允许数据丢失,想要快速上线,推荐使用Redis
- 大数据场景,日志收集,实时性要求高,推荐Kafka
- 金融领域,不能接受消息丢失或重复,推荐使用RabbitMQ或者RocketMQ
1.6.MQ常见协议(了解)
- AMQP协议
AMQP是一套公开的消息队列协议,最早在2003年被提出,它旨在从协议层定义消息通信数据的标准格式, 为的就是解决MQ市场上协议不统一的问题。基于此协议的客户端与消息中间件可传递 消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制,RabbitMQ就是遵循AMQP标准协议开发的MQ服务。 官方:http://www.amqp.org
- JMS协议
JMS是Java消息服务,是java提供的一套消息服务API标准,其目的是为所有的java应用程序提供统一的消息通信的标准,类似java的 jdbc,只要遵循jms标准的应用程序之间都可以进行消息通信。它和AMQP有什么 不同,jms是java语言专属的消 息服务标准,它是在api层定义标准,并且只能用于java应用;而AMQP是在协议层定义的标准,是跨语言的 。
- STOMP
STOMP,Streaming Text Orientated Message Protocol(面向流文本的消息协议),是一种MOM设计
的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。ActiveMQ是该协议的典型实现,RabbitMQ通过插件可以支持该协议。
- MQTT
MQTT,Message Queuing Telemetry Transport(消息队列遥测传输),是IBM开发的一个即时通讯协 议,是一种二进制协议,主要用于服务器和低功耗IoT(物联网)设备间的通信。该协议支持所有平 台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器的通信协议。 RabbitMQ通 过插件可以支持该协议。
2.RocketMQ介绍
2.1.RocketMQ是什么
RocketMQ是一个统一消息引擎、轻量级数据处理平台。
RocketMQ是⼀款阿⾥巴巴开源的消息中间件,双十一承载了万亿级消息的流转,2016年11⽉,阿⾥巴巴向 Apache 软件基⾦会捐赠 RocketMQ,成为 Apache 孵化项⽬,2017 年 9 ⽉ ,Apache 宣布 RocketMQ孵化成为 Apache 顶级项⽬(TLP )成为国内⾸个互联⽹中间件在 Apache 上的顶级项⽬。
2.2.RocketMQ特征
二.RocketMQ的安装
1.RocketMQ安装
1.1.下载RocketMQ
下载地址:https://rocketmq.apache.org/download/

下载后解压

Bin : 可执行文件目录
Conf:配置文件目录
Lib : 依赖库,一堆Jar包
1.2.配置ROCKETMQ_HOME
解压压缩包,配置 ROCKETMQ_HOME

1.3.启动MQ
- 启动NameServer
Cmd命令框执行进入至MQ文件夹\bin下,然后执行 start mqnamesrv.cmd,启动NameServer。

成功后会弹出提示框,此框勿关闭。
- 启动Broker
进入至MQ文件夹\bin下,修改Bean目录下的 runbroker.cmd 中JVM占用内存大小

CMD执行start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true ,启动Broker。
成功后会弹出提示框,此框勿关闭
1.4.RocketMQ存储结构[了解]
RocketMQ安装好之后会在用户目录下产生一个store目录用来存储相关数据:

- Commitlog : 消息是存储,在commitlog目录中,以mapperdFile文件顺序存储消息。
- Config : 存放运行期间的配置文件
- Consumerqueue : 该目录中存放的是队列,consume queue存放着commitlog中的消息的索引位置
- Index :存放着消息索引文件 indexFile,用来实现根据key进行消息的快速查询
- Abort : 该文件在broker启动后自动创建,正常关闭abort会消失
- Checkpoint :记录 Commitlog ,Consumerqueue 和index 文件的最后刷盘时间戳
[问]RocketMQ数据存储在磁盘会影响性能吗?
不会,RocketMQ的性能在所有的MQ中是比较高的,主要是因为RocketMQ使用了mmap零拷贝技术,consumequeue中的数据是顺序存放的,还引入了PageCache的预读取机制,使得对 consumequeue文件的读取几乎接近于内存读取,即使在有消息堆积情况下也不会影响性能。
2.RocketMQ插件
为了方便管理,我们需要安装一个可视化插件
2.1.下载插件
RocketMQ可视化管理插件下载地址:https://github.com/apache/rocketmq-externals/releases

2.2.修改配置
解压后,修改配置:src/main/resource/application.properties ,这里需要指向Name Server 的地址和端口 如下:

2.3.打包插件
回到安装目录(pom.xml所在目录),执行: mvn clean package -Dmaven.test.skip=true ,然后会在target目录生成打包后的jar文件

2.4.启动插件
进入 target 目录,CMD执行 java -jar rocketmq-console-ng-1.0.0.jar , 访问 http://localhost:8080

三.RocketMQ的原理
1.RokcetMQ架构
RocketMQ开发官方文档:
https://github.com/apache/rocketmq/blob/master/docs/cn/RocketMQ_Example.md
RocketMQ的集群架构如下

RocketMQ架构上主要分为四部分,如上图所示
1.1.Producer
消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
1.2.Consumer
消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。
1.3.Broker
Broker主要负责消息的存储、投递和查询以及服务高可用保证。
1.4.NameServer
NameServer是一个Broker与Topic路由的注册中心支持Broker的动态注册与发现,主要包括两个功能
2.RocketMQ入门
官方案例:https://github.com/apache/rocketmq/blob/master/docs/cn/RocketMQ_Example.md
2.1.导入依赖
注意和安装的MQ版本一致
1 2 3 4 5
| <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.8.0</version> </dependency>
|
2.2.生产者
步骤分析
- 创建producer组
- 设置NameServer地址 : 如果实在安装不上,可以使用这个地址:115.159.88.63:9876
- startr生产者
- 发送消息获取结果
- 结束producer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class Producer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producer-hello");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
Message message = new Message("log-topic", "info-tag", "这是一个info信息".getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult result = producer.send(message);
System.out.println("发送完毕,结果: "+result); } }
|
代码解释:
DefaultMQProducer : MQ生产者 , 可以指定组名 producerGroupName
producer.setNamesrvAddr : 指定Name Server地址,用作Brocker发现。注意IP和启动name server服务时指定的IP保持一致。
producer.start() : 启动 生产者
new Message(“topic_log”,”tags_error”,(“我是消息”+i).getBytes()) :消息,参数为:topic,tags,内容
producer.send(message) : 发送消息
SendResult :发送结果,其中包含
- sendStatus=SEND_OK :发送状态
- msgId :producer 创建的消息ID
- offsetMsgId :Brocker创建的消息ID
- messageQueue :消息存储的队列
- producer.shutdown():关闭生产者
2.3.消费者
创建consumer组
设置Name Server地址
设置消费位置,从最开始销毁
设置消息回调处理监听 -> 处理消息
Start consumer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public class Consumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-hello");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.subscribe("log-topic","info-tag");
consumer.registerMessageListener(new MessageListenerConcurrently(){
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { System.out.printf("%s 接收到新的消息: %n", Thread.currentThread().getName()); msgs.stream().forEach(messageExt -> { String body = null; try { body = new String(messageExt.getBody(), RemotingHelper.DEFAULT_CHARSET); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } System.out.println(body); }); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } });
consumer.start();
System.out.println("消费者启动...");
} }
|
- DefaultMQPushConsumer :消费者 , 可以指定 consumerGroupName
- consumer.setNamesrvAddr : 设置name server 地址
- consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET) :从什么位置开始消费
- consumer.subscribe(“topic_log”, “tags_error”) :订阅某个topic下的某个tags的消息
- consumer.registerMessageListener :注册消息监听器,拿到消息后,进行消息处理。
- ConsumeConcurrentlyStatus :消费者消费结果状态,ConsumeConcurrentlyStatus.CONSUME_SUCCESS代表成功,ConsumeConcurrentlyStatus.RECONSUME_LATER代表消费失败,稍后重试,会进行多次重试
3.RocketMQ 核心概念
3.1.RocketMQ工作原理

1.RocketMQ 网络部署特点
为了增强Broker性能与吞吐量,Broker一般都是以集群形式出现的。各集群节点中可能存放着相同Topic的不同Queue。
不过,这里有个问题,如果某Broker节点宕机,如何保证数据不丢失呢?其解决方案是,将每个Broker集群节点进行横向扩展,即将Broker节点再建为一个HA集群,解决单点问题。
Broker节点集群是一个主从集群,即集群中具有Master与Slave两种角色。Master负责处理读写操作请求,Slave负责对Master中的数据进行备份。当Master挂掉了,Slave则会自动切换为Master去工作。所以这个Broker集群是主备集群。Consumer既可以从Master订阅消息,也可以从Slave订阅消息
一个Master可以包含多个Slave,但一个Slave只能隶属于一个Master。 Master与Slave 的对应关系是通过指定相同的BrokerName、不同的BrokerId 来确定的。BrokerId为0表示Master非0表示Slave。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。
2.RocketMQ工作流程
启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心。
Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。
Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息
3.2.Producer 生产者
RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。
RocketMQ中的消息生产者都是以生产者组(Producer Group)的形式出现的。生产者组是同一类生产者的集合,这类Producer发送相同Topic类型的消息。一个生产者组可以同时发送多个主题的消息。
Producer会使用一定的算法选择把消息发送到哪个master的某个queue中。
3.3.Consumer 消费者
Consumer 支持两种消费形式:拉取式消费、推动式消费。(主动,被动),RocketMQ中的消息消费者都是以消费者组(Consumer Group)的形式出现的。消费者组是同一类消费者的集合,这类Consumer消费的是同一个Topic类型的消息,不同的 Consumer Group可以消费同一个Topic。

一个Consumer Group内的Consumer可以消费多个Topic的消息。

[注意] 一个Queue是不能被同一个ConsumerGroup中的多个Consumer消费的,目的是减少资源竞争提升整体性能。
3.4.Topic 消息主题
Topic表示一类消息的集合,每个topic主题包含若干条message消息,每条message消息只能属于一个topic主题,Topic是RocketMQ进行消息订阅的基本单位。
3.5.Message
消息是指,消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。
3.6.Tag 标签
为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。Topic是消息的一级分类,Tag是消息的二级分类
3.7.MessageQueue队列
一个Topic中可以包含多个Queue,一 个Topic的Queue也被称为一个Topic中消息的分区(Partition)。 在一个Consumer Group内,一个Queue最多只能分配给一个Consumer,一个Cosumer可以分配得到多个Queue。这样的分配规则,每个Queue只有一个消费者,可以避免消费过程中的多线程处理和资源锁定,有效提高各Consumer消费的并行度和处理效率。
消费者组中Consumer的数量应该小于等于订阅Topic的Queue数量。如果超出Queue数量,则多出的 Consumer将不能消费消息。如果一个Consmer挂了,该Consumer Group中的其它Consumer可以接着消费原Consumer消费的Queue。

【注意】 一个Topic可以对应多个消费者 ,一个Queue只能对应一个组中的一个消费者。
【注意】为了防止消息紊乱,一个Consumer Group 中的Consumer都是订阅相同Topic下的Queue。
读写队列
Queue分为 写队列 和 读队列 ,默认创建数量是都是4 ,这个读写队列是从逻辑上进行划分在物理上读/写是一个队列,Producer发送的消息进入写队列 ,Consumer从读队列获取数据,一般情况下读写队列数量是一样的。
可以通过可视化界面修改Topic中的队列数量

perm用于设置对当前创建Topic的操作权限:2表示只写,4表示只读,6表示读写。
3.8.MessageId/Key[了解]
RocketMQ中每个消息拥有唯一的MessageId,且可以携带具有业务标识的Key,以方便对消息的查询。 不过需要注意的是,MessageId有两个:在生产者send()消息时会自动生成一个MessageId(msgId),
当消息到达Broker后,Broker也会自动生成一个MessageId(offsetMsgId)。msgId、offsetMsgId与key都称为消息标识。
3.9.Rebalance重新负载[了解]
当消费者数量或者Queue的数量修改,Rebalance是把⼀个Topic下的多个Queue重新分配给Consumer Group下的Consumer。目的是增加消费能力。
由于一个队列只分配给一个Consumer,那么当Consumer Group中的消费者数量大于队列数量,那么多出来的Consumer分配不到队列。
3.10.消息拉取模式
消息到消费分为:拉取式 pull ,和推送是 push
3.11.消息消费模式
3.12.Queue的分配算法
Queue是如何分配给Consumer的,这对应了四种算法:平均分配策略,环形平均策略,一致性Hash策略,同机房策略。
平均分配【默认】:根据 qeueuCount / consumerCount 作为每个消费者平均分配数量,如果多出来的queue就再依次逐个分配给Consumer。
环形平均策略:根据消费者的顺序,一个一个的分配Queue即可类似于发扑克牌。
一致性Hash策略 : 该算法将Consumer的Hash值作为节点放到Hash环上,然后将Queue的hash值也放入Hash环上,通过顺时针进行就近分配。
同机房策略:该算法会根据queue的部署机房位置和consumer的位置,过滤出当前consumer相同机房的queue。然后按照平均分配策略或环形平均策略对同机房queue进行分配。如果没有同机房queue,则按照平均分配策略或环形平均策略对所有queue进行分配。
平均分配性能比较高,一致性Hash性能不高,但是能减少Rebalance,如果Consumer数量变动频繁可以使用一致性Hash。
3.13.Offset管理
RockertMQ通过Offset来维护Consumer的消费进度,比如:消费者从哪个位置开始持续消费消息的?这里有三个枚举来指定从什么位置消费
CONSUME_FROM_LAST_OFFSET:从queue的最后一条消息开始消费
CONSUME_FROM_FIRST_OFFSET:从queue的第一条消息开始消费
CONSUME_FROM_TIMESTAMP:从某个时间戳位置的消息开始消费。
消费者消费结束之后,会向Consumer会提交其消费进度offset给Broker。Offset信息的存储分为本地 Offset管理 和远程Offset管理
- 远程Offset管理:Brocker通过 store/config/consumerOffset.json 文件以JSON方式来存储offset相关数据以json的形式:适用于集群模式
- 本地Offset管理:offset相关数据以json的形式持久化到Consumer本地磁盘文件中,路径为当前用户主目录下的.rocketmq_offsets/${clientId}/${group}/Offsets.json :适用于广播模式
Offset的同步提交与异步提交: 集群消费模式下,Consumer消费完消息后会向Broker提交消费进度offset,其提交方式分为两种:
- 同步提交:消费者在消费完一批消息后会向broker提交这些消息的offset,等待broker的成功响应。若在等待超时之前收到了成功响应,则继续读取下一批消息进行消费(从ACK中获取 nextBeginOffset)。若没有收到响应,则会重新提交,直到获取到响应。而在这个等待过程中,消费 者是阻塞的。其严重影响了消费者的吞吐量。
- 异步提交:消费者在消费完一批消息后向broker提交offset,但无需等待Broker的成功响应,可以继续读取并消费下一批消息。这种方式增加了消费者的吞吐量。但需要注意,broker在收到提交的offset 后,还是会向消费者进行响应的。可能还没有收到ACK,此时Consumer会从Broker中直接获取 nextBeginOffset。
3.14.消息的清理[了解]
消息不会被单独清理,消息是顺序存储到commitlog的,消息是以commitlog为单位进行清理,RocketMQ有自己的清理规则,默认是72小时候后进行清理
到达时间清理点,自动清理过期的文件(凌晨4点)
磁盘空间使用率达到了过期清理阈值(75%),自动清理过期的文件。
磁盘占用率达到清理阈值(85%),开始按照设定的规则清理文件,从老的文件开始。
磁盘占用率达到系统危险阈值(90%),拒绝写入数据。
四.RocketMQ的使用-原始方式
官网:https://github.com/apache/rocketmq/tree/master/docs/cn
1.普通消息(重要)
1.1.同步发送
同步消息是发送者发送消息,需要等待结果的返回,才能继续发送第二条消息,这是一种阻塞式模型,虽然消息可靠性高,但是阻塞导致性能低。API : SendResult result = producer.send(message); 代码示例:
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.8.0</version> </dependency> </dependencies>
|
发送者代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class Producer {
public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { DefaultMQProducer producer = new DefaultMQProducer("syn-producerGroup"); producer.setNamesrvAddr("127.0.0.1:9876");
producer.setDefaultTopicQueueNums(2); producer.start();
for (int i = 0 ; i < 16 ; i++){
Message message = new Message(); message.setTopic("syn-topic"); message.setTags("sms"); message.setBody((i+"我是消息").getBytes(CharsetUtil.UTF_8)); SendResult result = producer.send(message); System.out.println(result); }
producer.shutdown(); } }
|
同步发送使用 SendResult result = producer.send(message); 方法即可,马上可以拿到返回值。SendResult 结果如下
1 2 3
| SendResult [ sendStatus=SEND_OK, msgId=C0A8006516B018B4AAC270EF9D940000,offsetMsgId=C0A8006500002A9F0000000000008E1C, messageQueue=MessageQueue [topic=syn-topic, brokerName=LAPTOP-20VLGCRC, queueId=3], queueOffset=0]
|
1.2.异步发送
异步消息是发送者发送消息,无需等待发送结果就可以再发送第二条消息,它是通过回调的方式来获取到消息的发送结果,消息可靠性高,性能也高。API : producer.send(message,SendCallback) 示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| . . . 省略. . . producer.send( new Message("asyn-topic", "sms", "我是消息".getBytes(CharsetUtil.UTF_8)), new SendCallback() {
@Override public void onSuccess(SendResult sendResult) { System.out.println(sendResult); } @Override public void onException(Throwable throwable) { System.out.println("发送异常:"+throwable.getMessage()); } } );
|
SendCallback 是消息发送结果回调。如果:sendResult.getSendStatus() == SendStatus.SEND_OK 表示成功
1.3.单向发送
这种方式指的是发送者发送消息后无需等待Broker的结果返回,Broker也不会返回结果,该方式性能最高,但是消息可靠性低。API : producer.sendOneway(message) 示例代码:
1 2 3
| ... 省略... Message message = new Message("asyn-topic", "sms", "我是消息".getBytes(CharsetUtil.UTF_8)); producer.sendOneway(message);
|
sendOneway 单向发送是没有返回结果值的。
1.5.总结
下面对三种发送方式做一个对比
使用场景建议如下
如果是比较重要的不可丢失的消息,且对时效性要去不高建议使用同步发送,如转账消息
如果是不重要的可失败的消息,比如日志消息,建议使用单向发送
如果对时效性要求比较高,且消息不能丢失,可以尝试使用异步发送
2.顺序消息(扩展)
在某些业务场景下是需要消息按照顺序进行消费,比如一个账户的加钱,减钱的动作必须按照时间先后去执行,否则就会发生金额不够导致操作失败。
按照发送的顺序进行消费就是顺序消息,遵循(FIFO), 默认生产者以Round Robin轮询方式把消息发送到不同的Queue分区队列;消费者从多个队列中消费消息,这种情况没法保证顺序。
2.1.全局有序
全局有序是一个topic下的所有消息都要保证顺序,如果要保证消息全局顺序消费,就需要保证使用一个队列存放消息,一个消费者从这一个队列消费消息就能保证顺序,即:单线程执行,可以通过 producer.setDefaultTopicQueueNums(1);来指定队列数量。
1.发送者
可以通过代码指定创建1个队列即可
1
| producer.setDefaultTopicQueueNums(1);
|
2.消费者
使用一个线程,一次只拉取一个消息 , 使用 MessageListenerOrderly 有序的消费消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| defaultMQPushConsumer.setConsumeThreadMax(1); defaultMQPushConsumer.setConsumeThreadMin(1);
defaultMQPushConsumer.setPullBatchSize(1);
...省略... defaultMQPushConsumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) { list.forEach(message->{ System.out.println(message+" ; "+new String(message.getBody(), CharsetUtil.UTF_8)); });
return ConsumeOrderlyStatus.SUCCESS; }
}); ...省略...
|
2.2.部分有序
还有一种就是分区有序或者部分有序,部分顺序消息只要保证某一组消息被顺序消费,即:只需要保证一个队列中的消息有序消费即可。
比如:保证同一个订单ID的生成、付款、发货消息按照顺序消费即可实现原理:

1.生产者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { DefaultMQProducer producer = new DefaultMQProducer("syn-producerGroup");
producer.setNamesrvAddr("127.0.0.1:9876"); producer.setSendMsgTimeout(1000); producer.start();
for (long i = 0 ; i < 4 ; i++){
Order order = new Order(i,"订单"+i,"创建");
byte[] bytes = (JSON.toJSONString(order)).getBytes(CharsetUtil.UTF_8);
Message message = new Message("topic-order","product-order",bytes); message.setKeys("key-"+i);
SendResult result = producer.send(message, new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { Long id = (Long) arg; int index =(int) (id % mqs.size()); return mqs.get(index); }
},order.getId());
System.out.println(result);
order.setStatus("支付"); bytes = (JSON.toJSONString(order)).getBytes(CharsetUtil.UTF_8); message = new Message("topic-order","product-order",bytes); message.setKeys("key-"+i);
result = producer.send(message,new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { Long id = (Long) arg; int index =(int) (id % mqs.size()); return mqs.get(index); } },order.getId());
System.out.println(result);
order.setStatus("发货"); bytes = (JSON.toJSONString(order)).getBytes(CharsetUtil.UTF_8); message = new Message("topic-order","product-order",bytes); message.setKeys("key-"+i);
result = producer.send(message,new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { Long id = (Long) arg; int index =(int) (id % mqs.size()); return mqs.get(index); } },order.getId());
System.out.println(result);
}
producer.shutdown(); }
|
2.消费者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class Consumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("syn-consumerGroup"); defaultMQPushConsumer.setNamesrvAddr("127.0.0.1:9876"); defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); defaultMQPushConsumer.subscribe("topic-order","product-order");
defaultMQPushConsumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) { list.forEach(message->{ System.out.println(message+" ; "+new String(message.getBody(), CharsetUtil.UTF_8)); });
return ConsumeOrderlyStatus.SUCCESS; } });
defaultMQPushConsumer.start(); } }
|
3.延迟消息(重要)
3.1.延迟消息概述
我们通常使用定时任务比如Quartz来解决超时业务,比如:订单支付超时关单,VIP会员超时提醒。但是使用定时任务来处理这些业务场景在数据量大的时候并不是一个很好的选择,会造成大量的空扫描浪费性能。我们可以考虑使用延迟消息来解决。
延迟消息即:把消息写到Broker后需要延迟一定时间才能被消费 , 在RocketMQ中消息的延迟时间不能任意指定,而是由特定的等级(1 到 18)来指定,分别有:
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
可以通过修改配置来增加级别,比如在mq安装目录的 broker.conf 文件中增加
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 2d 这个时候总共就有19个level。
下面是延迟消息内部工作流程图

RocketMQ Broker端在存储生产者写入的消息时,首先都会将其写入到CommitLog中。之后根据消息中的Topic信息和队列信息,将其转发到目标Topic的指定队列(ConsumeQueue)中。不过,在分发之前,系统会先判断消息中是否带有延时等级。若没有,则直接正常分发;如果有就走下面的流程
修改消息Topic的名字为SCHEDULE_TOPIC_XXXX
根据延时等级,在consumequeue目录中SCHEDULE_TOPIC_XXXX主题下创建出相应的queueId
目录与consumequeue文件
- 修改消息索引单元,计算出的投递时间当做消息Tag的哈希值存储到CosumeQueue中,投递时间 = 消息存储时间 + 延时等级时间 。下面是CosumeQueue单个存储单元组成结构如下

将消息索引写入到SCHEDULE_TOPIC_XXXX主题下相应的consumequeue中
Broker内部有⼀个延迟消息服务类ScheuleMessageService,根据延迟级别数,创建对应数量的定时器Timer,定时消费SCHEDULE_TOPIC_XXXX中的消息,并投递到目标Topic中。
在将消息到期后,队列的Level等级改为0,作为一条普通消息,投递到目标Topic。
3.2.延迟消息实战
只需要一处改动,发送者通过 message.setDelayTimeLevel(3); 设置延迟级别即可
1.消息发送者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public class Producer {
public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { DefaultMQProducer producer = new DefaultMQProducer("syn-producerGroup-delay");
producer.setNamesrvAddr("127.0.0.1:9876"); producer.start();
for (long i = 0 ; i < 4 ; i++){
Order order = new Order(i,"订单"+i,"创建");
byte[] bytes = (JSON.toJSONString(order)).getBytes(CharsetUtil.UTF_8);
Message message = new Message("topic-order-delay","product-order-delay",bytes); message.setDelayTimeLevel(3); message.setKeys("key-"+i);
SendResult result = producer.send(message); System.out.println("发送时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(result); }
producer.shutdown(); } }
|
2.消息消费者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class Consumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("syn-consumerGroup-delay"); defaultMQPushConsumer.setNamesrvAddr("127.0.0.1:9876"); defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
defaultMQPushConsumer.subscribe("topic-order-delay","product-order-delay");
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
list.forEach(message->{ System.out.println("消费时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); System.out.println(message+" ; "+new String(message.getBody(), CharsetUtil.UTF_8)); });
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } });
defaultMQPushConsumer.start(); } }
|
4.事务消息(重要)
4.1.事务消息概述
如果业务只涉及到一个数据库的写操作,我们只需要保证这一个事物的提交和回滚,这种事务管理叫传统事物或本地事务,如果业务涉及到多个数据库(多个服务)的写操作,我们需要保证多个数据库同时提交或回滚,这种夸多个数据库的事务操作叫分布式事务。 强一致性
分布式事物的解决方案有很多,如:2PC,TCC,最终一致性,最大努力通知等等。这里要介绍的是基于RocketMQ事务消息的最终一致性方案,下面举个例子。
用户注册成功,向用户数据库保存用户信息,同时通过远程调用积分服务为用户赠送积分,模型如下:

我们需要使用分布式事务管理实现用户数据库和积分数据库的一致性。即:用户保存成功,用户的积分也要保存成功,或者都回滚不做任何存储。这种业务场景可以选择2PC强一致性方案,也可以选择最终一致性。我们选择最终一致性,因为用户注册成功,不要求马上赠送积分,延迟一定时间后再赠送成功也是允许的。所以有了如下模型

事务流程
用户服务(事务发起方)往MQ中发送一个事务消息,
MQ返回结果是否发送成功
用户服务受到消息发送成功结果,保存用户数据,提交本地事务
积分服务拿到MQ中的事务消息
积分服务保存积分到数据库
4.2.事务消息原理
事务流程中的最大的难点就是如何保证事务消息发送和本地事务的原子性,即:第一步和第二步要么都成功,要么都失败,不能说消息发送成功了,结果用户保存失败了,那么积分服务可能会增加成功,就导致数据不一致。RocketMQ已经帮我们处理好这个问题。它的工作原理如下[理解]:
详细流程如下[扩展]

事务发起方,即用户服务会先向broker发送一个prepare“半事务消息”(一个并不完整的消息)到RMQ_SYS_TRANS_HALF_TOPIC的queue中, 该消息对消费者不可见。
MQ会返回一个ACK确认消息发送成功或者失败
消息发送成功,用户服务执行保存用户操作,提交本地事务,并根据本地事务的执行结果来决定半消息的提交状态为提交或者回滚
本地事务提交成功,事务发起方即用户服务会向broker再次发起“结束半事务”消息请求,commit或者rollback指令
broker端收到请求后,首先从RMQ_SYS_TRANS_HALF_TOPIC的queue中查出该消息,设置为完成状态。如果消息状态为提交,则把半消息从RMQ_SYS_TRANS_HALF_TOPIC队列中复制到这个消息原始topic的queue中去(之后这条消息就能被正常消费了);如果消息状态为回滚,则什么也不做。
Producer发送的半消息结束请求是oneway的,也就是发送后就不管了,只靠这个是无法保证半消息一定被提交的(比如未执行第4步),rocketMq提供了一个兜底方案,这个方案叫消息反查机制,Broker启动时,会启动一个TransactionalMessageCheckService任务,该任务会定时从半消息队列中读出所有超时未完成的半消息,针对每条未完成的消息,Broker会给对应的Producer发送一个消息反查请求,根据反查结果来决定这个半消息是需要提交还是回滚,或者后面继续来反查
consumer(本例中指积分系统)消费消息,执行本地数据变更,提交本地事务
4.3.事务消息实战
我们需要做什么
编写本地事务检查监听TransactionListener ,一是执行本地事务逻辑,二是返回本地事务执行状态
发消息时生产者需要设置producer.setTransactionListener 事务监听
1.事务监听器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class MyTransactionCheckListener implements TransactionListener {
@Override public LocalTransactionState executeLocalTransaction(Message message, Object o) {
return LocalTransactionState.COMMIT_MESSAGE ; }
@Override public LocalTransactionState checkLocalTransaction(MessageExt messageExt) { return LocalTransactionState.COMMIT_MESSAGE; } }
|
2.消息生产者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class TransationSender {
public static void main(String[] args) throws MQClientException { TransactionMQProducer producer = new TransactionMQProducer("tran-product-group");
producer.setNamesrvAddr("127.0.0.1:9876");
ExecutorService excutorService = Executors.newFixedThreadPool(20);
producer.setExecutorService(excutorService); producer.setTransactionListener(new MyTransactionCheckListener());
producer.start();
for(int i = 0 ; i < 10 ; i++){ String orderId = UUID.randomUUID().toString(); String tags = "Tag"; Message message = new Message("topic-tran", "tag", orderId, ("下单:"+i).getBytes(CharsetUtil.UTF_8)); TransactionSendResult transactionSendResult = producer.sendMessageInTransaction(message, null); System.out.println(transactionSendResult);
} producer.shutdown(); } }
|
3.消息消费者
正常消费
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class TransationConsumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("trans-consumer-group"); defaultMQPushConsumer.setNamesrvAddr("127.0.0.1:9876");
defaultMQPushConsumer.subscribe("topic-tran", "tag");
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
list.forEach(message->{ System.out.println(message+" ; "+new String(message.getBody(), CharsetUtil.UTF_8)); });
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } });
defaultMQPushConsumer.start(); } }
|
4.批量消息(扩展)
4.1.批量消息概述
批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的topic,而且不能是延时消息。此外,这一批消息的总大小不应超过4MB,如果超过可以有2种处理方案:
将消息进行切割成多个小于4M的内容进行发送
修改4M的限制改成更大
对于消费者而言Consumer的MessageListenerConcurrently监听接口的consumeMessage()方法的第一个参数为消息列 表,但默认情况下每次只能消费一条消息,可以通过:Consumer的pullBatchSize属性设置消息拉取数量(默认32),可以通过设置consumeMessageBatchMaxSize属性设置消息一次消费数量(默认1)。
[注意]:pullBatchSize 和 consumeMessageBatchMaxSize并不是设置越大越好,一次拉取数据量太大会导致长时间等待,性能降低。而且消息处理失败同一批消息都会失败,然后进行重试,导致消费时长增加。增加没必要的重试次数。
4.2.批量消息实战
我们需要做什么
定义消息切割器切割消息
发送消息把消息切割之后,进行多次批量发送
1.消息切割器
把消息按照4M切成多份,支持可迭代
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| public class ListSplitter implements Iterator<List<Message>> {
private final int SIZE_LIMIT = 1024 * 1024 * 4;
private final List<Message> messages;
private int currIndex;
public ListSplitter(List<Message> messages) { this.messages = messages; }
@Override public boolean hasNext() { return currIndex < messages.size(); }
@Override public List<Message> next() { int startIndex = getStartIndex(); int nextIndex = startIndex; int totalSize = 0; for (; nextIndex < messages.size(); nextIndex++) { Message message = messages.get(nextIndex); int tmpSize = calcMessageSize(message); if (tmpSize + totalSize > SIZE_LIMIT) { break; } else { totalSize += tmpSize; } } List<Message> subList = messages.subList(startIndex, nextIndex); currIndex = nextIndex; return subList; } private int getStartIndex() { Message currMessage = messages.get(currIndex); int tmpSize = calcMessageSize(currMessage); while(tmpSize > SIZE_LIMIT) { currIndex += 1; Message message = messages.get(currIndex); tmpSize = calcMessageSize(message); } return currIndex; } private int calcMessageSize(Message 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; return tmpSize; } }
|
2.发送者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public class BatchProducer {
public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException { DefaultMQProducer producer = new DefaultMQProducer("batch-producerGroup");
producer.setNamesrvAddr("127.0.0.1:9876"); producer.setMaxMessageSize(1024 * 1024 * 4); producer.start();
List<Message> messages = new ArrayList<>();
for (long i = 0 ; i < 10000 ; i++){ byte[] bytes = ("批量消息".getBytes(CharsetUtil.UTF_8)); Message message = new Message("topic-order-batch","product-order-batch",bytes); message.setKeys("key-"+i); messages.add(message); }
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) { try { List<Message> listItem = splitter.next(); SendResult sendResult = producer.send(listItem); System.out.println(sendResult); } catch (Exception e) { e.printStackTrace(); } }
producer.shutdown(); } }
|
3.消费者
我们要做什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class BatchConsumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("batch-consumerGroup"); defaultMQPushConsumer.setNamesrvAddr("127.0.0.1:9876"); defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); defaultMQPushConsumer.setPullBatchSize(32); defaultMQPushConsumer.setConsumeMessageBatchMaxSize(10);
defaultMQPushConsumer.subscribe("topic-order-batch","product-order-batch");
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
list.forEach(message->{ System.out.println(message+" ; "+new String(message.getBody(), CharsetUtil.UTF_8)); });
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } });
defaultMQPushConsumer.start(); } }
|
5.消息过滤(扩展)
5.1.消息过滤概述
消息过滤包括 tags过滤法sql过滤,消费者在消费消息的时候可以通过:Consumer.subscribe(topic,tags) 来指定要消费的消息,如果订阅多个Tag的消息,Tag间使用或运算符(双竖线||)连接。或者使用“*”来消费某Topic主题下的所有tags消息。如:
Consumer.subscribe(“topic”,”taga || tagb || tagc”)
除此之外RocketMQ还支持使用SQL进行消息过滤,这种方式可以实现对消息的复杂过滤。SQL过滤表达式中支持多种常量类型与运算符。
支持的常量类型:
数值:比如:123,3.1415
字符:必须用单引号包裹起来,比如:’abc’
布尔:TRUE 或 FALSE
NULL:特殊的常量,表示空
支持的运算符有:
不过,只有使用PUSH模式的消费者才能使用SQL过滤。API如下:
1
| public void subscribe(finalString topic, final MessageSelector messageSelector)
|
默认情况下Broker没有开启消息的SQL过滤功能,需要在Broker加载的配置文件conf/broker.conf中添加如下属性,以开启该功能:enablePropertyFilter = true
5.2.消息过滤实战
1.发送者
发送消息时,你能通过putUserProperty来设置消息的属性
1 2 3 4 5 6 7 8 9 10 11
| ...省略... byte[] bytes = (JSON.toJSONString("消息")).getBytes(CharsetUtil.UTF_8);
Message message = new Message("topic-order-filter","product-order-filter",bytes); message.putUserProperty("sex",(i % 2)+""); message.setKeys("key-"+i); SendResult result = producer.send(message); ...省略...
|
2.消费者
通过:consumer.subscribe(“topic”, MessageSelector.bySql(“ sql 条件”)); 来过滤SQL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ...省略...
defaultMQPushConsumer.subscribe("topic-order-filter", MessageSelector.bySql("sex = 0"));
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
list.forEach(message->{ System.out.println(message+" ; "+new String(message.getBody(), CharsetUtil.UTF_8)); });
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); ...省略...
|
6.消息重试(扩展)
对发送失败的消息进行重新发送叫消息重试,producer和consumer都有消息重试机制。
6.1.生产者重试
RocketMQ默认支持消息重试机制,消息重试具有如下特点
对于同步和异步消息支持消息重试,对于oneway单向消息不支持重试
普通消息具有消息重试,顺序消息不支持消息重试
消息重试可能会造成消息重复,所以消费者一定要做好幂等处理
消息发送失败有三种情况:同步发送失败、异步发送失败、消息刷盘失败
同步发送失败策略:
对于普通消息,消息发送默认采用round-robin策略来选择所发送到的队列。如果发送失败,默认重试2 次。但在重试时是不会选择上次发送失败的Broker,而是选择其它Broker。当然,若只有一个Broker其也只能发送到该Broker,但其会尽量发送到该Broker上的其它Queue。相关设置如下:
1 2 3 4
| producer.setRetryTimesWhenSendFailed(3);
producer.setSendMsgTimeout(5000);
|
如果超过重试次数,则抛出异常,由Producer去保证消息不丢。当然当生产者出现 RemotingException、MQClientException和MQBrokerException时,Producer会自动重投消息。
同时,Broker还具有失败隔离功能,使Producer尽量选择未发生过发送失败的Broker作为目标 Broker。其可以保证其它消息尽量不发送到问题Broker,为了提升消息发送效率,降低消息发送耗时。
异步发送失败策略:
异步发送失败重试时,异步重试不会选择其他broker,仅在同一个broker上做重试,所以该策略无法保 证消息不丢。 相关设置如下:
1 2
| producer.setRetryTimesWhenSendAsyncFailed(0)
|
消息刷盘失败策略:
消息刷盘超时(Master或Slave)或slave不可用(slave在做数据同步时向master返回状态不是 SEND_OK)时,默认是不会将消息尝试发送到其他Broker的。不过,对于重要消息可以通过在Broker 的配置文件设置retryAnotherBrokerWhenNotStoreOK属性为true来开启。
发送端重试实例:
1 2 3 4
| DefaultMQProducer producer = new DefaultMQProducer("DefaultProducer"); producer.setRetryTimesWhenSendFailed(5); producer.send(msg,5000L);
|
6.2.消费者重试
顺序消息重试
对于顺序消息消费失败默认会进行每隔1000毫秒进行重试,由于要保证消息是顺序消费,所以重试会导致后面的消息阻塞。可以通过下面的设置来修改重试间隔时间:
1 2
| consumer.setSuspendCurrentQueueTimeMillis(100);
|
[注意]顺序消息没有发送失败重试机制,但具有消费失败重试机制 ,顺序消息重试是无止境的,为了防止消息一直重试阻塞,务必要做好监控工作。
无顺消息重试
对于无序消息(普通消息、延时消息、事务消息),当Consumer消费消息失败时,可以通过设置返回 状态达到消息重试的效果。不过需要注意,无序消息的重试只对集群消费方式生效,广播消费方式不提供失败重试特性。即对于广播消费,消费失败后,失败消息不再重试,继续消费后续消息
重试时间间隔
对于无序消息集群消费下的重试消费,每条消息默认最多重试16次,但每次重试的间隔时间是不同的,会逐渐变长。每次重试的间隔时间如: 1s 5s 10s …2h ,如果16次都重试失败,消息进入死信队列
可在broker.conf文件中配置Consumer端的重试次数和重试时间间隔,如下:
1 2
| consumer.setMaxReconsumeTimes(10);
|
重试队列
对于需要重试消费的消息,并不是Consumer在等待了指定时长后再次去拉取原来的消息进行消费,而 是将这些需要重试消费的消息放入到了一个特殊Topic的队列中,而后进行再次消费的。这个特殊的队
列就是重试队列。 当出现需要进行重试消费的消息时,Broker会为每个消费组都设置一个Topic名称为%RETRY%consumerGroup@consumerGroup 的重试队列。
消费端重试实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| defaultMQPushConsumer.setMaxReconsumeTimes(10);
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for(MessageExt message : list){
System.out.println(message+" ; "+new String(message.getBody(), CharsetUtil.UTF_8));
if(message.getReconsumeTimes() > 3){
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }
try { throw new Exception("出现异常了..."); } catch (Exception e) { e.printStackTrace(); return ConsumeConcurrentlyStatus.RECONSUME_LATER; }
};
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } });
|
7.死信队列(扩展)
7.1.死信队列概述
消息多次消费失败,达到最大重试次数,消息不会被丢弃而是进入死信队列(Dead-Letter Queue,DLQ),死信队列中的消息被称为死信消息(Dead-Letter Message,DLM)。
死信队列具有如下特征
死信队列中的消息无法再消费,死信队列对应Topic的权限为2,只有写权限,所以死信队列没有办法读取。
3天之后死信队列分钟的消息被删除,和普通消息一样
死信队列就是一个特殊的Topic,名称为%DLQ%consumerGroup@consumerGroup,其中每个队列都是死信队列
如果⼀个消费者组未产生死信消息,则不会为其创建相应的死信队列
如果出现死信队列,说明程序除了问题,程序员应该及时的排除,进行BUG的处理。我们应该在消费者重试次数达到一定程度就对消息进行持久化,方便后续的处理。或额外定时重试。
五.SpringBoot整合RocketMQ
1. 环境搭建
1.1. 导入依赖
xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.4</version> </parent>
<dependencies> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.25</version> </dependency> </dependencies>
|
1.2. 启动类
1 2 3 4 5 6 7 8 9
| import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication public class RocketMQApplication { public static void main(String[] args) { SpringApplication.run(RocketMQApplication.class, args); } }
|
1.3. 配置文件(application.yml)
1 2 3 4 5 6 7 8 9 10 11 12 13
| rocketmq: name-server: 127.0.0.1:9876 producer: group: "service-producer-group" max-message-size: 4096 send-message-timeout: 3000 retry-times-when-send-failed: 2 retry-times-when-send-async-failed: 2 consumer: pull-batch-size: 32 pull-interval: 1000
|
2. 基础消息(回顾)
包含:普通同步消息、异步消息、Tag 消息、基础延时消息
2.1 基础生产者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Service;
@Service public class BaseRocketMQProducer {
@Autowired private RocketMQTemplate rocketMQTemplate;
@Value("${rocketmq.producer.send-message-timeout}") private Integer messageTimeOut;
public SendResult sendMsg(String topic, String msgBody) { return rocketMQTemplate.syncSend(topic, MessageBuilder.withPayload(msgBody).build()); }
public void sendAsyncMsg(String topic, String msgBody) { rocketMQTemplate.asyncSend(topic, MessageBuilder.withPayload(msgBody).build(), new SendCallback() { @Override public void onSuccess(SendResult sendResult) { System.out.println("异步消息发送成功:" + sendResult); } @Override public void onException(Throwable e) { System.out.println("异步消息发送失败:" + e.getMessage()); } }); }
public SendResult sendDelayMsg(String topic, String msgBody, Integer delayLevel) { return rocketMQTemplate.syncSend(topic, MessageBuilder.withPayload(msgBody).build(), messageTimeOut, delayLevel); }
public SendResult sendTagMsg(String topic, String tag, String msgBody) { String destination = topic + ":" + tag; return rocketMQTemplate.syncSend(destination, MessageBuilder.withPayload(msgBody).build()); } }
|
2.2 基础消费者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.stereotype.Component; import cn.hutool.core.util.CharsetUtil;
@Slf4j @Component @RocketMQMessageListener( topic = "base_topic", selectorExpression = "*", consumerGroup = "base_consumer_group" ) public class BaseConsumer implements RocketMQListener<MessageExt> { @Override public void onMessage(MessageExt message) { String msg = new String(message.getBody(), CharsetUtil.UTF_8); log.info("基础消费者接收消息:{}", msg); } }
|
3. 顺序消息(核心)
3.1 概念
- 顺序消息:消息严格按照发送顺序进行消费,保证 FIFO(先进先出)
- 分类:全局顺序、分区顺序(常用)
- 实现原理:相同业务 ID 的消息发送到同一个 MessageQueue
3.2 顺序消息生产者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Service;
@Service public class OrderlyProducer {
@Autowired private RocketMQTemplate rocketMQTemplate;
public SendResult sendOrderlyMsg(String topic, String msgBody, Long orderId) { return rocketMQTemplate.syncSendOrderly( topic, MessageBuilder.withPayload(msgBody).build(), orderId.toString() ); } }
|
3.3 顺序消息消费者
关键:consumeMode = ConsumeMode.ORDERLY 开启顺序消费
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.ConsumeMode; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.stereotype.Component; import cn.hutool.core.util.CharsetUtil;
@Slf4j @Component @RocketMQMessageListener( topic = "orderly_topic", consumerGroup = "orderly_consumer_group", consumeMode = ConsumeMode.ORDERLY, // 核心:开启顺序消费 selectorExpression = "*" ) public class OrderlyConsumer implements RocketMQListener<MessageExt> { @Override public void onMessage(MessageExt message) { String msg = new String(message.getBody(), CharsetUtil.UTF_8); log.info("顺序消费者接收消息:{},队列ID:{}", msg, message.getQueueId()); } }
|
4. 高级延时消息(优化版)
4.1 概念
- 延时消息:消息发送后,延迟指定时间才会被消费者消费
- 适用场景:订单超时未支付自动取消、定时通知
4.2 高级延时生产者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Service;
@Service public class DelayProducer {
@Autowired private RocketMQTemplate rocketMQTemplate;
public SendResult sendDelayMsg(String msg) { return rocketMQTemplate.syncSend( "delay_topic", MessageBuilder.withPayload(msg).build(), 3000, 5 ); } }
|
4.3 延时消息消费者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.stereotype.Component; import cn.hutool.core.util.CharsetUtil;
@Slf4j @Component @RocketMQMessageListener( topic = "delay_topic", consumerGroup = "delay_consumer_group" ) public class DelayConsumer implements RocketMQListener<MessageExt> { @Override public void onMessage(MessageExt message) { String msg = new String(message.getBody(), CharsetUtil.UTF_8); log.info("延时消费者接收消息:{}", msg); } }
|
5. 事务消息(核心)
5.1 概念
- 事务消息:保证本地事务与消息发送的原子性(最终一致性)
- 流程:半消息 → 执行本地事务 → 提交 / 回滚消息 → 消息投递
- 适用场景:订单创建 + 扣减库存 + 发送通知
5.2 事务消息生产者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import org.apache.rocketmq.client.producer.TransactionSendResult; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Service;
@Service public class TransactionProducer {
@Autowired private RocketMQTemplate rocketMQTemplate;
public TransactionSendResult sendTransactionMsg(String msg) { return rocketMQTemplate.sendMessageInTransaction( "tx_producer_group", "tx_topic", MessageBuilder.withPayload(msg).build(), null ); } }
|
5.3 事务监听器(核心)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener; import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener; import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState; import org.springframework.messaging.Message; import org.springframework.stereotype.Component;
@Slf4j @Component @RocketMQTransactionListener(producerGroup = "tx_producer_group") public class TransactionListener implements RocketMQLocalTransactionListener {
@Override public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) { try { log.info("执行本地事务成功"); return RocketMQLocalTransactionState.COMMIT; } catch (Exception e) { log.error("本地事务执行失败", e); return RocketMQLocalTransactionState.ROLLBACK; } }
@Override public RocketMQLocalTransactionState checkLocalTransaction(Message msg) { log.info("执行事务回查,状态:提交"); return RocketMQLocalTransactionState.COMMIT; } }
|
5.4 事务消息消费者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.stereotype.Component; import cn.hutool.core.util.CharsetUtil;
@Slf4j @Component @RocketMQMessageListener( topic = "tx_topic", consumerGroup = "tx_consumer_group" ) public class TransactionConsumer implements RocketMQListener<MessageExt> { @Override public void onMessage(MessageExt message) { String msg = new String(message.getBody(), CharsetUtil.UTF_8); log.info("事务消费者接收消息:{}", msg); } }
|
6. 死信队列(核心)
6.1 概念
- 死信消息:消费失败达到最大重试次数后,消息进入死信队列
- 死信队列:专门存储死信消息的队列,格式:
%DLQ%消费者组名
- 适用场景:异常消息兜底、人工排查处理
6.2 死信触发消费者(测试用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.stereotype.Component; import cn.hutool.core.util.CharsetUtil;
@Slf4j @Component @RocketMQMessageListener( topic = "normal_topic", consumerGroup = "normal_consumer_group", maxReconsumeTimes = 3 // 最大重试次数,超过进入死信 ) public class NormalConsumer implements RocketMQListener<MessageExt> { @Override public void onMessage(MessageExt message) { String msg = new String(message.getBody(), CharsetUtil.UTF_8); log.info("开始消费消息:{}", msg); throw new RuntimeException("消费异常,消息进入死信队列"); } }
|
6.3 死信队列消费者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.stereotype.Component; import cn.hutool.core.util.CharsetUtil;
@Slf4j @Component @RocketMQMessageListener( // 死信队列格式:%DLQ%消费者组名 topic = "%DLQ%normal_consumer_group", consumerGroup = "dlq_consumer_group" ) public class DLQConsumer implements RocketMQListener<MessageExt> { @Override public void onMessage(MessageExt message) { String msg = new String(message.getBody(), CharsetUtil.UTF_8); log.error("=====死信队列接收消息====="); log.error("死信消息内容:{}", msg); log.error("死信消息ID:{}", message.getMsgId()); } }
|
7. 统一测试接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.TransactionSendResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;
@RestController public class RocketMQTestController {
@Autowired private BaseRocketMQProducer baseProducer;
@Autowired private OrderlyProducer orderlyProducer;
@Autowired private DelayProducer delayProducer;
@Autowired private TransactionProducer transactionProducer;
@GetMapping("/send/base") public SendResult baseMsg(String msg) { return baseProducer.sendMsg("base_topic", msg); }
@GetMapping("/send/orderly") public SendResult orderlyMsg(String msg, Long orderId) { return orderlyProducer.sendOrderlyMsg("orderly_topic", msg, orderId); }
@GetMapping("/send/delay") public SendResult delayMsg(String msg) { return delayProducer.sendDelayMsg(msg); }
@GetMapping("/send/transaction") public TransactionSendResult transactionMsg(String msg) { return transactionProducer.sendTransactionMsg(msg); }
@GetMapping("/send/dlq") public SendResult dlqMsg(String msg) { return baseProducer.sendMsg("normal_topic", msg); } }
|
8. 核心知识点总结
- 顺序消息:
syncSendOrderly + ConsumeMode.ORDERLY,依靠 hashKey 保证队列唯一
- 延时消息:通过延时等级实现定时投递,共 18 个固定等级
- 事务消息:保证本地事务与消息原子性,包含半消息、事务执行、事务回查
- 死信队列:消费重试失败后进入,用于异常消息兜底处理
- 核心注解
@RocketMQMessageListener:消费者监听
@RocketMQTransactionListener:事务监听
RocketMQTemplate:消息发送核心工具类
六.总结
1.重点内容
- MQ使用场景
- RocketMQ的架构
- RocketMQ消息(普通,延迟,事务)
- SpringBoot整合RocketMQ
2.面试必备
- MQ有哪些使用场景
- 说下一下RokcetMQ的架构
- RocketMQ的数据是存储到内存还是硬盘,如果是硬盘那么怎么保证速度
- 发送消息有几种方式
- Consumer消息的拉取模式有哪两种
- 如何保证消息的顺序
- 延迟消息有用过吗?怎么用?可以用在什么业务场景
- 事务消息的使用场景,举例说明