黑马商城MQ入门学习笔记

189 阅读29分钟

一、什么是MQ?

1.同步调用

同步调用的基本概念

同步调用是指在调用一个方法或函数时,调用者会等待该方法或函数执行完毕并返回结果后,才继续执行后续的代码。同步调用的特点是执行顺序明确,但在等待过程中可能会阻塞调用者的线程,导致效率降低。

选择同步调用的场景

  1. 任务顺序重要:如果任务必须按顺序执行,且后续任务依赖前一个任务的结果,那么同步调用是合适的选择。
  2. 短时间任务:如果任务执行时间很短,不会导致明显的延迟或阻塞,那么可以使用同步调用。
  3. 简单逻辑:同步调用的代码逻辑简单,易于理解和维护,适用于不需要并发处理的场景。

同步调用的优缺点

同步调用的优势是什么?

  • 时效性强,等待到结果后才返回。

同步调用的问题是什么?

  • 拓展性差
  • 性能下降
  • 级联失败问题

2.异步调用

异步调用的基本概念

异步调用是指在调用一个方法或函数时,调用者不会等待该方法或函数执行完毕,而是立即继续执行后续的代码。异步调用通常通过回调函数、Promise 或 Future 等机制来处理结果。异步调用的特点是可以提高并发性和效率,但代码逻辑可能会变得复杂。

选择异步调用的场景

  1. 长时间任务:如果任务执行时间较长,可能会导致阻塞,那么异步调用可以提高系统的响应速度和并发性。
  2. 并发处理:如果需要同时处理多个任务,异步调用可以提高系统的并发处理能力。
  3. 用户体验:在前端开发中,异步调用可以避免界面卡顿,提供更好的用户体验。例如,发送网络请求时使用异步调用,用户可以继续操作界面,而不必等待请求完成。

异步调用的优缺点

异步调用的优势是什么?

  • 耦合度低,拓展性强
  • 异步调用,无需等待,性能好
  • 故障隔离,下游服务故障不影响上游业务
  • 缓存消息,流量削峰填谷

异步调用的问题是什么?

  • 不能立即得到调用结果,时效性差
  • 不确定下游业务执行是否成功
  • 业务安全依赖于Broker的可靠性

3.MQ技术选型

异步调用通常是基于消息通知的方式,包含三个角色:

  • 消息发送者:投递消息的人,就是原来的调用者
  • 消息接收者:接收和处理消息的人,就是原来的服务提供者
  • 消息代理:管理、暂存、转发消息,你可以把它理解成微信服务器

MQ(MessageQueue),即消息队列,也就是异步调用中的Broker。消息队列(MQ)技术选型是一个复杂的过程,需要考虑多个因素,包括性能、可用性、功能特性、社区支持等。以下是几种常见的消息队列技术及其特点:

Kafka

  • 优点
    • 高吞吐量:适用于大数据处理场景,如实时日志收集、流式数据处理等。
    • 持久化存储:将消息持久化到磁盘,确保数据可靠。
    • 分布式架构:支持水平扩展,具备良好的容错能力。
    • 消息顺序保证:在分区级别保证消息的有序性。
  • 缺点
    • 配置和管理复杂:涉及分区、副本、消费者组等概念。
    • 强依赖 ZooKeeper:增加了系统的外部依赖和运维成本。

RabbitMQ

  • 优点
    • 灵活的路由模型:提供丰富的交换机类型,支持复杂的路由规则。
    • 高可用性:通过主从复制实现高可用集群。
    • 广泛的语言支持:提供多种客户端库,几乎覆盖所有主流编程语言。
  • 缺点
    • 吞吐量和延迟:相较于 Kafka 和 RocketMQ,性能稍逊。
    • 资源消耗:在集群环境中资源消耗较大。
    • 集群管理复杂:配置与维护相对繁琐。

RocketMQ

  • 优点
    • 高性能与低延迟:适合金融、电商等对性能要求严苛的场景。
    • 分布式事务支持:确保消息发送与业务操作的一致性。
    • 阿里巴巴背书:经过大规模生产环境验证。
  • 缺点
    • 社区活跃度:相较于 Kafka,社区活跃度略逊。
    • 学习曲线:部分高级特性的理解和使用需要一定的学习和实践经验。

ActiveMQ

  • 优点
    • 成熟稳定:历史悠久,社区成熟,稳定性良好。
    • 协议丰富:支持多种消息协议,易于与其他系统集成。
    • 轻量级:适合小型项目或对资源敏感的场景。
  • 缺点
    • 性能瓶颈:单机吞吐量较低,不适合大规模消息处理。
    • 可靠性问题:在高并发或网络不稳定环境下,存在数据丢失风险。
    • 管理工具不足:原生管理工具功能较为简单。

选择指南

  1. 性能需求:如对吞吐量、延迟有极高要求,优先考虑 Kafka 和 RocketMQ;对性能要求适中,RabbitMQ 是不错的选择;对资源有限的小型项目,ActiveMQ 可能是最轻量的解决方案。
  2. 消息语义:如需严格的消息顺序保证、事务支持,RocketMQ 更胜一筹;如需灵活的路由规则,RabbitMQ 更适合。
  3. 生态与集成:考量现有系统使用的语言、框架及已有中间件的兼容性,以及社区支持、插件丰富度等因素。
  4. 运维复杂度:对于运维团队实力较强、愿意投入精力管理复杂系统的组织,可以选择 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 命令。以下是对每个参数的详细解释:

  1. docker run:这是 Docker 命令,用于运行一个新的容器。
  2. -e RABBITMQ_DEFAULT_USER=itheima:设置环境变量,指定 RabbitMQ 的默认用户名为 itheima
  3. -e RABBITMQ_DEFAULT_PASS=123321:设置环境变量,指定 RabbitMQ 的默认密码为 123321
  4. -v mq-plugins:/plugins:将主机上的 mq-plugins 目录挂载到容器内的 /plugins 目录,用于存储插件。
  5. --name mq:为容器指定一个名称 mq
  6. --hostname mq:为容器指定一个主机名 mq
  7. -p 15672:15672:将主机的 15672 端口映射到容器的 15672 端口,用于访问 RabbitMQ 管理界面。
  8. -p 5672:5672:将主机的 5672 端口映射到容器的 5672 端口,用于 RabbitMQ 的消息传输。
  9. --network hmall:将容器连接到名为 hmall 的 Docker 网络。
  10. -d:以守护进程模式运行容器,即在后台运行。
  11. 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的官方地址

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 异常。

解决方案 要解决这个问题,可以采取以下步骤:

  1. 升级 Lombok 版本:将 Lombok 库升级到 1.18.30 或更高版本。该版本已经修复了这个问题。

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.32</version>
        <scope>provided</scope>
    </dependency>
    
  2. 更新 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>
    
  3. 重新加载项目:在 IntelliJ IDEA 中,重新加载 Maven 项目,确保使用最新的 Lombok 版本。如果问题仍然存在,尝试删除本地 Maven 仓库中的 Lombok 缓存,并重新构建项目。

因此需在mq-demo的pom.xml文件中增加Maven 编译插件,并在 maven-compiler-plugin 中指定 Lombok 的版本。

重启消费者,成功监听到了来自simple.queue的消息。

总结:SpringAMQP如何收发消息?

  1. 引入spring-boot-starter-amqp依赖
  2. 配置rabbitmq服务端信息
  3. 利用RabbitTemplate发送消息
  4. 利用@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处理一半的偶数。

结论:

  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。

总结

  1. 交换机的作用是什么?
  • 接收publisher发送的消息
  • 将消息按照规则路由到与之绑定的队列
  • 不能缓存消息,路由失败,消息丢失
  • FanoutExchange会将消息路由到每个绑定的队列
  1. 发送消息到交换机的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入门学习总结

  1. 学习了同步调用和异步调用的优缺点和使用场景。
  2. 学习了RabbitMQ的安装和使用和数据隔离。
  3. 学习了MQ的Java客户端的使用,workqueue,三种交换机fanout、direct、topic,基于Bean或注解声明队列交换机,消息转换器。
  4. 改造了余额支付功能,将支付成功后基于OpenFeign的交易服务的更新订单状态接口的同步调用,改为基于RabbitMQ的异步通知。
  5. 删掉了user-service中多余且导致支付功能错误的登录校验代码。