RabbitMQ基础
RabbitMQ
使用背景
在微服务项目中,由于服务进行了拆分,必然会涉及到不同服务之间的相互调用,而在调用中发起请求需要等待服务器执行业务返回结果,才能继续执行后面的业务,也就是说在等待过程中是处于阻塞状态,因此我们将这种调用方式称为同步调用,也可以将做同步通讯。
而在很多场景中我们会使用异步通讯
- 同步通讯:双方的交互是实时的,就像打电话一样,同一时间,你只能跟一个人打视频电话
- 异步通讯:双方的交互不是实时的,不需要立刻给对方回应,因此在异步通讯下,可以多线操作,同时跟多个人聊天
两种方式各有优势,但是在我们的业务中,如果需要实时得到响应,则应该选择同步通讯,而如果追求更高的效率,而且不需要实时响应,则应该选择异步通讯
在之前我们同步调用是采用OpenFeign调用
而本篇文章学习异步调用采用RabbitMQ调用
异步调用
异步调用的方式其实就是基于消息通知的方式,一般包含三个角色
- 消息发送者:投递消息的人,就是原来的调用方
- 消息Broker:管理,暂存,转发消息,你可以把它理解成微信服务器
- 消息接收者:接收和处理消息的人
在异步调用中,发送者不再直接同步调用接收者的业务接口,而是发送一条消息投递给消息Broker。然后接收者根据自己的需求从消息Broker那里订阅消息。每当发送方发送消息后,接收者都能获取消息并处理。
这样就能实现,发送消息的人和接收消息的人就完成解耦
优点
- 耦合度更低
- 性能更好
- 业务拓展性强
- 故障隔离,避免级联失败
异步通信也有它的缺点;
- 完全依赖于Broker的可靠性,安全性,和性能
- 架构复杂,后期维护和调试麻烦
RabbitMQ架构
publisher
:生产者,也就是发送消息的一方consumer
:消费者,也就是消费消息的一方queue
:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理exchange
:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。virtual host
:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue
收发消息
- 交换机
- 队列
安装
我们如果想要使用MQ,则需要在环境上安装MQ
基于Docker来安装RabbitMQ
docker run \ -e RABBITMQ_DEFAULT_USER=itcaca \ -e RABBITMQ_DEFAULT_PASS=123321 \ -v mq-plugins:/plugins \ --name mq \ --hostname mq \ -p 15672:15672 \ -p 5672:5672 \ --network hm-net\ -d \ rabbitmq:3.8-management
15672:MQ提供的管理控制台的端口
2672:MQ的消息发送处理接口
安装完成后,我们需要访问 http://xxxxxxx:15672就可以进入管理控制台,首次访问需要登录,默认的用户名和密码已经在安装命令中已经指定了(itcaca/123321)
收发架构
概念:
publisher
:生产者,也就是发送消息的一方consumer
:消费者,也就是消费消息的一方queue
:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理exchange
:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。virtual host
:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue
流程
我们发送到交换机的消息,只会路由到与其绑定的队列,因此我们创建完队列之后,我们需要将其与其交换机绑定
Spring AMQP
在业务开发中,我们不会在控制台收发消息,而是应该基于编程的方式,由于RabbitMQ采用了AMQP协议,因此他具备跨语言的特性,任何语言只要遵循AMQP协议收发消息,都可以与RabbitMQ交互,并且RabbitMQ官方也提供了各种不同语言的客户端
Spring官方基于RabbitMQ提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配。
Spring AMQP提供了三个功能
- 自动声明队列,交换机及其绑定关系
- 基于注解的监听器模式,异步接收消息
- 封装了RabbitTemplate工具,用于发送消息
依赖
<!--AMQP依赖,包含RabbitMQ--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
入门
MQ的流程应该是将消息发送到交换机然后,交换机发送消息到绑定的队列,在以下实例代码中,我们直接跳过交换机,向队列发送消息。
在使用之前,我们先配置MQ地址,在yml文件中添加配置
spring: rabbitmq: host: # 你的虚拟机IP port: 5672 # 端口 virtual-host: /hmall # 虚拟主机 username: hmall # 用户名 password: 123 # 密码
发送消息
在publisher
服务中编写测试类,并利用RabbitTemplate实现消息发送
package com.itheima.publisher.amqp; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSimpleQueue() { // 队列名称 String queueName = "simple.queue"; // 消息 String message = "hello, spring amqp!"; // 发送消息 rabbitTemplate.convertAndSend(queueName, message); } }
接收消息
接收消息也需要首先配置MQ地址,在yml中添加配置
spring: rabbitmq: host:# 你的虚拟机IP port: 5672 # 端口 virtual-host: /hmall # 虚拟主机 username: hmall # 用户名 password: 123 # 密码
在consumer
服务中编写消息监听器类,并利用@RabbitListener实现消息接收消费
package com.itheima.consumer.listener; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Slf4j @Component public class SpringRabbitListener { /* 利用@RabbitListener注解,可以监听到对应队列的消息 一旦监听的队列有消息,就会回调当前方法,在方法中接收消息并消费处理消息 */ @RabbitListener(queues = "simple.queue") public void listenSimpleQueueMessage(String message) throws Exception { System.out.println("SpringRabbitListener listenSimpleQueueMessage 消费者接收到消息: " + message); } }
测试
启动consumer服务,然后在publisher服务运行测试代码,发送MQ消息。
WorkQueue模型
Work queues,任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。
这就产生了问题
问题应用场景
当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。
此时就可以使用work模型,多个消费者共同处理消息处理,消息处理的速度就能大大提高了。
正常情况下,消息平均分配到每个消费者,并没有考虑到消费者的处理能力,导致1个消费者空闲,另一个消费者忙到不可开交,没有充分利用每一个消费者的能力,最终消息处理的耗时远远超过了1秒。
能者多劳配置
在Spring中可以这样配置,可以使每次只能获取一条消息,处理完成才能获取下一个消息
spring: rabbitmq: listener: simple: prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
这样配置可以使两个消费者充分利用他们的处理能力,可以有效避免消息积压问题。
Work模型的使用
- 多个消费者绑定到一个队列,同一个消费只会被一个消费者处理
- 通过设置prefetch来控制消费者预取的消息数量
交换机类型
下面我们引入交换机。
注意,Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失。
交换机有四种类型
- Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是Fanout交换机。
- Direct:订阅,基于RoutingKey发送给订阅了消息的队列
- Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符
- Headers:头匹配,基于MQ的消息头匹配,用的较少
Fanout交换机
Fanout,英文翻译是扇出,我觉得在MQ中叫广播更合适。
- 可以有多个队列
- 每个队列都要绑定到Exchange(交换机)
- 生产者发送的消息,只能发送到交换机
- 交换机把消息发送给绑定过的所有队列
- 订阅队列的消费者都能拿到消息
场景
- 创建一个名为
hmall.fanout
的交换机,类型是Fanout
- 创建两个队列
fanout.queue1
和fanout.queue2
,绑定到交换机hmall.fanout
消息发送
在publisher服务的SpringAmqpTest类中添加测试方法
/* 测试 fanout exchange; 向 hmall.fanout 交换机发送消息,消息内容为 hello everyone!,会发送到所有绑定到该交换机的队列 */ @Test public void testFanoutExchange() { //交换机名称 String exchangeName = "hmall.fanout"; //发送的内容 String message = "hello everyone!"; //发送消息 rabbitTemplate.convertAndSend(exchangeName, "", message); }
convertAndSend方法的第二个参数,路由key由于没有绑定,所以可以指定为空
消息接收
在consumer服务的SpringRabbitListener中添加两个办法,作为消费者:
/* 监听 fanout.queue1 队列的消息 */ @RabbitListener(queues = "fanout.queue1") public void listenFanoutQueue1(String message) { System.out.println("【消费者1】接收到消息: " + message ); } /* 监听 fanout.queue2 队列的消息 */ @RabbitListener(queues = "fanout.queue2") public void listenFanoutQueue2(String message) { System.out.println("【消费者2】接收到消息: " + message ); }
交换机的作用:
- 接收publisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- 不能缓存消息,路由失败,消息丢失
- FanoutExchange的会将消息路由到每个绑定的队列
Direct交换机
在Fanout模式中,一条消息,会被所有订阅的队列都消费,但是,在某些场景下,我们希望不同的消息被不同的队列消费,这时就要用到Direct类型的Exchange。
在Direct模型下:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
- 消息的发送方法在向Exchange发送消息时,也必须指定消息的RoutingKey
- Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing key进行判断,只有队列的Routing Key与消息的Routing Key完全一致,才会接收到消息
场景
- 声明一个名为hmall.direct的交换机
- 声明队列direct.queue1,绑定hmall.direct,bingdingKey为blud和red
- 声明队列direct.queue2,绑定hmall.direct,bindingKey为yellow和red
- 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
- 在publisher中编写测试方法,向hmall.direct发送消息
消息发送
在publisher服务的SpringAmqpTest类中添加测试方法:
/* 测试 direct exchange; 向 hmall.direct 交换机发送消息,会根据路由key发送到所有绑定到该交换机的队列 */ @Test public void testDirectExchange() { String exchangeName = "hmall.direct"; String message = "震惊!哈尔滨上空惊现黑龙,吞云吐雾喜迎八方来客!"; //发送 路由key 为 red 的消息; rabbitTemplate.convertAndSend(exchangeName, "red", message); //发送 路由key 为blue的消息; message = "最新消息!哈尔滨上空的黑龙实为纸鸢,已送给来尔滨的公主及殿下。"; rabbitTemplate.convertAndSend(exchangeName, "blue", message); }
由于hmall.redirect交换机绑定的两个队列的路由key有red;所以指定了路由key为red的消息能被两个消费者都收到。
而路由key为blue的队列只有direct.queue1;所以只有监听这个队列的消费者1能够接收到消息;
消息接收
在consumer服务的SpringRabbitListener中添加方法:
/* 监听 direct.queue1 队列的消息 */ @RabbitListener(queues = "direct.queue1") public void listenDirectQueue1(String message) { System.out.println("【消费者1】接收到消息: " + message ); } /* 监听 direct.queue2 队列的消息 */ @RabbitListener(queues = "direct.queue2") public void listenDirectQueue2(String message) { System.out.println("【消费者2】接收到消息: " + message ); }
Direct交换机和Fanout交换机的差异:
- Fanout交换机将消息路由给每一个与之绑定的队列
- Direct交换机根据RoutingKey判断路由给哪个队列
- 如果多个队列具有相同的RoutingKey,则于Fanout功能类似
Topic交换机
概述
Topic类型的Exchange与Ditect相比,都是可以根据RoutingKey把消息路由到不同的队列
只不过Topic类型Exchange可以让队列在绑定RoutingKey的时候使用通配符。
RoutingKey一般都是有一个或多个单词组成,多个单词之间以.分割,列如:item.insert
通配符规则
#
:匹配一个或多个词*
:匹配恰好一个词
列如
item.#
可以匹配item.spu.insert或者item.spuitem.*
只能匹配item.spu
场景
publicsher发送的消息使用的RoutingKey共有四种:
china.news
代表有中国的新闻消息china.weather
代表中国的天气消息japan.news
则代表日本新闻japan.weather
代表日本的天气消息
topic.queue1
:绑定的是china.#
,凡是以china.
开头的routing key
都会被匹配到。包括:
china.news
china.weather
topic.queue2
:绑定的是#.news
,凡是以.news
结尾的routing key
都会被匹配,包括:
china.news
japan.news
消息发送
在publisher服务的SpringAmqpTest类中添加测试方法
/* 测试 topic exchange; 向 hmall.topic 交换机发送消息,路由key为china.news 的消息 */ @Test public void testTopicExchange() { String exchangeName = "hmall.topic"; String message = "中国冰城哈尔滨在这个冬天旅游火爆!"; //发送 路由key 为 china.news 的消息; rabbitTemplate.convertAndSend(exchangeName, "china.news", message); }
消息接收
在consumer服务的SpringRabbitListener中添加方法
/* 监听 topic.queue1 队列的消息 */ @RabbitListener(queues = "topic.queue1") public void listenTopicQueue1(String message) { System.out.println("【消费者1】接收到消息: " + message ); } /* 监听 topic.queue2 队列的消息 */ @RabbitListener(queues = "topic.queue2") public void listenTopicQueue2(String message) { System.out.println("【消费者2】接收到消息: " + message ); }
Direct交换机与Topic交换机的差异
- Topic交换机接收的消息RoutingKey必须是多个单词,以
.
分割 - Topic交换机与队列绑定时的RoutingKey可以指定通配符
#
:代表0个或者多个词*
:代表1个词
代码声明队列和交换机
基本API
SpringAMQP提供了一个Queue类,用来创建队列
public class Queue extends AbstractDeclarable implements Cloneable{}
SpringAMQP还提供了一个Exchange接口,来表示所有不同类型的交换机,我们可以自己创建队列和交换机,不过SpringAMQP还提供了ExchangeBuilder来简化这个过程。
在绑定队列和交换机的时候,需要BindingBuilder来创建Binding对象
fanout示例
在consumer服务中创建一个配置类,FanoutConfig,声明队列和交换机
package com.itheima.consumer.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.FanoutExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FanoutConfig { /* 声明fanout类型交换机 */ @Bean public FanoutExchange fanoutExchange() { return new FanoutExchange("hmall.fanout"); } /* 声明队列,名称为 fanout.queue1 */ @Bean public Queue fanoutQueue1() { return new Queue("fanout.queue1"); } /* 绑定队列和交换机 */ @Bean public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) { return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange); } /* 声明队列,名称为 fanout.queue2 */ @Bean public Queue fanoutQueue2() { return new Queue("fanout.queue2"); } /* 绑定队列和交换机 */ @Bean public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange) { return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange); } }
运行完之后,可以在控制台中查看是否自动创建了对应交换机,队列,以及相互绑定。
direct示例
在consumer中创建一个配置类,DirectConfig,声明队列和交换机,direct模式要绑定多个KEY,会非常麻烦,每一个Key都要编写一个binding。
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 DirectConfig { /* 声明direct类型交换机 */ @Bean public DirectExchange directExchange(){ return new DirectExchange("hmall.direct"); } /* 声明队列,名称为 direct.queue1 */ @Bean public Queue directQueue1() { return new Queue("direct.queue1"); } /* 绑定队列和交换机;路由key 为 red */ @Bean public Binding directBinding1(Queue directQueue1, DirectExchange directExchange) { return BindingBuilder.bind(directQueue1).to(directExchange).with("red"); } /* 绑定队列和交换机;路由key 为 blue */ @Bean public Binding directBinding2(Queue directQueue1, DirectExchange directExchange) { return BindingBuilder.bind(directQueue1).to(directExchange).with("blue"); } /* 声明队列,名称为 direct.queue2 */ @Bean public Queue directQueue2() { return new Queue("direct.queue2"); } /* 绑定队列和交换机;路由key 为 red */ @Bean public Binding directBinding3(Queue directQueue2, DirectExchange directExchange) { return BindingBuilder.bind(directQueue2).to(directExchange).with("red"); } /* 绑定队列和交换机;路由key 为 yellow */ @Bean public Binding directBinding4(Queue directQueue2, DirectExchange directExchange) { return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow"); } }
运行之后,就可以看到自动创建了对应交换机,队列,以及相互绑定
基于注解声明
基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明,不过是在消息监听的时候基于注解的方式来声明。
列如我们同样声明Direct模式的交换机和队列,修改SpringRabbitListener中对应的listenDirectQueue1和listenDirectQueue2两个方法
/* 监听 direct.queue1 队列的消息 */ @RabbitListener(bindings = @QueueBinding( value = @Queue("direct.queue1"), exchange = @Exchange(value = "hmall.direct", type = ExchangeTypes.DIRECT), key = {"red", "blue"} )) public void listenDirectQueue1(String message) { System.out.println("【消费者1】接收到消息: " + message ); } /* 监听 direct.queue2 队列的消息 */ @RabbitListener(bindings = @QueueBinding( value = @Queue("direct.queue2"), exchange = @Exchange(value = "hmall.direct", type = ExchangeTypes.DIRECT), key = {"red", "yellow"} )) public void listenDirectQueue2(String message) { System.out.println("【消费者2】接收到消息: " + message ); }
消息转换器
Spring的消息发送代码接收的消息体是一个Object:
而在数据传输时,它会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节发序列化为Java对象
只不过,在默认情况下Spring采用的序列化方式是JDK序列化,但是JDK序列化存在以下问题
- 数据体积过大
- 有安全漏洞
- 可读性差
配置JSON转换器
添加依赖
JDK序列化方式并不合适,我们希望消息体的体积更小,可读性更高,因此可以使用JSON方式来做序列化和反序列化。
在publisher
和consumer
两个服务中都引入依赖
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
注意:如果项目中已经引入spring-boot-starter-web
依赖,则无需再次引入Jackson
依赖
配置消息转换器
配置消息转换器,在publisher
和consumer
两个服务的启动类中添加一个Bean即可
package com.itheima.publisher; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class PublisherApplication { public static void main(String[] args) { SpringApplication.run(PublisherApplication.class); } @Bean public MessageConverter messageConverter() { //1、定义消息转换器 Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); //2、配置每条消息自动创建id;用于识别不同消息,也可以在页面中基于id判断是否是重复消息 jackson2JsonMessageConverter.setCreateMessageIds(true); return jackson2JsonMessageConverter; } }
package com.itheima.consumer; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } @Bean public MessageConverter messageConverter() { //1、定义消息转换器 Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); //2、配置每条消息自动创建id;用于识别不同消息,也可以在页面中基于id判断是否是重复消息 jackson2JsonMessageConverter.setCreateMessageIds(true); return jackson2JsonMessageConverter; } }
本文作者:奕帆卷卷
本文链接:https://www.cnblogs.com/yifan0820/p/18041860
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理