目录
一.初步认识
1.介绍
1.1.同步、异步介绍
同步调用就像你和她视频聊天,同步进行;异步调用像你每天的早安、晚安,她都已读不回。
之前我们使用的 OpenFeign 远程调用属于同步调用,它的优点就是时效性强。而这种方式在处理高并发时存在问题,还有在调用链过长时,导致扩展性差、性能低、容易级联失败等问题。这就需要我们使用到新的技术——异步调用,而消息队列 MessageQueue(MQ) 就很好的解决了这个问题,提高了效率。
异步通讯
消息发送者:投递消息的人,就是原来远程调用中的调用者。
消息接收者:接收和处理消息的人,就是原来远程调用中的服务提供者。
消息队列:管理、暂存、转发消息,相当于一个中间的服务器。
优点:解耦合、减少耗时、故障隔离、缓存消息。
缺点:时效性差、无法保证后续业务成功、业务安全依赖于消息代理(broker)的可靠性。
1.2.技术选型
以下是常用的 4 个 MQ 技术,而要学习的是其中的 RabbitMQ 技术。
2.安装部署
我们选择使用 docker 将其部署到 Linux 虚拟机中,便于我们使用,需要先拉取到 RabbitMQ的镜像然后使用以下命令部署,也可使用资料中准备好的 tar 包加载进来再部署。
docker run \
-e RABBITMQ_DEFAULT_USER=登录用户名\
-e RABBITMQ_DEFAULT_PASS=登录密码\
-v mq-plugins:/plugins \
--name mq \
--hostname mq \
-p 15672:15672 \
-p 5672:5672 \
--network hm-net\
-d \
rabbitmq:3.8-management
1.登录用户名和密码可自行设置。
2.-v:数据卷的挂载。
3.--name ---hostname:容器名 主机名。
4.-p:端口
4.1:15672:控制台的端口。
4.2:5672:收发消息的端口。
5.--network:网络(若不需要,可删除)。
6.-d:后台运行。
成功安装后运行,访问 虚拟机地址:端口号 进入到 RabbitMQ 的控制台,使用之前配置的用户名和密码登录。
成功登陆控制台
3.控制台操作
在后续的控制台操作示例中,我们主要会在 Exchange(交换机)、Queue(队列)、admin(管理员)这三个菜单中进行操作,而 RabitMQ 会在创建时生成一些默认的队列与交换机,方便我们进行操作。
3.1.Queue(队列)
在队列的页面中第一个展示的是所有的队列,可点击队列名查看详细信息并对其进行操作。
第二个是新建队列的展示,队列名是必填项。
进入到队列详情中 Bindings 可以查看与交换机的绑定关系也可进行绑定,同时下面可以进行删除队列,查看队列接收的消息。
3.2.Exchange(交换机)
交换机界面与队列界面类似。
进入到交换机查看详情,在 Bindings 下输入队列的名称绑定 Exchange 与 Queue 的关系,如果不绑定,则发送到交换机的信息会丢失,因为交换机只负责转发消息,不能存储。
在交换机的详情页的·Publish message 下发送信息,因为我们成功的绑定了队列与交换机的关系,所以成功后在队列中点击 Get Message 可以获取到发送的信息内容。
4.数据的隔离
RabbitMQ 的数据隔离主要通过其内置的虚拟主机(Virtual Host)功能来实现,不同的主机之间的数据不互通。可以给用户创建不同的虚拟主机来实现登录账号不同,则看到的数据也不同的效果。我们可以在 admin 页面下进行设置。
在用户管理下可以查看所有的用户以及添加新的用户。其中,再添加用户时密码需输入两次确认,还需设置用户的权限(如 Admin、Monitoring 等)。
而新建的用户是没有虚拟主机的,并且无法操作属于别人的交换机、队列。所以需要创建虚拟主机并绑定到与之对应的用户上。(注意,在创建时会自动绑定到当前登录的用户)
可以在此处切换想要查看的虚拟主机。
二.Java客户端操作
1.常规操作
1.1.发送消息
我们已经基本了解了在控制台中的基本操作,但在业务开发中,主要是在 Java 的客户端中去进行开发,而想要在 Java 中进行操作可以使用 Spring AMQP 这套 API 规范来实现功能。
收发消息
1.引入依赖
<dependencies>
<!--AMQP依赖,包含RabbitMQ-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.7.18</version>
</dependencies>
2.在 yml 配置文件中配置 RabbitMQ 服务端信息
需要在发送者和接收者中都配置好,端口是 5672 而不是15672 ,不要配置错误。
3.注入 RabbitTemplate 工具类,准备好参数,发送(队列要提前创建好)
可以看到成功发送消息并接收到
1.2.接收消息
声明一个类加上 @Component 注解注册成一个 Bean,类中方法加上 @RabbitListener 注解使之成为一个监听器,接收信息的参数类型与发送的类型对应,而对象也可传递,Spring 会自动实现从对象到消息的转换,我们只需对信息进行处理即可。
可以看到成功接收到了信息并输出
2.WorkQueue
WorkQueue,任务模型。就是让多个消费者绑定到一个队列上,共同消费队列中的消息,可以加快消息处理的速度,避免消息的堆积。
通过代码对同一个队列进行监听,并通过循环发送 50 次消息。
可以看到,两个消费者接收消息的顺序十分规律,所以消息只能被处理一次,当有多个消费者监听同一个队列时,RabbitMQ 会按照轮询的方式将消息平均发送给这些消费者。即每个消费者会依次接收到一条消息,直至所有消息都被分配完毕。这种方式确保了消息的均衡分配,避免了某个消费者过载而其他消费者空闲的情况。
需要注意的是,虽然 RabbitMQ 默认采用轮询方式分发消息,但在某些情况下,也可以通过配置实现其他分发策略,如公平分发。在公平分发模式下,RabbitMQ 会根据消费者的消费能力进行动态调整,确保处理能力强的消费者能够接收到更多的消息,而处理能力弱的消费者则接收到较少的消息。这种分发方式可以更加灵活地适应不同的应用场景和需求。
例如我们调整不同消费者的处理速度,使其休眠不同的时间来模拟处理效率的差异,并且修改相关 yml 配置文件的配置,使同一时刻最多投递的消息数为 1,只有处理完成才能获取下一个信息。(能者多劳)
spring:
rabbitmq:
listener:
simple:
prefetch: 1
结果就是处理较快的会处理更多的消息,而处理慢的会处理少的消息,使总耗时大大降低,提高了消息处理的效率。
3.三种交换机
可在控制台中新增交换机处选择类型(Type)
3.1.广播交换机(Fanout Exchange)
- 工作原理:
- 广播交换机将消息发送到所有与之绑定的队列中,无论消息的路由键是什么。
- 它实现了一对多的消息分发,类似于广播模式。
- 应用场景:
- 适用于需要广播消息的场景,例如实时消息发布、通知系统等。
- 当需要将同一条消息发送给多个消费者时,可以使用广播交换机。
发送时需填入三个参数,分别为交换机名、Routing Key、消息,其中 Routing Key 可以为空。类似于广播模式,可以实现一条消息被多个消费者都处理的效果。
3.2.定向交换机(Direct Exchange)
- 工作原理:
- 定向交换机根据消息的路由键(Routing Key)将消息发送到与之匹配的队列中。
- 如果消息的路由键与队列的绑定键(Binding Key)完全匹配,那么消息将被发送到该队列中。
- 应用场景:
- 适合一对一的消息传递,例如日志处理、任务分发等。
- 当需要确保消息准确发送到特定队列时,可以使用定向交换机。
发送时需填入三个参数,分别为交换机名、Routing Key、消息,根据队列与交换机之间的键来判断路由交由哪个队列去处理,如果多个队列具有相同的 路由键,则功能与 Fanout 交换机的功能类似。
3.3.话题交换机(Topic Exchange)
- 工作原理:
- 主题交换机根据消息的路由键和队列的绑定键的模式进行匹配。
- 可以用通配符(*和#)来匹配多个路由键使拓展性更强,从而实现更灵活的消息路由。
- 其中,* 表示一个词,# 表示一个或多个词。
- 应用场景:
- 适合主题订阅模型,例如邮件分类、日志级别过滤等。
- 当需要根据消息的某个主题或类别进行路由时,可以使用主题交换机。
假如存在 #.new 与 china.# 这两个绑定键,那么如果路由键为 china.weather 则只有第二个会收到消息,如果路由键为 china.new 则两个都会收到消息。
4.声明队列交换机
4.1.Bean声明
手动的声明队列与交换机的效率低下,且存在误输入的风险,可靠性并不高,所有要使用 Java 代码来声明队列与交换机来提高开发的效率。
而在 Spring AMQP 中提供了对应的类用来声明队列、交换机、以及它们之间的绑定关系。
1,Queue:用于声明队列,也可以使用工厂类 QueueBuilder 来构建。
2,Exchange:用于声明交换机,也可以使用工厂类 ExchangeBuilder 来构建。
3,Binding:用于声明两者之间的绑定关系,也可以使用工厂类 BindingBuilder 来构建。
注意:Exchange 只是一个接口,其具体的实现类分别是对应的几个不同类型的交换机,如 FanoutExchange 、DirectExchange 、TopicExchange 等等。
声明方式
创建一个 Configuration 配置类,在其中将队列、交换机、绑定关系分别配置成 Bean,返回对应的对象或使用工厂类构造,运行对应的启动类,就会自动的去创建与之对应队列、交换机、以及绑定关系。
其中绑定关系的构建是 BindingBuilder.bind(队列).to(交换机).with(RoutingKey) 这样的。
4.2.@RabbitListener 注解声明
可以使用当时用于定义消费者的注解 @RabbitListener 来定义队列、交换机、及绑定关系,只需其中的 bindings 属性,在其中使用 @QueueBinding 注解进行定义。
1.value = @Queue(...) 定义了队列的具体属性。
2.exchange = @Exchange(...) 指定关联的交换机详情。
3.key = {"hi"} 设置了绑定的路由键。
创建成功
5.消息转换器
在 Spring AMQP 在内部进行消息转化的时候会使用 JDK 自带的序列化方式,这种方法存在着问题,首先 JDK 的序列化存在安全风险,反序列化时容易被代码注入,其次,序列化后的消息占用空间太多,可读性差。
建议使用 JSON 序列化代替默认的 JDK 序列化。
1.在消息的接收者和消费者中都引入 jackson 的依赖
<!--jackson-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.4</version>
</dependency>
2. 在两者中都要配置 MessageConverter 成 Bean(可在启动类中配置)
配置成功后的消息
三.进阶操作(业务优化)
1.发送者的可靠性
1.1.发送者重连
有的时候由于网络波动,可能会出现发送者连接 MQ 失败的情况。我们可以通过手动配置开启连接失败后的重连机制,红色框中参数为默认值,可根据需要调整。
关闭掉正在运行的 RabbitMQ,模拟无法连接的状况。可以看到基本每隔 2 秒尝试重新连接,总共连接了 3 次。间隔时间 = 连接超时时间 + 重试间隔时间,所以是 2 秒。
缺点:Spring AMQP 提供的重试机制时阻塞式的重试,在多次重试等待的过程中,当前线程是被阻塞的,会增加业务耗时从而影响业务的性能,如果对性能有要求,可以禁用重连机制或进行合适的配置,也可使用异步线程来执行发送消息的代码。
1.2.发送者确认机制
Spring AMQP 提供了 Publisher Confirm 和 Publisher Return 两种确认机制,开启确认机制后,当发送者发送消息给 MQ 后,MQ 会返回确认结果给发送者。
1.消息投递到了 MQ,但是路由失败,此时会返回 PublisherReturn 返回路由异常原因,然后返回 ACK,告知投递成功。(基本都是由于本身代码的问题,再次重发可能还会失败,所以返回 ACK 算投递成功)
2.临时消息投递到了 MQ,并且入队成功,返回 ACK,告知投递成功。(临时消息不需要持久化,只要投递过去就算成功)
3.持久消息投递到了 MQ,并且入队完成持久化,返回 ACK,告知投递成功。
4.其他情况都会返回 NACK,告知投递失败。
实现发送者确认机制
1.在发送者的 yml 配置文件中添加配置。
spring:
rabbitmq:
publisher-confirm-type: correlated #开启 publisher confirm 机制,并设置 confirm 类型
publisher-returns: true #开启 publisher return 机制
publisher-confirm-type 配置有三种类型:
1)none:关闭 confirm 机制。
2)simple:同步阻塞等待 MQ 的回执消息。
3)correlated:MQ 异步回调方式返回回执消息。(常用)
2.每个 RabbitTemplate 只能配置一个 ReturnCallback,因此需要在项目启动过程中配置。
使用 @PostConstruct 注解在初始化时执行该方法来配置消息发送失败时的回调。
3.发送消息,指定消息 ID、消息 ConfirmCallback
CorrelationData 需要两个东西,一个是消息的唯一标识 ID,另一个是它的一个回调函数,而 ID 可以在创建时使用 UUID 工具类生成随机的 ID,而回调函数可以使用 addCallback 方法添加。onFailure 方法是 Spring AMQP 在内部处理 MQ 返回的结果中出现异常后的处理,几乎不发生。 onSuccess 方法是成功拿到处理结果后的处理。最后需要将回调函数传入发送消息方法中。
因为是测试,运行完后会终止进程,就无法得到回调,所以要休眠 2 秒等待回调结果返回,同时也需要将日志级别调为 debug 级,才可以看到日志。
缺点:会影响消息发送的效率,不建议开启或限制重试次数。
2.MQ的可靠性
在默认情况下,RabbitMQ 会将接收到的信息保存在内存中以降低消息收发的延迟,这样就会导致两个问题。
1.一旦 MQ 宕机,内存中的消息就会丢失。
2.内存空间有限,当消费者故障或处理速度较慢时,会导致消息堆积,引起阻塞。
解决方法:
2.1.数据持久化
交换机持久化:
在默认情况下创建的交换机都是默认持久化的,也可以在创建时指定为临时的交换机。
队列持久化:
在默认情况下创建的队列都是默认持久化的,也可以在创建时指定为临时的队列。
消息持久化:
在默认情况下是非持久的,可以选择 2 发送持久化的消息,而 Spring AMQP 发送的消息默认是持久化的,我们也可以通过自定义构建消息来发送非持久化的消息。
Message message = MessageBuilder
.withBody("holle, SpringAMQP".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT)
.build();
其中 setDeliveryMode 用于设置投递模式为持久化或非持久化。
持久化的优点在于重启后,持久化的交换机、队列、消息仍然会存在,提高了效率。
2.2.Lazy Queue(惰性队列)
在 RabbitMQ 的 3.6.0 版本增加的概念,在 3.12 版本后默认所有队列都是 Lazy Queue 模式,无法更改。Lazy Queue 在接收到信息后直接存入到磁盘,不在存储在内存中,消费者要消费信息时才会从磁盘中读取并加载到内存(可以提前缓存部分消息到内存,最多为2048条)
旧版本更换 Lazy Queue 模式:
1.控制台:
在新建队列界面下的 Arguments 选项中选择 Lazy mode 模式创建。
2.Java客户端:
2.1.Bean注解
2.2.@RabbitListener注解
在开启持久化和生产者确认时,RabbitMQ 只有在消息持久化完成之后才会给生产者返回 ACK 回执。
3.接收者的可靠性
3.1.消费者确认机制
消费者确认机制是为了确认消费者是否成功处理消息。当消费者处理消息结束后应该向 RabbitMQ 发送一个回执,告知 RabbitMQ 自己消息的处理状态。
ack:成功处理消息,RabbitMQ 从队列中删除该消息。
nack:消息处理失败,RabbitMQ需要再次投递消息。
reject:消息处理失败并拒绝该消息,RabbitMQ 从队列中删除该消息。
而 Spring AMQP 已经实现了消费者的消息确认功能,并且可以通过配置消费者的配置文件来选择三种不同的处理方式。
none:不处理,即消息投递该消费者后立刻 ack,消息会立刻从 MQ 删除。(不建议)
manua:手动模式。需要自己在业务代码中调用 api,发送 ack 或 reject,存在业务入侵,但更加灵活。
auto:自动模式。Spring AMQP 利用 AOP 对我们的消息处理逻辑做了增强循环,当业务正常执行时则自动返回 ack,当业务出现异常时,根据异常判断返回不同结果。
如果是业务异常,会自动返回 nack;如果是消息处理或校验异常,自动返回 reject
spring:
rabbitmq:
listener:
simple:
ackonwledge-mode: auto # 配置为 auto 模式
3.2.失败重试机制
Spring AMQP 提供了消费者重试机制,在消费者出现异常时利用本地重试,而不是无限的发送消息到 MQ 中,我们可以通过在 yml 配置文件中添加相关配置来开启重试机制。
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启重试机制
initial-interval: 1000ms # 第一次重试间隔时间
multiplier: 1 # 失败后重试间隔倍数
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,则设置为false
这些都是配置的参数都是默认值,可根据需求进行修改。而在开启重试模式后,如果耗尽重试次数后消息仍然失败,就会使用 MessageRecoverer 接口来处理,它包含三种实现。
1.RejectAndDontRequeueRecoverer:重试次数耗尽后,直接 reject,丢弃消息。(默认)
2.ImmediateRequeueMessageRecoverer:重试耗尽后,返回 nack,消息重新入队。
3.RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机。
3.1.定义接收失败消息的交换机、队列及其绑定关系。
3.2.定义 RepublishMessageRecoverer 为一个 Bean
3.3.业务幂等性
在程序开发中,幂等是指同一个业务,执行一次或多次对业务状态的影响是一致的。
保证幂等性的方案:
1.每一个消息都设置一个唯一 Id,利用 Id 区分是否是重复复消息:
每一条消息都生成一个唯一 Id,随消息一起投递。消费者接收到消息后去处理业务,成功后将 Id 储存在数据库中,如果下次收到相同消息,利用数据库中的 Id 进行重复判断,若重复则放弃业务处理。
而在消息转化器的声明中,我们可以配置自动生成创建消息 Id,以此来实现业务幂等性。
可以看到消息成功带上了 Id
如果想要在 Java 客户端中获取到消息,则可以将数据的接收类型改为 Message 类型,getbody 方法获取消息体,getMessageId 方法获取消息 Id。
2.结合业务逻辑,基于业务本身做判断。(略)
4.延迟消息
发送者可以在发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才会收到消息。可以通过延时消息实现延时任务功能,设置在一定时间之后才执行任务。
4.1.死信交换机
当一个队列中的消息满足下列情况之一时,就会成为死信(dead letter):
- 消费者使用 basic.reject 或 basic.nack 声明消费失败,并且消息的 requeue 参数设置为 false
- 消息是一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
如果队列通过 dead-letter-exchange 属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中。这个交换机称为死信交换机(Dead Letter Exchange,简称 DLX),我们可以利用该机制实现延迟消息。
生成相应的交换机及队列并绑定关系。
使用 setExpiration 方法可以设置消息的过期时间。(单位 ms)
4.2.延迟消息插件
DelayExchange 插件可以将普通的交换机改造为支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列。
因为 RabbitMQ 是通过 docker 进行安装的,所以只需将下载好的文件移动至当时配置好的 mq 的数据卷位置即可。
可通过 docker volume inspect xxx 命令查看对应的数据卷
docker exec -it mq xxx enable rabbitmq_delayed_message_exchange 安装
方式1:在 @Exchange 中添加 delayed 并设置开启。
方式2:在构造交换机时后跟 .delayed() 开启延时。
最后在发送消息时添加上 setDelay 方法设置延时时间。(单位 ms)
【~~完结~~】
如有不足,请指出,谢谢。