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的使用场景(重要)
- 限流削峰
MQ可以将系统的超量请求暂存其中,以便系统后期可以慢慢进行处理,从而避免了请求的丢失或系统 被压垮。
-
异步&解耦
上游系统对下游系统的调用若为同步调用,则会大大降低系统的吞吐量与并发度,且系统耦合度太高。 而异步调用则会解决这些问题。所以两层之间若要实现由同步到异步的转化,一般性做法就是,在这两层间添加一个MQ层。 即使消费者挂掉也不影响生产者工作,只要把消息放入队列即可,消费者重启后自己消费即可。
- 数据收集
分布式系统会产生海量级数据流,如:业务日志、监控数据、用户行为等。针对这些数据流进行实时或 批量采集汇总,然后对这些数据流进行大数据分析,这是当前互联网平台的必备技术。通过MQ完成此 类数据收集是最好的选择。
-
大数据处理
比如我们的平台向“三方平台”获取数据,一次请求了大量数据回来要进行处理,由于数据较多处理不过来,那么就可以放入MQ,再创建一些消费者进行数据处理即可。
【注意
】如下情况不太适合
MQ
- 小项目,体量不大,并发量低的使用MQ会太过笨重 - 你可以考虑使用Redis做一个消息队列。
- 对数据的一致性有要求(强一致性)的的场景不适合使用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是在协议层定义的标准,是跨语言的 。 - 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特征(了解)
-
支持集群模型、负载均衡、水平扩展能力
-
亿级别消息堆积能力
-
采用零拷贝的原理,顺序写盘,随机读
-
底层通信框架采用Netty NIO
-
NameServer代替Zookeeper,实现服务寻址和服务协调
-
消息失败重试机制、消息可查询
-
强调集群无单点,可扩展,任意一点高可用,水平可扩展
-
经过多次双十一的考验
二.RocketMQ的安装
1.RocketMQ安装
1.1.下载RocketMQ
下载地址:http://rocketmq.apache.org/release_notes/release-notes-4.2.0/
1.2.配置ROCKETMQ_HOME
解压压缩包,配置 ROCKETMQ_HOME
1.3.启动MQ
- 启动NameServer
Cmd命令框执行进入至MQ文件夹\bin
下,然后执行 start mqnamesrv.cmd
,启动NameServer。(注册中心)
成功后会弹出提示框,此框勿关闭。
- 启动Broker(MQ)
进入至MQ文件夹\bin
下,修改Bean目录下的 runbroker.cmd
中JVM占用内存大小
CMD执行start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true
,启动Broker。
成功后会弹出提示框,此框勿关闭
2.RocketMQ插件
2.1.下载插件
RocketMQ可视化管理插件下载地址:https://github.com/apache/rocketmq-externals/releases
2.2.修改配置
解压后,修改配置:src/main/resource/application.properties ,这里需要指向Name Server 的地址和端口 如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D5hebuLp-1691293663279)(课件图片/1634807425286.png)]
2.3.打包插件
回到安装目录(pom.xml所在目录),执行: mvn clean package -Dmaven.test.skip=true
,然后会在target目录生成打包后的jar文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jTGACwlb-1691293663279)(课件图片/1634807473014.png)]
2.4.启动插件
进入 target 目录,执行 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主要由 Producer、Broker、Consumer、NameServer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器。
为了实现高可用,Broker本身是主备架构,Master负责写请求和读请求,Slave负责数据备份和分担读请求。这样的好处是提高了吞吐量同时防止数据丢失。
1.1.Producer
消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
1.2.Consumer
消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费
。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。
1.3.Broker
Broker主要负责消息的存储、投递和查询以及服务高可用保证。
1.4.NameServer
NameServer是一个Broker与Topic路由的注册中心支持Broker的动态注册与发现主要包括两个功能
-
Broker管理
NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供
心跳检测机制
,检查Broker是否还存活。 -
路由信息管理
每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费
2.RocketMQ 核心概念
2.1.RabbitMQ工作原理
2.2.RabbitMQ工作流程(重要)
-
启动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建立连接通道,开始消费消息
2.3.Producer 生产者
RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送
。同步和异步方式均需要Broker返回确认信息,单向发送不需要。
RocketMQ中的消息生产者都是以生产者组(Producer Group)的形式出现的。生产者组是同一类生产者的集合,这类Producer发送相同Topic类型的消息。一个生产者组可以同时发送多个主题的消息。
Producer会使用一定的算法选择把消息发送到哪个master的某个queue中。
2.4.Consumer 消费者
Consumer 支持两种消费形式:拉取式消费、推动式消费
。(主动,被动),RocketMQ中的消息消费者都是以消费者组(Consumer Group)的形式出现的。消费者组是同一类消费者的集合,这类Consumer消费的是同一个Topic类型的消息,不同的 Consumer Group可以消费同一个Topic。
一个Consumer Group内的Consumer可以消费多个Topic的消息。
[注意] 一个Queue是不能被同一个ConsumerGroup中的多个Consumer消费的
,目的是减少资源竞争提升整体性能。
2.5.Topic 消息主题
Topic表示一类消息的集合,每个topic主题包含若干条message消息,
每条message消息只能属于一个topic主题,Topic是RocketMQ进行消息订阅的基本单位。
brocker -> topic -> queue -> messgae
tags : 是对消息的标签
2.6.Message
消息是指,消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。
2.7.Tag 标签
为消息设置的标志,用于同一主题下区分不同类型的消息
。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。
标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。Topic是消息的一级分类,Tag是消息的二级分类
2.8.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。
3.8.消息拉取模式(掌握)
消息的消费分为:拉取式 pull ,和推送是 push
-
Pull:拉取式,需要消费者间隔一定时间就去遍历关联的Queue,实时性差但是便于应用控制消息的拉取
-
Push:默认推送式,封装了Queue的遍历,实时性强,但是对系统资源占用比较多。
3.9.消息消费模式(掌握)
消息的消费模式有广播模式和集群模式
-
广播模式:
同一个Consumer Group 下的所有Consumer都会受到同一个Topic的所有消息。同一个消息可能会被消费多次
。 -
集群模式(默认):同一个Gonsumer Group 下的Consumer平分同一个Topic下的消息。
同一个消息只是被消费一次
。
3.10.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。
四.RocketMQ的使用
官网:https://github.com/apache/rocketmq/tree/master/docs/cn
1.环境搭建
1.1.导入依赖
<parent>
<groupId> org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<!-- <version>2.0.4</version> -->
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
1.2.启动类
@SpringBootApplication
public class ApplicationStart {
public static void main(String[] args) {
SpringApplication.run(ApplicationStart.class);
}
}
1.3.配置文件
rocketmq:
name-server: 127.0.0.1:9876
#生产者配置
producer:
#生产者组名字
group: "service-producer"
# 消息最大长度 默认 1024 * 1024 * 4 (4M)
max-message-size: 4194304
# 发送消息超时时间,默认 3000
send-message-timeout: 3000
# 发送消息失败重试次数,默认2
retry-times-when-send-failed: 2
# 异步消息发送失败重试次数
retry-times-when-send-async-failed: 2
#达到 4096 ,进行消息压缩
compress-message-body-threshold: 4096
consumer:
#消费者名字
group: "service-consumer"
#批量拉取消息数量
pull-batch-size: 10
message-model: CLUSTERING
selector-expression: "*"
2.发送消息[重要]
我们使用的是SpringBoot和RocketMQ整合的方式进行消息发送,MQ提供了RocketMQTemplate来发送消息
2.1.同步发送
同步消息是发送者发送消息,需要等待结果的返回,才能继续发送第二条消息,这是一种阻塞式模型,虽然消息可靠性高,但是阻塞导致性能低
。
- API : void send(D destination, Message<?> message) :发送普通消息,destination是目的地,在SpringBoot中使用“topic:tags”方式拼接。,send方法模式使用的是 syncSend 同步发送方式。
@RunWith(SpringRunner.class)
@SpringBootTest(classes = KillApp.class)
public class RocketMQTest {
@Autowired
private RocketMQTemplate mqTemplate;
public void sendMessage(){
Message<String> message = MessageBuilder.withPayload("我是一个字符串消息").build();
mqTemplate.send("topic-test:tags-test",message);
System.out.println("完成消息发送...");
}
}
除了使用send发送同步消息,还可以使用synSend,该方法更加灵活,API如下:
- SendResult syncSend(String destination, Message<?> message, long timeout) :可以指定超时时间
- SendResult syncSend(String destination, Object payload, long timeout) :MQ底层自动把payload对象转为Message,然后进行同步发送
public void syncSendMessage(){
Message<String> message = MessageBuilder.withPayload("我是一个字符串消息").build();
//发送同步消息,2s发送不成功就超时
SendResult sendResult = mqTemplate.syncSend("topic-test:tags-test", message, 2000);
System.out.println(sendResult);
//判断发送结果状态
if(sendResult.getSendStatus() == SendStatus.SEND_OK){
System.out.println("发送成功");
}else{
System.out.println("发送失败");
}
}
这个 syncSend方法是可以拿到发送结果SendResult,通过他我们可以判断消息是否发送成功SendResult 结果如下
SendResult [
sendStatus=SEND_OK, msgId=C0A8006516B018B4AAC270EF9D940000,offsetMsgId=C0A8006500002A9F0000000000008E1C,
messageQueue=MessageQueue [topic=topic-test, brokerName=LAPTOP-20VLGCRC, queueId=3], queueOffset=0]
-
SendStatus : 发送的状态OK
-
msgId: 发送者自动生成的ID
-
OffsetMsgId : 由Broker生成的消息ID
-
MessageQueue :队列信息
2.2.异步发送
异步消息是发送者发送消息,无需等待发送结果就可以再发送第二条消息,它是通过回调的方式来获取到消息的发送结果,消息可靠性高,性能也高。
- API : public void asyncSend(String destination, Message<?> message, SendCallback sendCallback)
public void asyncSendMessage(){
Message<String> message = MessageBuilder.withPayload("我是一个字符串消息").build();
mqTemplate.asyncSend("topic-test:tags-test", message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
System.out.println("发送成功");
}
@Override
public void onException(Throwable e) {
System.out.println("发送失败");
e.printStackTrace();
}
});
}
SendCallback 是消息发送结果回调。如果:sendResult.getSendStatus() == SendStatus.SEND_OK 表示成功
2.3.单向发送
这种方式指的是发送者发送消息后无需等待Broker的结果返回,Broker也不会返回结果,该方式性能最高,但是消息可靠性低。
- API : public void sendOneWay(String destination, Message<?> message)
public void sendOneWayMessage(){
Message<String> message = MessageBuilder.withPayload("我是一个字符串消息").build();
mqTemplate.sendOneWay("topic-test:tags-test",message);
System.out.println("完成消息发送...");
}
sendOneway 单向发送是没有返回结果值的。
2.4.消费者
MQ提供消息监听器:RocketMQListener,他会负责监听MQ中的消息从而进行消费
@Component
@RocketMQMessageListener(
//消费者的名字
consumerGroup = "consumer-group-xxx",
//主题
topic = "topic-test",
//tags
selectorExpression = "tags-test",
//消息消费模式:默认是CLUSTERING集群,还支持BROADCASTING广播
messageModel = MessageModel.CLUSTERING)
//MessageExt:Message对象的子类
public class TestConsumer implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
//这里拿到的消息 message.getBody 是byte[]格式。
if(message.getBody() == null ||message.getBody().length == 0)return;
//拿到消息:如果发送的消息是字符串,那么需要把byte[]转为字符串
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
}
}
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单个存储单元组成结构如下
-
Commit Log Offset:记录在CommitLog中的位置。
-
Size:记录消息的大小
-
Message Tag HashCode:记录消息Tag的哈希值,用于消息过滤。特别的,对于延迟消息,这个字段记录的是消息的投递时间戳。
-
将消息索引写入到SCHEDULE_TOPIC_XXXX主题下相应的consumequeue中
-
Broker内部有⼀个延迟消息服务类ScheuleMessageService,根据延迟级别数,创建对应数量的定时器Timer,定时消费SCHEDULE_TOPIC_XXXX中的消息,并投递到目标Topic中。
-
在将消息到期后,队列的Level等级改为0,作为一条普通消息,投递到目标Topic。
3.2.延迟消息实战
在SpringBoot中发送RocketMQ延迟消息只需要设置一个延迟等级即可
- api : public SendResult syncSend(String destination, Message<?> message, long timeout, int delayLevel) : delayLevel就是延迟等级
SendResult sendResult = mqTemplate.syncSend("topic-test:tags-test", message, 2000, 3);//延迟消息
log.info("发送延迟消息{}",sendResult);
这里设置的等级是 3,对应的是 10S,也就是10s之后,消费者就可以收到该消息了。
4.事务消息
4.1.事务消息概述
如果业务只涉及到一个数据库的写操作,我们只需要保证这一个事物的提交和回滚,这种事务管理叫传统事物或本地事务,如果业务涉及到多个数据库(多个服务)的写操作,我们需要保证多个数据库同时提交或回滚,这种夸多个数据库的事务操作叫分布式事务。
分布式事物的解决方案有很多,如:2PC,TCC,最终一致性,最大努力通知等等。这里要介绍的是基于RocketMQ事务消息的最终一致性方案,下面举个例子。
用户注册成功,向用户数据库保存用户信息,同时通过远程调用积分服务为用户赠送积分,模型如下:
我们需要使用分布式事务管理实现用户数据库和积分数据库的一致性。即:用户保存成功,用户的积分也要保存成功,或者都回滚不做任何存储。这种业务场景可以选择2PC强一致性方案,也可以选择最终一致性。我们选择最终一致性,因为用户注册成功,不要求马上赠送积分,延迟一定时间后再赠送成功也是允许的。所以有了如下模型
事务流程
-
用户服务(事务发起方)往MQ中发送一个事务消息,
-
MQ返回结果是否发送成功
-
用户服务受到消息发送成功结果,保存用户数据,提交本地事务
-
积分服务拿到MQ中的事务消息
-
积分服务保存积分到数据库
4.2.事务消息原理
事务流程中的最大的难点就是如何保证事务消息发送和本地事务的原子性,即:第一步和第二步要么都成功,要么都失败,不能说消息发送成功了,结果用户保存失败了,那么积分服务可能会增加成功,就导致数据不一致。RocketMQ已经帮我们处理好这个问题。它的工作原理如下[理解]:
4.3.事务消息实战
我们需要做什么
-
编写本地事务检查监听TransactionListener ,一是执行本地事务逻辑,二是返回本地事务执行状态
-
发消息时生产者需要设置producer.setTransactionListener 事务监听
这里我们还是使用用户和积分来举例:
1.事务监听器
//:事务监听器
@Slf4j
@RocketMQTransactionListener(txProducerGroup = "tx-producer-group")
public class CourserOrderTransactionListener implements RocketMQLocalTransactionListener {
//执行本地事务的方法
//msg: 消息对象 :
//arg : 扩展参数
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
//执行本地事务方法,就是做数据库
return RocketMQLocalTransactionState.COMMIT;
}
//检查本地事务的方法
//msg:消息对象
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
//判断本地事务是否执行成功,就是去数据库查询是否有 executeLocalTransaction 方法执行之后的结果
return RocketMQLocalTransactionState.COMMIT;
}
}
解释:
- RocketMQLocalTransactionListener :MQ提供的事务消息监听器的接口
- @RocketMQTransactionListener(txProducerGroup = “”) : 为监听器指定组名
- executeLocalTransaction(Message msg, Object arg):执行本地事务方法,msg:消息的内容,arg扩展参数
- checkLocalTransaction :检查本地事务,也就是检查executeLocalTransaction方法是否执行成功
2.消息生产者
TransactionSendResult result = mqTemplate.sendMessageInTransaction(
"tx-producer-group",//事务监听器指定的名字
"topic-test:tags-test", //目的地:topic:tags
message, //发给MQ的消息内容 Message对象
arg);//扩展参数 Object; 该参数会传递给事务监听器的arg
log.info("事务消息发送结果 {} , {}",result.getLocalTransactionState() , result.getSendStatus() );