一、什么是MQ?
1.同步调用
同步调用的基本概念
同步调用是指在调用一个方法或函数时,调用者会等待该方法或函数执行完毕并返回结果后,才继续执行后续的代码。同步调用的特点是执行顺序明确,但在等待过程中可能会阻塞调用者的线程,导致效率降低。
选择同步调用的场景
- 任务顺序重要:如果任务必须按顺序执行,且后续任务依赖前一个任务的结果,那么同步调用是合适的选择。
- 短时间任务:如果任务执行时间很短,不会导致明显的延迟或阻塞,那么可以使用同步调用。
- 简单逻辑:同步调用的代码逻辑简单,易于理解和维护,适用于不需要并发处理的场景。
同步调用的优缺点
同步调用的优势是什么?
- 时效性强,等待到结果后才返回。
同步调用的问题是什么?
- 拓展性差
- 性能下降
- 级联失败问题
2.异步调用
异步调用的基本概念
异步调用是指在调用一个方法或函数时,调用者不会等待该方法或函数执行完毕,而是立即继续执行后续的代码。异步调用通常通过回调函数、Promise 或 Future 等机制来处理结果。异步调用的特点是可以提高并发性和效率,但代码逻辑可能会变得复杂。
选择异步调用的场景
- 长时间任务:如果任务执行时间较长,可能会导致阻塞,那么异步调用可以提高系统的响应速度和并发性。
- 并发处理:如果需要同时处理多个任务,异步调用可以提高系统的并发处理能力。
- 用户体验:在前端开发中,异步调用可以避免界面卡顿,提供更好的用户体验。例如,发送网络请求时使用异步调用,用户可以继续操作界面,而不必等待请求完成。
异步调用的优缺点
异步调用的优势是什么?
- 耦合度低,拓展性强
- 异步调用,无需等待,性能好
- 故障隔离,下游服务故障不影响上游业务
- 缓存消息,流量削峰填谷
异步调用的问题是什么?
- 不能立即得到调用结果,时效性差
- 不确定下游业务执行是否成功
- 业务安全依赖于Broker的可靠性
3.MQ技术选型
异步调用通常是基于消息通知的方式,包含三个角色:
- 消息发送者:投递消息的人,就是原来的调用者
- 消息接收者:接收和处理消息的人,就是原来的服务提供者
- 消息代理:管理、暂存、转发消息,你可以把它理解成微信服务器
MQ(MessageQueue),即消息队列,也就是异步调用中的Broker。消息队列(MQ)技术选型是一个复杂的过程,需要考虑多个因素,包括性能、可用性、功能特性、社区支持等。以下是几种常见的消息队列技术及其特点:
Kafka
- 优点:
- 高吞吐量:适用于大数据处理场景,如实时日志收集、流式数据处理等。
- 持久化存储:将消息持久化到磁盘,确保数据可靠。
- 分布式架构:支持水平扩展,具备良好的容错能力。
- 消息顺序保证:在分区级别保证消息的有序性。
- 缺点:
- 配置和管理复杂:涉及分区、副本、消费者组等概念。
- 强依赖 ZooKeeper:增加了系统的外部依赖和运维成本。
RabbitMQ
- 优点:
- 灵活的路由模型:提供丰富的交换机类型,支持复杂的路由规则。
- 高可用性:通过主从复制实现高可用集群。
- 广泛的语言支持:提供多种客户端库,几乎覆盖所有主流编程语言。
- 缺点:
- 吞吐量和延迟:相较于 Kafka 和 RocketMQ,性能稍逊。
- 资源消耗:在集群环境中资源消耗较大。
- 集群管理复杂:配置与维护相对繁琐。
RocketMQ
- 优点:
- 高性能与低延迟:适合金融、电商等对性能要求严苛的场景。
- 分布式事务支持:确保消息发送与业务操作的一致性。
- 阿里巴巴背书:经过大规模生产环境验证。
- 缺点:
- 社区活跃度:相较于 Kafka,社区活跃度略逊。
- 学习曲线:部分高级特性的理解和使用需要一定的学习和实践经验。
ActiveMQ
- 优点:
- 成熟稳定:历史悠久,社区成熟,稳定性良好。
- 协议丰富:支持多种消息协议,易于与其他系统集成。
- 轻量级:适合小型项目或对资源敏感的场景。
- 缺点:
- 性能瓶颈:单机吞吐量较低,不适合大规模消息处理。
- 可靠性问题:在高并发或网络不稳定环境下,存在数据丢失风险。
- 管理工具不足:原生管理工具功能较为简单。
选择指南
- 性能需求:如对吞吐量、延迟有极高要求,优先考虑 Kafka 和 RocketMQ;对性能要求适中,RabbitMQ 是不错的选择;对资源有限的小型项目,ActiveMQ 可能是最轻量的解决方案。
- 消息语义:如需严格的消息顺序保证、事务支持,RocketMQ 更胜一筹;如需灵活的路由规则,RabbitMQ 更适合。
- 生态与集成:考量现有系统使用的语言、框架及已有中间件的兼容性,以及社区支持、插件丰富度等因素。
- 运维复杂度:对于运维团队实力较强、愿意投入精力管理复杂系统的组织,可以选择 Kafka 或 RocketMQ;反之,若希望简化运维,RabbitMQ 或 ActiveMQ 可能是更优选择。
二、RabbitMQ的使用
1.基于Docker安装RabbitMQ
使用docker load命令加载资料中的镜像。
使用下面的docker run命令部署rabbitmq的容器。
docker run \
-e RABBITMQ_DEFAULT_USER=itheima \
-e RABBITMQ_DEFAULT_PASS=123321 \
-v mq-plugins:/plugins \
--name mq \
--hostname mq \
-p 15672:15672 \
-p 5672:5672 \
--network hmall\
-d \
rabbitmq:3.8-management
这段代码是用来运行一个 RabbitMQ 容器的 Docker 命令。以下是对每个参数的详细解释:
docker run:这是 Docker 命令,用于运行一个新的容器。-e RABBITMQ_DEFAULT_USER=itheima:设置环境变量,指定 RabbitMQ 的默认用户名为itheima。-e RABBITMQ_DEFAULT_PASS=123321:设置环境变量,指定 RabbitMQ 的默认密码为123321。-v mq-plugins:/plugins:将主机上的mq-plugins目录挂载到容器内的/plugins目录,用于存储插件。--name mq:为容器指定一个名称mq。--hostname mq:为容器指定一个主机名mq。-p 15672:15672:将主机的 15672 端口映射到容器的 15672 端口,用于访问 RabbitMQ 管理界面。-p 5672:5672:将主机的 5672 端口映射到容器的 5672 端口,用于 RabbitMQ 的消息传输。--network hmall:将容器连接到名为hmall的 Docker 网络。-d:以守护进程模式运行容器,即在后台运行。rabbitmq:3.8-management:指定使用rabbitmq:3.8-management镜像来创建容器,这个镜像包含了 RabbitMQ 3.8 版本及其管理插件。
这段命令会启动一个配置好的 RabbitMQ 容器,并使其在后台运行。
使用docker logs命令进去mq容器的日志目录。
根据日志信息,可知rabbitmq容器成功部署并启动了相关服务。
回到windows主机,打开浏览器,访问虚拟机IP:15672。使用部署容器时配置的用户名itheima和密码123321登录RabbitMQ控制台界面。
登录后即可看到管理控制台总览页面。
2.快速入门
RabbitMQ的整体架构及核心概念:
- publisher:生产者,也就是发送消息的一方
- consumer:消费者,也就是消费消息的一方
- queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理
- exchange:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。
- virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue
入门案例:在RabbitMQ的控制台完成下列操作:
- 新建队列hello.queue1和hello.queue2
- 向默认的amp.fanout交换机发送一条消息
- 查看消息是否到达hello.queue1和hello.queue2
- 总结规律
新建队列hello.queue1和hello.queue2。
!
利用控制台中的publish message 发送一条消息。这里是由控制台模拟了生产者发送的消息。由于没有消费者存在,最终消息丢失了,这样说明交换机没有存储消息的能力。交换机只负责转发消息,不存储消息。所以路由失败,消息会丢失。
发送到交换机的消息,只会路由到与其绑定的队列,因此仅仅创建队列是不够的,我们还需要将其与交换机绑定。因此在amq.fanout的binding设置中绑定队列hello.queue1和hello.queue2。
在队列详情中可以看到绑定的交换机。
再次回到exchange页面,找到刚刚绑定的amq.fanout,点击进入详情页,再次发送一条消息。
回到Queues页面,可以发现两个hello.queue中已经有一条消息了。
点击队列名称,进入详情页,查看队列详情,这次我们点击get message,可以看到消息到达队列了。这个时候如果有消费者监听了MQ的hello.queue1或hello.queue2队列,自然就能接收到消息了。
总结:消息发送的注意事项有哪些?
- 交换机只能路由消息,无法存储消息
- 交换机只会路由消息给与其绑定的队列,因此队列必须与交换机绑定
3.数据隔离
学习案例:在RabbitMQ的控制台完成下列操作:
- 新建一个用户hmall
- 为hmall用户创建一个virtual host
- 测试不同virtual host之间的数据隔离现象
点击Admin选项卡,首先会看到RabbitMQ控制台的用户管理界面: 这里的用户都是RabbitMQ的管理或运维人员。目前只有安装RabbitMQ时添加的itheima这个用户。仔细观察用户表格中的字段,如下:
- Name:itheima,也就是用户名
- Tags:administrator,说明itheima用户是超级管理员,拥有所有权限
- Can access virtual host: /,可以访问的virtual host,这里的/是默认的virtual host
对于小型企业而言,出于成本考虑,我们通常只会搭建一套MQ集群,公司内的多个不同项目同时使用。这个时候为了避免互相干扰, 我们会利用virtual host的隔离特性,将不同项目隔离。一般会做两件事情:
- 给每个项目创建独立的运维账号,将管理权限分离。
- 给每个项目创建不同的virtual host,将每个项目的数据隔离。
我们给黑马商城创建一个新的用户,命名为hmall。
此时hmall用户没有任何virtual host的访问权限。
先退出登录,切换到刚刚创建的hmall用户登录。
此时hmall用户没有访问权限,需要为hmall用户创建一个virtual host。
点击Virtual Hosts菜单,进入virtual host管理页。可以看到目前只有一个默认的virtual host,名字为 /。给黑马商城项目创建一个单独的virtual host,名字为“/hmall”。
创建完成后如图。
在Exchanges页面观察,这些交换机属于不同虚拟主机,相互隔离开。
切换虚拟主机为“/hmall”,会发现属于“/”的交换机不见了,这就是基于virtual host 的隔离效果。
再次查看queues选项卡,会发现之前的队列已经看不到了。
添加simple.queue。
向队列中发送消息“洛天依”。
从队列中取出消息。
综上即为virtual host的数据隔离效果。
三、RabbitMQ的Java客户端
1.快速入门
SpringAMQP
将来我们开发业务功能的时候,肯定不会在控制台收发消息,而是应该基于编程的方式。由于RabbitMQ采用了AMQP协议,因此它具备跨语言的特性。任何语言只要遵循AMQP协议收发消息,都可以与RabbitMQ交互。并且RabbitMQ官方也提供了各种不同语言的客户端。但是,RabbitMQ官方提供的Java客户端编码相对复杂,一般生产环境下我们更多会结合Spring来使用。而Spring的官方刚好基于RabbitMQ提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配,使用起来非常方便。
SpringAMQP提供了三个功能:
- 自动声明队列、交换机及其绑定关系
- 基于注解的监听器模式,异步接收消息
- 封装了RabbitTemplate工具,用于发送消息
导入课前资料提供的Demo工程
包括三部分:
- mq-demo:父工程,管理项目依赖
- publisher:消息的发送者
- consumer:消息的消费者
在mq-demo这个父工程中,已经配置好了SpringAMQP相关的依赖,因此,子工程中就可以直接使用SpringAMQP了。
入门案例
在之前的案例中,我们都是经过交换机发送消息到队列,不过有时候为了测试方便,我们也可以直接向队列发送消息,跳过交换机。即
- publisher直接发送消息到队列
- 消费者监听并处理队列中的消息
入门案例需求
- 利用控制台创建队列simple.queue
- 在publisher服务中,利用SpringAMQP直接向simple.queue发送消息
- 在consumer服务中,利用SpringAMQP编写消费者,监听simple.queue队列
(1) 引入spring-boot-starter-amqp依赖
在父工程中引入spring-amqp依赖,这样publisher和consumer服务都可以使用:
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
(2) 配置rabbitmq服务端信息
在每个微服务中引入MQ服务端信息,这样微服务才能连接到RabbitMQ
spring:
rabbitmq:
host: 192.168.239.146 # 你的虚拟机IP
port: 5672 # 端口
virtual-host: /hmall # 虚拟主机
username: hmall # 用户名
password: 123 # 密码
(3) 利用RabbitTemplate发送消息
SpringAMQP提供了RabbitTemplate工具类,方便我们发送消息。
之前在控制台已为/hamll虚拟主机创建过队列simple.queue。
在publisher服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送。
打开控制台,可以看到消息已经发送到队列中。
(4) 利用@RabbitListener注解声明要监听的队列,监听消息
SpringAMQP提供声明式的消息监听,我们只需要通过注解在方法上声明要监听的队列名称,将来SpringAMQP就会把消息传递给当前方法:
利用RabbitListener来声明要监听的队列信息,将来一旦监听的队列中有了消息,就会推送给当前服务,调用当前方法,处理消息。可以看到方法体中接收的就是消息体的内容。
这个错误通常是由于升级到 Java 21 后,Lombok 等库无法正确访问内部的 Java 编译器 API 导致的。具体原因如下:
Lombok 在早期版本中使用反射访问 com.sun.tools.javac.tree.JCTree$JCImport 类的 qualid 字段,该字段在 Java 21 中的类型发生了变化。在 Java 21 及更高版本中,qualid 字段的类型从 JCTree 变更为 JCFieldAccess。这导致了 Lombok 无法正确访问该字段,从而抛出 NoSuchFieldError 异常。
解决方案 要解决这个问题,可以采取以下步骤:
-
升级 Lombok 版本:将 Lombok 库升级到 1.18.30 或更高版本。该版本已经修复了这个问题。
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.32</version> <scope>provided</scope> </dependency> -
更新 Maven 编译插件:如果使用 Maven,还需要在
maven-compiler-plugin中指定 Lombok 的版本。<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.32</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build> -
重新加载项目:在 IntelliJ IDEA 中,重新加载 Maven 项目,确保使用最新的 Lombok 版本。如果问题仍然存在,尝试删除本地 Maven 仓库中的 Lombok 缓存,并重新构建项目。
因此需在mq-demo的pom.xml文件中增加Maven 编译插件,并在 maven-compiler-plugin 中指定 Lombok 的版本。
重启消费者,成功监听到了来自simple.queue的消息。
总结:SpringAMQP如何收发消息?
- 引入spring-boot-starter-amqp依赖
- 配置rabbitmq服务端信息
- 利用RabbitTemplate发送消息
- 利用@RabbitListener注解声明要监听的队列,监听消息
2.Work Queues
Work queues,任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work 模型,多个消费者共同处理消息处理,消息处理的速度就能大大提高了。
入门案例:模拟WorkQueue,实现一个队列绑定多个消费者
(1) 在RabbitMQ的控制台创建一个队列,名为work.queue
(2) 在publisher服务中定义测试方法,用于发送50条消息到work.queue
(3) 在consumer服务中定义两个消息监听者,都监听work.queue队列
运行testWorkQueue测试方法,启动ConsumerApplication,观察日志监听结果。可以看到消费者1处理一半的奇数,消费者2处理一半的偶数。
结论:
- 队列中的同一个消息只会被一个消费者处理
- 消息被均匀分配
作用:work.queue可以提高处理消息的速度
(4) 消费者1每秒处理40条消息,消费者2每秒处理5条消息
修改代码,在监听方法中添加线程休眠的逻辑,从而设置消费者处理消息的速度。使得消费者1每监听处理一条消息就休眠25ms,即1s处理40条消息。消费者2每监听处理一条消息就休眠200ms,即1s处理5条消息。
从监听日志看消费者1处理的快,在28s这1s内便很快处理完25条消息,而消费者2处理的慢,从28s处理到32s还没有处理完。总耗时至少5s,这是因为消息并不按照两个消费者处理消息的快慢来分配,而是各自均匀分配25条,并没有考虑到消费者的处理能力。导致1个消费者空闲,另一个消费者忙的不可开交。没有充分利用每一个消费者的能力,最终消息处理的耗时远远超过了1秒。这样显然是有问题的。
(5) 修改策略:消费者消息推送限制
默认情况下,RabbitMQ的会将消息依次轮询投递给绑定在队列上的每一个消费者。但这并没有考虑到消费者是否已经处理完消息,可能出现消息堆积。因此我们需要修改application.yml,设置preFetch值为1,确保同一时刻最多投递给消费者1条消息:
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
再次测试,观察监听结果。可以发现,由于消费者1处理速度较快,所以处理了更多的消息;消费者2处理速度较慢,只处理了几条消息。而最终总的执行耗时也在1秒左右,大大提升。处理的越快的处理的越多,实现了能者多劳。这样充分利用了每一个消费者的处理能力,可以有效避免消息积压问题。
总结:Work模型的使用
- 多个消费者绑定到一个队列,可以加快消息处理速度
- 同一条消息只会被一个消费者处理
- 通过设置prefetch来控制消费者预取的消息数量,处理完一条再处理下一条,实现能者多劳
3.交换机类型
消息发送模型
- Publisher:生产者,不再发送消息到队列中,而是发给交换机
- Exchange:交换机,一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
- Queue:消息队列也与以前一样,接收消息、缓存消息。不过队列一定要与交换机绑定。
- Consumer:消费者,与以前一样,订阅队列,没有变化
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
交换机的类型有四种
- Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是Fanout交换机
- Direct:订阅,基于RoutingKey(路由key)发送给订阅了消息的队列
- Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符
- Headers:头匹配,基于MQ的消息头匹配,用的较少。
4.Fanout交换机
Fanout Exchange 会将接收到的消息路由到每一个跟其绑定的queue,所以也叫广播模式。有了fanout交换机,可以给每个微服务创建自己的队列,每个微服务作为一个消费者绑定到自己的队列。发消息时,只需发一次消息给交换机,交换机会复制消息到每一个微服务的队列,使得每个微服务的消费者代码都可以处理各自业务。
利用SpringAMQP演示FanoutExchange的使用
(1) 在RabbitMQ控制台中,声明队列fanout.queue1和fanout.queue2
(2) 在RabbitMQ控制台中,声明交换机hmall.fanout,将两个队列与其绑定
创建hmall.fanout交换机。
将fanout.queue1和fanout.queue2与hmall.fanout绑定。
(3) 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
(4) 在publisher中编写测试方法,向hmall.fanout发送消息
观察监听日志,发现发给交换机的消息,被分别投递到绑定的两个队列,进而分别投递到消费者1和消费者2。
总结
- 交换机的作用是什么?
- 接收publisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- 不能缓存消息,路由失败,消息丢失
- FanoutExchange会将消息路由到每个绑定的队列
- 发送消息到交换机的API是怎样的?
@Test
public void testFanoutExchange() {
// 1.交换机名
String exchangeName = "hmall.fanout";
// 2.消息
String message = "hello,miku!";
// 3.发送消息,参数分别是:交互机名称、RoutingKey(暂时为空)、消息
rabbitTemplate.convertAndSend(exchangeName,null,message);
}
5.Direct交换机
Direct Exchange 会将接收到的消息根据规则路由到指定的Queue,因此称为定向路由。
- 每一个Queue都与Exchange设置一个BindingKey
- 消息的发送方在向Exchange发送消息时,也必须指定消息的 RoutingKey
- Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
利用SpringAMQP演示DirectExchange的使用
(1) 在RabbitMQ控制台中,声明队列direct.queue1和direct.queue2
(2) 在RabbitMQ控制台中,声明交换机hmall. direct ,将两个队列与其绑定
声明交换机hmall. direct。
将hmall.direct与direct.queue1和direct.queue2绑定,并设置路由键值。
(3) 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
(4) 在publisher中编写测试方法,利用不同的RoutingKey向hmall. direct发送消息
向交换机发送消息时,设置routingKey为"red"。
由于两个队列均与交换机绑定过"red"路由键,故交换机向两个队列均转发了消息,进而投递到两个消费者。
向交换机发送消息时,设置routingKey为"blue"。
由于只有direct.queue1和交换机绑定了路由键"blue",故交换机只将消息转发到direct.queue1,所以只有消费者1 监听到本次消息。
总结
描述下Direct交换机与Fanout交换机的差异?
- Fanout交换机将消息路由给每一个与之绑定的队列
- Direct交换机根据RoutingKey判断路由给哪个队列
- 如果多个队列具有相同RoutingKey,则与Fanout功能类似
6.Topic交换机
TopicExchange与DirectExchange类似,区别在于routingKey可以是多个单词的列表,并且以 . 分割。Queue与Exchange指定BindingKey时可以使用通配符:
- #:代指0个或多个单词
- *:代指一个单词
利用SpringAMQP演示DirectExchange的使用
(1) 在RabbitMQ控制台中,声明队列topic.queue1和topic.queue2
(2) 在RabbitMQ控制台中,声明交换机hmall. topic ,将两个队列与其绑定
绑定topic.queue1,且路由键为china.#。绑定topic.queue2,且路由键为#.news。
(3) 在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
(4) 在publisher中编写测试方法,利用不同的RoutingKey向hmall. topic发送消息
用路由键"china.news"向hmall. topic发送消息。
交换机根据路由键向两个队列转发消息,进而分别投递到两个消费者。
用路由键"china.weather"向hmall. topic发送消息。
交换机根据路由键仅向topic.queue1转发消息,进而投递到消费者1。
总结
描述下Direct交换机与Topic交换机的差异?
- Topic交换机接收的消息RoutingKey可以是多个单词,以 . 分割
- Topic交换机与队列绑定时的bindingKey可以指定通配符
- #:代表0个或多个词
- *:代表1个词
7.声明队列和交换机
在之前我们都是基于RabbitMQ控制台来创建队列、交换机。但是在实际开发时,队列和交换机是程序员定义的,将来项目上线,又要交给运维去创建。那么程序员就需要把程序中运行的所有队列和交换机都写下来,交给运维。在这个过程中是很容易出现错误的。因此推荐的做法是由程序启动时检查队列和交换机是否存在,如果不存在自动创建。
SpringAMQP提供了几个类,用来声明队列、交换机及其绑定关系:
- Queue:用于声明队列,可以用工厂类QueueBuilder构建
- Exchange:用于声明交换机,可以用工厂类ExchangeBuilder构建
- Binding:用于声明队列和交换机的绑定关系,可以用工厂类BindingBuilder构建
基于Java Bean的方式声明一个Fanout类型的交换机,并且创建队列与其绑定
(1) 删除fanout.queue1,fanout.queue2,hmall.fanout
(2) 通常在消费者端声明队列和交换机以及绑定关系,而发送者不关心队列的信息。
package com.itheima.consumer.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutConfiguration {
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("hmall.fanout");
// 方式一
//return ExchangeBuilder.fanoutExchange("hmall.fanout").build();
//方式二
}
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
//return QueueBuilder.durable("fanout.queue1").build();
}
@Bean
public Binding fanoutQueue1Binding(Queue fanoutQueue1,FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Queue fanoutQueue2() {
return new Queue("fanout.queue2");
//return QueueBuilder.durable("fanout.queue2").build();
}
@Bean
public Binding fanoutQueue2Binding(Queue fanoutQueue2,FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
(3) 启动消费者,观察创建效果
启动ConsumerApplication。
已创建fanout.queue1和fanout.queue2。
已创建hmall.fanout。
进入hmall.fanout详情页,可以看到其已绑定了fanout.queue1和fanout.queue2。成功利用代码自动完成队列和交换机的声明和绑定。
SpringAMQP还提供了基于@RabbitListener注解来声明队列和交换机的方式
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1",durable = "true"),
exchange = @Exchange(name = "hmall.direct",type = ExchangeTypes.DIRECT),
key = {"red","blue"}
))
public void listenDirectQueue1(String message) {
log.info("消费者1监听到direct.queue1的消息:【{}】",message);
}
(1) 删除原先的hmall.direct,direct.queue1,direct.queue2。
(2) 通常在消费者端声明队列和交换机以及绑定关系,而发送者不关心队列的信息。
用@RabbitListener注解方式声明队列,交换机,路由键。
注释掉原本基于Java Bean的声明配置的配置类注解@Configuration。
(3) 启动消费者,观察创建效果
启动消费者。
观察控制台,可以看到创建成功了。
总结
声明队列、交换机、绑定关系的Bean是什么?
- Queue
- FanoutExchange、DirectExchange、TopicExchange
- Binding
基于@RabbitListener注解声明队列和交换机有哪些常见注解?
- @Queue
- @Exchange
8.MQ消息转换器
Spring的消息发送代码接收的消息体是一个Object。而在数据传输时,它会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。 只不过,默认情况下Spring采用的序列化方式是JDK序列化。众所周知,JDK序列化存在下列问题:
- 数据体积过大
- 有安全漏洞
- 可读性差
学习案例:测试利用SpringAMQP发送对象类型的消息
(1) 声明一个队列,名为object.queue
检查是否图片错误
(2) 编写单元测试,向队列中直接发送一条消息,消息类型为Map
(3) 在控制台查看消息,总结你能发现的问题
可以看到消息格式非常不友好。
问题
Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。 存在下列问题:
- JDK的序列化有安全风险
- JDK序列化的消息太大
- JDK序列化的消息可读性差
建议采用JSON序列化代替默认的JDK序列化,要做两件事情
(1) 在publisher和consumer中都要引入jackson依赖
注意,如果项目中引入了spring-boot-starter-web依赖,则无需再次引入Jackson依赖。
(2) 在publisher和consumer中都要配置MessageConverter
在服务的启动类中添加Bean,定义消息转换器。
再次测试发送消息。
这是在队列详情查看刚发送的消息,发现是json格式,且消息内容清晰易读。
在服务的启动类中添加Bean,定义消息转换器。
添加对object.queue的监听方法。我们在consumer服务中定义一个新的消费者,publisher是用Map发送,那么消费者也一定要用Map接收。
启动消费者,观察日志信息。发消息时,spring amqp帮我们把map转成字节,发到rabbitmq,rabbitmq把消息投递给我们时是字节,而spring amqp还能把接收的字节转回成map给我们使用。此时,由于队列中有两条消息,第一条消息没用json转换器而是用jdk自动序列化,而第二条才是使用json序列化的。第一个用的jdk序列化,无法使用json处理,故报错。以后只要消息转换器都一致,收发均用json,就不会报错。
四、业务改造
案例需求:改造余额支付功能,将支付成功后基于OpenFeign的交易服务的更新订单状态接口的同步调用,改为基于RabbitMQ的异步通知。
1.引依赖
在trade、pay、common模块均添加SpringAMQP的依赖。
2.配MQ地址
在trade和pay模块的application.yaml中添加rabbitmq的相关配置。
3.配消息转换器,写消息监听器,指定队列交换机
消息转换器的配置类。
该配置要被扫描包扫到才能生效。由于该配置类在hm-common模块下,无法被pay和trade模块扫描到,因此我们采用Springboot自动装配的原理, 在spring.factories中添加MqConfig,使得Springboot能够扫描到它,从而使它生效。
在order模块编写支付状态监听类。
4.将同步调用逻辑改为基于RabbitMQ的异步调用逻辑
将支付成功后基于OpenFeign的交易服务的更新订单状态接口的同步调用,改为基于RabbitMQ的异步通知。调用rabbitTemplate模板库将业务订单号发送给路由键为"pay.success"的交换机"pay.direct"。
重启失败,发现要在common里也加amqp依赖
5.创建交换机,队列,并绑定路由键
创建trade.pay.success.queue队列,用于接收修改订单状态的消息。
创建pay.direct交换机。
将交换机与队列绑定。
6.由订单页进入支付页
在黑马商城进入订单确认页面。
由于虚拟机时间与主机时间不一致,导致一直支付超时。故先设置linux虚拟机的时间为当前时间。
提交订单,进入支付页面。
7.付款前
pay_order的status=1即待支付。
order的status=1即未付款。
jack的余额为1000000。
8.付款后
多次进行支付操作,均失败。分析报错信息和代码后,发现是因为之前在拆分微服务时,在user-service模块里多放置了用于登录校验的AuthProperties.java,MvcConfig.java,LoginInterceptor.java三个类,导致支付时出现了要登录校验的逻辑。由于登录校验在网关模块做了,user-service这里的要删去。之后就可以运行支付程序了。
现在可以成功支付了。
在user模块运行日志显示扣款成功。
Jack用户余额已扣除。
order表中订单状态改为2即“已支付,未发货”。说明执行了修改订单状态的代码。
在trade.pay.success.queue队列可以看到消息发送记录,说明异步调用成功了。
MQ入门学习总结
- 学习了同步调用和异步调用的优缺点和使用场景。
- 学习了RabbitMQ的安装和使用和数据隔离。
- 学习了MQ的Java客户端的使用,workqueue,三种交换机fanout、direct、topic,基于Bean或注解声明队列交换机,消息转换器。
- 改造了余额支付功能,将支付成功后基于OpenFeign的交易服务的更新订单状态接口的同步调用,改为基于RabbitMQ的异步通知。
- 删掉了user-service中多余且导致支付功能错误的登录校验代码。