RabbitMQ
本文介绍了JMS的方式去操作MQ,帮助大家更快了解RabbitMQ。
同时主要使用Springboot的方去教学,帮助大家快速的开发。
本文章是详细介绍RabbitMQ使用的一篇文章,博主总结了CSDN上面两个大佬的文章和黑马程序员的视频。
第一个文章:RabbitMQ快速入门 这一篇文章是采用的JMS的方式来操作RabbitMQ,JMS类似于JDBC,对我们熟悉RabbitMQ有很大的帮助。
第二个文章:RabbitMQ详解,用心看完这一篇就够了 这一篇文章主要是采用SpringBoot 的方式进行演示代码,对实际开发很有帮助。
消息队列
消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传递。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。
消息队列是一种应用间的异步协作机制,同时消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。
RabbitMQ
RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue )的开源实现,官网地址:http://www.rabbitmq.com。RabbitMQ作为一个消息代理,主要负责接收、存储和转发消息,它提供了可靠的消息机制和灵活的消息路由,并支持消息集群和分布式部署,常用于应用解耦,耗时任务队列,流量削锋等场景。
首先,了解RabbitMQ首先要了解一下JMS和AMQP是什么?
JMS
JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
JMS就是API,就相当于JDBC,JDBC我们都知道是Java操作关系型数据库的API(最原始的开发就是使用JDBC来完成数据库的增删改查。而JMS就是Java操作MQ的API,现在大多消息队列都实现了这个东西,都能用Java的方式去操作它。
AMQP
JMQP(advanced message queuing protocol)是一种协议,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。这使得实现了AMQP的provider天然性就是跨平台的。
什么意思,协议就是规定了你该怎么做,但是并不在意你的细节。我们可以参考HTTP协议,HTTP只是一个协议,但是你可以通过Java来实现他,也可以使用python做为web程序的后端。
AMQP就是规定了MQ该怎么传输的协议,例如RabbitMQ就是实现了这个协议的一个产品。
简单来说:RabbitMQ实现了AMQP协议,然后我们可以通过JMS去操作它
原理介绍
我们幻想一个场景,我们自己要来实现一个消息队列,应该怎么设计呢?
最简单的方式,就是我们放一个队列在这里,然后生产者放消息,消费者拿消息。
但是这样会有一个问题,只有一个队列,只能放置一种消息,如果消息种类多了岂不是不行了。所以我们进行改造:让生产者和消费者直接有多个队列
然后又出现一个问题,生产者放置消息的时候,怎么知道放在哪个队列呢?如果队列很多,那么我放置消息的时候是不是要一一对应每一个消息队列去放置。
所以我们在生产者和队列之间增加一个交换机Exchange
,然后让交换机跟队列产生绑定binding
我只管给交换机发送消息,然后交换机通过跟队列的binding自行将消息放到队列中去。
但是又出现了问题,如果我们编程实现了上面的设计,那么一个模型只有一个交换机,每个业务都要单独启动用一个这个模型,所以我们希望自己设计的MQ要高效一点,要通用一点。
所以我们增加了一个概念,叫虚拟主机Virtual host
,这个虚拟主机就是用来逻辑上面隔离的。一个虚拟主机中能有多个交换机,然后每个主机之间是互相不干扰的。现在多个业务就能同时使用我们设计的模型了
以上就是RabbitMQ的简单模型:
-
Publisher:消息发送者,将消息发送到Exchange并指定RoutingKey,以便queue可以接收到指定的消息。
-
Consumer:消息消费者,从queue获取消息,一个Consumer可以订阅多个queue以从多个queue中接收消息。
-
Server:一个具体的MQ服务实例,也称为Broker。
-
Virtual host:虚拟主机,一个Server下可以有多个虚拟主机,用于隔离不同项目,一个Virtualhost通常包含多个Exchange、Message Queue。
-
Exchange:交换器,接收Producer发送来的消息,把消息转发到对应的Message Queue中。
-
Message Queue:实际存储消息的容器,并把消息传递给最终的Consumer。
RabbitMQ的安装
这里直接跳过,我们引入其他博主的文章,都是最简单的安装(一直下一步)
安装成功后记得回来
rabbitMQ的使用
RabbitMQ有管理页面,和代码操作的使用
管理页面稍微点两下,就能清楚怎么操作,我们着重讲代码使用
RabbitMQ管理页面介绍
安装好后,RabbitMQ是开机默认启动的,所以我们打开RabbitMQ的管理页面:http://localhost:15672/,默认的账号和密码都是guest。
可以看到管理页面上面有几个分类:
1)OverView:有RabbitMQ的一些基本信息
2)Connection:链接,不管是消费者还是生产者,都会跟RabbitMQ产生链接
3)Channel:通道,产生链接之后,要发送消息和接受消息,需要建立通道
4)Exchanges:交换机,点进去可以看到有多种类型的交换机(直连交换机、主题交换机等)
5)Queues:队列,就是运行时候的真正的消息队列
6)Admin:管理,可以管理用户、管理虚拟主机、管理集群等。
这里是关于用户和虚拟主机的创建的截图,一般是创建一个用户,然后分配虚拟主机,然后再进行使用。
RabbitMQ的消息模型
详情可见官方文档的入门文档:RabbitMQ Tutorials — RabbitMQ
主要就是分为如下的五种,这里你们只需要记住有这五种就行了,后面我会一一带领大家去操作
1)基本消息模型和工作消息模型:都是直接基于消息队列的方式进行发送和接受消息的
2)基于发布订阅模式:广播消息模型、路由消息模型、主题消息模型
入门代码——基本消消息模型
这里我们导入依赖,来进行MQ的操作
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.1</version>
</dependency>
流程是:
1)创建链接connection
2)通过连接获取通道channel
3)使用通道声明队列
注意:消费者和生产者的前三步都是一样的
4)生产消息/消费消息
生产者代码
值得注意的是,发送的消息要求是字节的形式,所以在发送消息的时候要转化为字节
@Test
public void testSendMessage() throws IOException, TimeoutException {
//1.1创建链接工厂
ConnectionFactory factory = new ConnectionFactory();
//1.2设置信息
factory.setHost("localhost");//RabbitMQ的ip地址
factory.setPort(5672);//端口地址
factory.setVirtualHost("/");//虚拟主机(可以在管理页面创建)
factory.setUsername("guest");//用户名和密码:也可以在管理页面创建
factory.setPassword("guest");
//1.3获取链接
Connection connection = factory.newConnection();
//2获取通道
Channel channel = connection.createChannel();
//3创建队列
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare("simple.queue", false, false, false, null);
//4发送消息
String message = "hello";
/**
* basicPublish()方法参数明细:
* 1、exchange,交换机,如果不指定将使用mq的默认交换机(设置为"")
* 2、routingKey,路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3、props,消息的属性
* 4、body,消息内容
*/
channel.basicPublish("","simple.queue",null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("消息发送成功:"+message);
//5关闭通道和链接
channel.close();
connection.close();
}
运行程序之后:可以看到已经有了一条消息
消费者代码
消费者进行消费消息,主要是依靠DefaultConsumer这个类实现的,所以我们使用匿名内部类的方式进行消息的消费
@Test
public void testConsumeMessage() throws IOException, TimeoutException {
//1.1创建链接工厂
ConnectionFactory factory = new ConnectionFactory();
//1.2设置信息
factory.setHost("localhost");//RabbitMQ的ip地址
factory.setPort(5672);//端口地址
factory.setVirtualHost("/");//虚拟主机(可以在管理页面创建)
factory.setUsername("guest");//用户名和密码:也可以在管理页面创建
factory.setPassword("guest");
//1.3获取链接
Connection connection = factory.newConnection();
//2获取通道
Channel channel = connection.createChannel();
//3创建队列
channel.queueDeclare("simple.queue", false, false, false, null);
//4消费消息
/**
* basicConsume()方法参数明细
* 1.消息队列的名字(要跟发送方保持一样)
* 2.是否是自动确认机制:ACK机制,选择true进行自动确认
* 3.怎么消费这个消息:用匿名内部类的方式实现
*/
channel.basicConsume("simple.queue",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("收到的消息是:"+message);
}
});
}
消息已经被消费了
入门案例就是典型的JMS实现方式。我们可以借助JDBC来进行理解,例如:我们在操作MySql的时候,也是先获取连接工厂、获取连接、获取操作数据库的对象、进行操作。而这里也是差不多的形式:获取连接工厂、创建连接、获取通道、定义队列、消息操作。简直是一模一样的流程。
但是,可以看到入门案例过程十分繁琐,每次我都要重复操作一些东西就很浪费时间,做了跟业务无关的操作这个是得不偿失的。就像操作数据库一样,Spring也给我们准备了关于消息队列的模板AMQP。
Spring—AMQP
Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来进行发送和接收消息。
就像我们使用redisTemplate一样,定义好了模板。然后实现Springt-AMQP的就是Spring-Rabbit。
来自官网的介绍
Features:
Listener container for asynchronous processing of inbound messages
RabbitTemplate for sending and receiving messages
RabbitAdmin for automatically declaring queues, exchanges and bindings
翻译过来就是,SpringAMQP具有的特点有:
-
用于异步处理入站消息的侦听器容器
就是一直在监听,有消息就消费,没有就继续监听,方便得很
-
RabbitTemplate发送和接收消息的模板
之前发送和接收消息,需要我们手动去转成字节,手动去发送和接收,烦的一
-
RabbitAdmin,用于自动声明队列,交换和绑定
以前声明队列都是我们手动去生成的,特别不方便
简单队列
我们来实现刚刚的入门案例,用Spring的方式实现:简单队列模式
我们依旧使用上面例子中的队列名:simple.queue
一、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
二、编写yml配置
这里我们生产者和消费者都配置一样的参数
spring:
rabbitmq:
host: localhost #主机名
port: 5672 #MQ的端口号
virtual-host: / #虚拟主机名
username: guest
password: guest
三、创建队列
我们创建一个配置类来进行队列的创建
注意:如果创建不成功,是因为入门代码那边已经创建了一个simple.queue队列,请先删除再创建
@Configuration
public class QueueConfiguration {
@Bean
public Queue simpleQueue(){
return new Queue("simple.queue");//队列的名字
}
}
四、使用RabbitTemplate发送消息
只需要调用rabbitTemplate.convertAndSend方法,就能够成功发送消息了
@SpringBootTest
public class testProvider {
@Autowired
RabbitTemplate rabbitTemplate;
@Test
public void testSendMessage(){
String queueName = "test.queue";
String message = "我在学习RabbitMQ";
rabbitTemplate.convertAndSend(queueName,message);
}
}
五、客户端接收消息
定义一个类,然后使用注解的方式来进行监听消息,真实开发场景可以定义成一个Service
@Service
public class MessageListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String message){
System.out.println("收到的消息是:"+message);
}
}
然后启动我们的springboot项目,因为这个是监听,所以要一直启动状态
五、测试基本消息队列
1)发送方一直发送消息
2)查看消费方的控制台
发送Object对象
我们发送消息,不仅仅能够发送String类型,还能发送任意对象,SpringAMQP会帮我们自动转换。
注意:传递的对象一定要实现Serializable接口
我们这里创建一个object.queue的队列,然后给他发送Person对象,再监听输出
@Bean
public Queue ObjectQueue(){
return new Queue("object.queue");
}
@Test
public void testSendObject(){
Person person = new Person();
person.setName("black_pp");
person.setSex("boy");
person.setAge("永远18岁");
rabbitTemplate.convertAndSend("object.queue",person);
}
@RabbitListener(queues = "object.queue")
public void listenObject(Person person){
System.out.println("收到的对象是:"+person);
}
测试,对象成功的转发了:
消息转换器
上面传递的对象实际上在RabbitMQ中表现形式是这种的,一个小小的对象,要占用很多字符,所以我们这里可以使用JSON的格式来序列化对象。
我们需要重新配置一个消息转换器(发送方和接收方都需要配置),这里配置的是JSON格式的消息转换器。
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
这时候,我们在发送和接收对象类型,就不需要实现Serializable接口了,并且在RabbitMQ中的表现形式也是JSON的格式,非常的友好。
工作队列
工作队列就是消费者不止一个,众多消费者之间是合作关系;假如消息发送者,每秒有50条消息,但是一个消费者每秒只能消费30条消息,这就需要多个消费者构成一个工作队列了。能提高消息消费的速度。
我们改造一下发送方的代码:让他一秒发50条数据
@Test
public void testWorkQueue() throws InterruptedException {
String queueName = "simple.queue";
for (int i = 1; i <= 50; i++) {
rabbitTemplate.convertAndSend(queueName,"这是第 "+i+" 条消息");
Thread.sleep(20);//每次休息20秒,总共50次,正好是一秒50条消息
}
}
然后在消费者这边,定义两个监听来模拟有多个消费者
@Service
public class MessageListener {
private static int numA = 0;
private static int numB = 0;
@RabbitListener(queues = "simple.queue")
public void listenWorkQueueA(String message) throws InterruptedException {
numA++;
System.out.println("【A】收到的消息是:"+message+",已经消费的数量是:"+numA);
}
@RabbitListener(queues = "simple.queue")
public void listenWorkQueueB(String message) throws InterruptedException {
numB++;
System.err.println("【B】收到的消息是:"+message+",已经消费的数量是:"+numB);
}
}
最后的结果是:两个消费者轮流进行消费
但是我们希望的是,谁消费更快,谁拿更多的消息,不采用这种方式,那该怎么办呢?
消费预取机制
这个机制就是说,在我们消费者消费之前,我们进行预先轮流取消息,然后再消费
例如:
A在真正消费之前,就开始取消息:1、3、5、7、9....
B在真正消费之前,也开始同时取消息:2、4、6、8....
这样就是说,我还没开始消费,就已经预定了,所以不管我消费速度如何,这些消息都是我来消费
改进方案
怎么解决这个问题呢?让消息处理更快的拿去到更多的消费信息,我们可以将消费预取的长度设置为1
两个消费者预取到一条数据之后,就开始执行,执行完毕又再取一条数据,这样就是谁执行越快,取到的消息越多。
1)我们在消费者这边,让A和B每次执行之后都休眠
A休眠50毫秒,一秒大概能消费20条数据
B休眠30毫秒,一秒大概能消费30条数据左右
@RabbitListener(queues = "simple.queue")
public void listenWorkQueueA(String message) throws InterruptedException {
numA++;
System.out.println("【A】收到的消息是:"+message+",已经消费的数量是:"+numA);
Thread.sleep(30);
}
@RabbitListener(queues = "simple.queue")
public void listenWorkQueueB(String message) throws InterruptedException {
numB++;
System.err.println("【B】收到的消息是:"+message+",已经消费的数量是:"+numB);
Thread.sleep(50);
}
2)在消费者这边,修改application.yml文件
添加:listener.simple.prefetch = 1
spring:
rabbitmq:
host: localhost #主机名
port: 5672 #MQ的端口号
virtual-host: / #虚拟主机名
username: guest
password: guest
listener:
simple:
prefetch: 1 #每次只预先取一个数据
3)测试结果:消费能力快的,消费得更多
发布订阅模式
之前的案例中,我们的消息是什么被消费了,就删除了。消息A消费了一个消息,那个消息就被直接删除了,如果B也想消费那个消息,显然是做不到了的。
想象一个例子:我们购买了一个商品,然后订单消息会发送给:物流系统、商家平台等。如果使用之前的模式,那么物流系统拿到订单消息之后,直接把消息删除了,商家就拿不到该订单的消息了,那商家就不能进行发货处理。
所以我们得使用发布——订阅模式
这里假设consumer1和consumer2都是物流系统,consumer3是商家系统,只要同时给这两个队列发送相同的消息,就能解决上面的问题。实现的方式就是加上交换机。
交换机有多个种类:
-
fanout:广播交换机
-
direct:路由交换机
-
topic:话题交换机
广播交换机
FanoutExchange—广播交换机:会把自己接收到的每一条消息,都转发给所有跟自己绑定的消息队列。
实现的思路就是:创建队列、创建交换机、给他们进行绑定,然后就是消费者监听,生产者发送。
一、创建交换机和队列并进行绑定
这里我们在消费者中创建配置类:FanoutConfiguration,然后再配置类中使用@Bean
注解生成需要的队列和交换机等
注意:这里Queue的类型是:org.springframework.amqp.core.Queue;
@Configuration
public class FanoutConfiguration {
/**
*创建交换机对象,取名叫:test.fanout
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("test.fanout");
}
/**
*创建两个队列
*/
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
/**
* 创建绑定对象:分别绑定交换机跟队列
*/
@Bean
public Binding binding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Binding binding2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
二、更改发送方
之前我们使用RabbitTemplate的时候传参是队列名称和消息
现在我们传参变为:交换机名称、路由键、消息
-
因为我们现在是发消息给交换机了,所以我们这边需要交换机名字(刚刚配置文件中的名字)
-
因为这是广播的方式,所以不需要路由,中间那个参数填空字符串就行(RabbitMQ遵循默认的规则)
@Test
public void testFanoutExchange() throws InterruptedException {
String exchangeName = "test.fanout";
String message = "hello,fanoutExchange";
rabbitTemplate.convertAndSend(exchangeName,"",message);
}
三、编写接收方
接收方和之前的代码并没有区别,只是监听的队列变了
@RabbitListener(queues = "fanout.queue1")
public void listenFanout1(String message){
System.out.println("从队列fanout.queue1中拿到消息:"+message);
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanout2(String message){
System.out.println("从队列fanout.queue2中拿到消息:"+message);
}
测试:发送方发送一条消息,结果两个客户端都收到了同一个消息
路由交换机
路由交换机,顾名思义就是能够对消息进行路由,交换机会根据一个值,把消息路由到指定的队列中去
执行流程是:
1)你发送消息给交换机,并指定路由的key
2)如果key的值是cat,交换机则转发到queue1;如果是dog,交换机则转发到queue2。
注意:如果key值相等,那么两个队列都会收到消息。
一、创建交换机和队列,并绑定
在广播交换机的时候,我们使用配置类的方式来声明交换机、队列、绑定关系。
这里我们直接在消费端的 @RabbitListener
注解中完成上面的任务,进一步方便开发。
注意:一个队列可以指定多个路由key
/**
* 1.在RabbitListener中指定bindings属性
* 2.在QueueBinding中指定,要绑定的交换机、队列、和key
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "directExchange",type = ExchangeTypes.DIRECT),
key = "cat"
)
)
public void listenDirect1(String message){
System.out.println("从队列路由key为cat的队列中拿到消息:"+message);
}
二、发送方发送消息,指定路由key
@Test
public void testFanoutExchange() throws InterruptedException {
String exchangeName = "directExchange";
String message = "hello,fanoutExchange";
rabbitTemplate.convertAndSend(exchangeName,"cat",message);
}
测试结果:指定路由规则之后,正常接收到消息
话题交换机
话题交换机跟上面的路由交换机是一样的,路由的时候都指定一个路由key,然后进行路由。
特点:
-
话题交换机的路由key是用
.
分割的例如:xxx.xxx 的形式
-
话题交换机支持通配符
#代表0个或者多个单词
*代表一个单词
例如:
用 user.#代表路由key,就能匹配所有user开头的队列。
用#.log表示路由key,就能匹配所有跟日志相关的队列。
一、声明交换机、队列和绑定关系
还是跟上面一样,在消费端直接使用注解来声明
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "topic.exchange",type = ExchangeTypes.TOPIC),
key = "user.#"
)
)
public void listenTopic1(String message){
System.out.println("收到关于user的话题消息:"+message);
}
二、发送方发送消息
发送方发送两个消息,一个路由key是user.active,一个key是user.order
@Test
public void testFanoutExchange() throws InterruptedException {
String exchangeName = "topic.exchange";
String message = "用户的订单";
rabbitTemplate.convertAndSend(exchangeName,"user.order",message);
}
测试:发送两个不同的路由key,但是都能接收到