目录
5. Publish/Subscribe发布订阅(Fanout)
一、RabbitMQ介绍
1. MQ介绍
Message Queue消息队列
1 同步调用与异步调用
程序里所有的通信,有两种形式:
-
同步通信:通信时必须得到结果才会执行下一步
-
异步通信:通信时不必等待结果,可以直接处理下一步
同步调用
同步调用的缺点:
-
业务链长,消耗时间增加,用户体验不好
-
耦合性强
-
流量洪峰服务器压力大
同步调用的好处:
-
时效性强,可以立即得到结果
异步调用
异步调用的好处:
-
异步调用,调用链短,用户等待时间短,体验好
-
降低耦合,服务之间耦合性低了,任何一个服务出现问题,对其它服务的影响都非常小
-
削峰填谷,中间件Broker具备一定的消息堆积与缓存能力,下游服务可以根据自己的能力慢慢处理
异步调用的坏处:
-
业务复杂度增加:需要考虑数据一致性问题、消息丢失问题、消息重复消费问题、消息的顺序问题等等
-
架构复杂度增加:对中间件Broker的依赖性增强了,必须保证Broker的高可用;一旦Broker出错,会对整个系统造成非常大的冲击
2. MQ介绍
什么是MQ
Message Queue,消息队列
消息中间件利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下扩展进程间的通信。
MQ的作用
-
异步:实现服务之间异步通信
-
削峰:实现流量的削峰填谷
-
解耦:实现服务之间的耦合性降低
3. 常见的MQ
在使用MQ时:
-
追求可用性:Kafka、 RocketMQ 、RabbitMQ,都支持集群部署
-
追求可靠性:RocketMQ(或RabbitMQ)
-
追求吞吐能力:Kafka(或RocketMQ)
-
追求消息低延迟:RabbitMQ(或Kafka)
2. RabbitMQ介绍
1 AMQP协议
AMQP,Advanced Message Queuing Protocol,高级消息队列,是一种网络协议。它是应用层协议的一个开发标准,为面向消息的中间件而设计。
基于此协议的客户端与消息中间件可传递消息,并不受客户端、中间件不同产品、不同编程语言的限制。
-
Publisher:消息发布者
-
Exchange:交换机。用于分发消息
-
Routes:路由。消息被分发到目标队列上的过程
-
Queue:消息队列。用于存储消息数据
-
Consumer:消息消费者。从队列里取出消息、处理消息
2 RabbitMQ
Rabbit公司基于AMQP协议标准,开发了RabbitMQ1.0。RabbitMQ采用Erlang语言开发,Erlang是专门为开发高并发和分布式系统的一种语言,在电信领域广泛使用。
官网地址:RabbitMQ: One broker to queue them all | RabbitMQ
-
Broker:消息中间件,指的就是RabbitMQ服务器
-
Virtual Host:虚拟主机。当多个不同用户使用同一个RabbitMQ服务时,可以划分出多个vhost,每个用户在自己的vhost内创建exchange和queue等,互不干扰
-
Connection:消息生产者、消费者 与 RabbitMQ之间建立的TCP连接
-
Channel:Channel作为轻量级的Connection,可以极大的减少建立Connection的开销。
如果每一次访问RabbitMQ都建立一个TCP Connection,将会造成巨大的性能开销,性能非常低下。
Channel是在Connection内部建立的逻辑连接,Channel之间完全隔离。如果应用程序支持多线程,通常每个线程创建独立的Channel进行通讯, AMQP method包含了channel id,帮助客户端和broker识别channel,所以channel之间是完全隔离的
-
Exchange:message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中去。常用的类型有:direct(point-to-point), topic(publish-subscribe),fanout(multicast)
-
Queue:消息最终被送到这里,等待Consumer取走
-
Binding:exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange的查询表中,用作message的分发依据
3. 小结
MQ是什么:是消息队列
MQ的好处:
-
异步:服务之间实现异步通信(异步交互传输数据)
-
削峰:可以堆积消息,应用流量洪峰
-
解耦:服务之间的耦合性降低了
MQ的缺点:
-
业务复杂度增加:防止消息丢失、消息重复,要保证数据的一致性等等问题
-
系统架构复杂度增加:必须保证MQ的高可用
RabbitMQ的几个概念:
-
Producer:生产者,是发送消息的代码
-
Consumer:消费者,是接收消息的代码
-
Broker:中间件,指的就是RabbitMQ
-
Exchange:交换机,作用是路由消息到队列
-
Queue:队列,真正存储消息的队列
-
VirtualHost:虚拟主机。每个虚拟主机里可以有多个交换机和队列, 不同虚拟主机之间互相隔离
二、RabbitMQ安装
1. 拉取RabbitMQ镜像
方式一:在线拉取镜像
docker pull rabbitmq:3.8-management
方式二:从本地加载镜像【我们采用这种方式】
把资料中的《mq.tar》上传到虚拟机CentOS里
在CentOS里执行命令加载镜像:
#先切换到mq.tar所在的目录
#再执行命令加载镜像:已经加载过了,不需要重复加载
docker load -i mq.tar#加载后,查看一下镜像。找一下有没有rabbitmq这个镜像
docker images
2. 安装RabbitMQ
执行下面的命令来运行MQ容器:
docker run \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
-v mq-plugins:/plugins \
--name mq \
--hostname mq \
-p 15672:15672 \
-p 5672:5672 \
-d \
--restart=always \
rabbitmq:3-management
-
Java程序连接RabbitMQ:使用端口5672
-
RabbitMQ控制台页面: http://ip:15672, 登录帐号:itcast, 密码:123321
3. RabbitMQ控制台
打开浏览器输入地址:http://ip:15672, 登录帐号:itcast, 密码:123321
在控制台里,可以查看、管理 交换机、队列等等
三、SpringAMQP
1. 准备代码环境
SpringAMQP是基于RabbitMQ封装的一套模板,并且还利用SpringBoot对其实现了自动装配,使用起来非常方便。
SpringAmqp的官方地址:Spring AMQP
-
AMQP:规定了 一个MQ技术应该实现的规范
-
SpringAMQP:是 Java代码收发消息的 API规范
SpringAMQP提供了三个功能:
-
自动声明队列、交换机及其绑定关系
-
基于注解的监听器模式,异步接收消息
-
封装了RabbitTemplate工具,用于发送消息
为了演示SpringAMQP的功能,我们需要先准备代码环境,步骤如下:
-
创建project,删除其src目录,然后添加依赖
-
创建生产者模块
-
创建消费者模块
1 创建project
创建maven类型的project,不选择骨架,直接设置工程坐标然后下一步
创建后,删除src目录
修改pom.xml添加依赖坐标如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.9.RELEASE</version>
</parent><properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties><dependencies>
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency><!--单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency><dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
2 创建生产者模块
在project里创建module,起名称为demo-producer
依赖
不需要添加依赖,父工程里已经导入了依赖,子模块直接继承父工程里的依赖就足够了
配置
修改配置文件application.yaml
spring:
application:
name: demo-producer
rabbitmq:
host: 192.168.200.137 #RabbitMQ服务的ip
port: 5672 #RabbitMQ服务的端口
username: itcast #RabbitMQ的帐号
password: 123321 #RabbitMQ的密码
引导类
创建引导类,没有什么特殊的要求
package com.itheima;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoProducerApplication {
public static void main(String[] args) {
SpringApplication.run(DemoProducerApplication.class, args);
}
}
3 创建消费者模块
在project里创建module,起名称为demo-consumer
依赖
不需要添加依赖,父工程里已经导入了依赖,子模块直接继承父工程里的依赖就足够了
配置
修改配置文件application.yaml
spring:
application:
name: demo-producer
rabbitmq:
host: 192.168.200.137 #RabbitMQ服务的ip
port: 5672 #RabbitMQ服务的端口
username: itcast #RabbitMQ的帐号
password: 123321 #RabbitMQ的密码
引导类
package com.itheima;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class DemoConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(DemoConsumerApplication.class, args);
}
}
2. RabbitMQ工作模式
RabbitMQ提供了6种工作模式,参考:RabbitMQ Tutorials | RabbitMQ
-
basic queue:简单模式
-
work queues:工作队列集群消费
-
Publish/Subscribe:发布订阅模式,也称为Fanout,是一种消息广播模式
-
Routing:路由模式,也称为Direct模式
-
Topics:主题模式
-
RPC远程调用模式:远程调用,其实算不上MQ,这里不做介绍
无论哪种模式,可能都需要用到交换机、队列、交换机与队列的绑定。如何声明这些队列和交换机?
-
在控制台页面上直接创建。不经常用
-
使用@Bean的方式,声明交换机、队列、绑定关系
-
使用注解方式,可以在消费者一方直接声明交换机、队列、绑定关系,适合于复杂的绑定关系声明
3. basic queue简单队列【了解】
1 模式说明
basic queue是RabbitMQ中最简单的一种队列模式:生产者把消息直接发送到队列queue,消费者从queue里直接接收消息。
2 使用示例
生产者
package com.itheima;
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 DemoProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 简单模式:发送消息示例
*/
@Test
public void test01Simple(){
rabbitTemplate.convertAndSend("demo01.simple.queue", "hello,simple queue");
}
}
消费者
-
创建一个类,用于监听消息。类上需要添加@Component注解
-
类里定义一个方法,用于处理消息。
方法上需要添加注解
@RabbitListener(queuesToDeclare = @Queue("队列名称"))
方法上需要添加一个
String
类型的形参:是接收到的消息内容
package com.itheima.listener;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class Demo01SimpleListener {
@RabbitListener(queuesToDeclare = @Queue("demo01.simple.queue"))
public void listen(String msg){
System.out.println("msg = " + msg);
}
}
3 测试
先启动消费者监听消息,再由生产者发送消息
因为:我们在消费者代码里声明了队列,只有先启动消费者,才会在RabbitMQ里创建队列,然后才能正常发送消息
4 注意事项
要想使用RabbitMQ收发消息,必须要保证已经有队列和交换机已经存在,才可以正常收发。
-
可以直接在RabbitMQ控制台里创建队列、交换机和绑定关系。 然后再启动代码收发消息,先启动生产者或先消费者都行
-
可以在生产者一方使用@Bean声明队列、交换机和绑定关系。然后必须先启动生产者服务发送消息,再启动消费者监听消息
-
可以在消费者一方使用@RabbitListener声明队列、交换机和绑定关系。然后必须先启动消费者监听消息,再运行生产者发送消息
4. work queues工作队列【了解】
假如只有一个消费者处理消息,那么处理消息的速度就有可能赶不上发送消息的速度。该如何同时处理更多的消息呢?
可以在同一个队列上创建多个竞争的消费者,以便消费者可以同时处理更多的消息
1 模式说明
多个消费者相互竞争,从同一个队列里获取消息。生产者发送的消息将被所有消费者分摊消费
注意: 一个队列里的一条消息,只能被消费一次,不可能多个消费者同时消费处理
对于任务过重,或者任务较多的情况,使用工作队列可以提高任务处理的速度
例如:短信通知服务。 订单完成后要发短信通知
2 示例代码
生产者
在测试类里发送消息
@Test
public void test02WorkQueue(){
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("demo02.work.queue","hello, 这是消息"+i);
}
}
消费者
package com.itheima.listener;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class Demo02WorkQueueListener {
@RabbitListener(queuesToDeclare = @Queue("demo02.work.queue"))
public void listener1(String msg){
System.out.println("消费者1收到消息msg = " + msg);
}
@RabbitListener(queuesToDeclare = @Queue("demo02.work.queue"))
public void listener2(String msg){
System.out.println("消费者2收到消息msg = " + msg);
}
}
3 注意事项
在WorkQueues模式的默认情况下,一个队列里的所有消息,将平均分配给每个消费者。这种情况并没有考虑到消费者的实际处理能力,显然是有问题的。
例如:生产者发送了50条消息,有两个消费者,各接收到了25条消息。假如
-
消费者1,每秒能处理100条消息。 很快就能处理完消息
-
消费者2,每秒能处理10条消息。 消息堆积越来越多
要解决这个问题其实非常简单:让每个消费者一次性只拉取1条消息
修改消费者的配置文件application.yaml:
spring:
rabbitmq:
listener:
simple:
prefetch: 1 #消费者一次抓取几条消息
5. Publish/Subscribe发布订阅(Fanout)
在上一章节中,我们创建了一个工作队列。工作队列背后的假设是,每个任务只传递给一个消费者。在这一节中,我们将做一些完全不同的事情——我们将向多个消费者传递一条消息。这种模式称为“发布/订阅”。
1 模式说明
-
P:生产者,用于发送消息。但是生产者要把消息发给交换机(X)
-
X:Exchange交换机
-
接收消息,接收生产者发送的消息
-
处理消息,把消息投递给某个或某些队列,或者把消息丢弃。具体会如何操作,由Exchange类型决定:
-
Fanout:广播,把消息交给绑定的所有队列(交换机绑定了哪些队列,就把消息投递给这些队列)
-
Direct:定向,把消息交给符合指定routing key的队列
-
Topic:通配符,把消息交给符合routing pattern的队列
-
-
注意:交换机只负责转发消息,不具备存储消息的能力。所以如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,消息将会丢失
-
-
Queue:消息队列,接收消息、缓存消息
-
C:消费者,等待消息、处理消息
2 示例代码
为了说明这种模式,我们将构建一个简单的日志系统。它将由两个程序组成:
-
生产者程序将发出日志消息
-
消费者程序将接收日志消息
-
第一组消费者,接收到日志消息并保存到磁盘上
-
第二组消费者,接收到日志消息并打印到控制台
-
生产者
@Test
public void test03Fanout(){
//参数1:交换机名。参数2:路由key。参数3:消息内容
rabbitTemplate.convertAndSend("demo03.fanout.exchange","demo03.key", "这是一条广播消息");
}
消费者
package com.itheima.listener;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class Demo03FanoutListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue("demo03.queue1"),
exchange = @Exchange(value = "demo03.fanout.exchange", type = ExchangeTypes.FANOUT)
))
public void listener1(String msg){
System.out.println("消费者1收到消息msg = " + msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("demo03.queue2"),
exchange = @Exchange(value = "demo03.fanout.exchange", type = ExchangeTypes.FANOUT)
))
public void listener2(String msg){
System.out.println("消费者2收到消息msg = " + msg);
}
}
6. Direct(Routing)
在上一个章节中,我们构建了一个简单的日志系统。我们能够向许多消费者广播日志消息。
在本节中,我们将向其添加一个功能:我们将使消费者能够仅订阅消息的子集。例如:
-
只能将关键错误消息定向到日志文件(以节省磁盘空间)
-
同时仍然能够在控制台上打印所有日志消息。
1 模式说明
-
队列在绑定交换机时,需要给队列指定一个Routing Key(路由key)
-
生产者在发送消息时,必须指定消息的Routing Key
-
交换机根据消息的RoutingKey进行判断:只有队列的RoutingKey 与 消息的RoutingKey完全相同,才会收到消息
2 示例代码
生产者
@Test
public void test04Direct(){
rabbitTemplate.convertAndSend("demo04.direct.exchange","demo04.error", "这是error消息");
rabbitTemplate.convertAndSend("demo04.direct.exchange","demo04.info", "这是info消息");
}
消费者
package com.itheima.listener;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class Demo04DirectListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue("demo04.direct.queue2"),
exchange = @Exchange(value = "demo04.direct.exchange", type = ExchangeTypes.DIRECT),
key = {"demo04.info", "demo04.error"}
))
public void listener1(String msg){
System.out.println("消费者1(监听demo04.erorr和demo04.info)收到消息msg = " + msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("demo04.direct.queue1"),
exchange = @Exchange(value = "demo04.direct.exchange", type = ExchangeTypes.DIRECT),
key = {"demo04.error"}
))
public void listener2(String msg){
System.out.println("消费者2(监听demo04.erorr)收到消息msg = " + msg);
}
}
7. Topic【重点】
在上一个章节中,我们改进了日志系统。我们没有使用仅能进行消息广播的FANOUT
,而是使用了DIRECT
,实现了了有选择地接收日志。
虽然使用DIRECT
改进了我们的系统,但它仍然有局限性——它不能基于多个标准进行路由,例如:
-
第一组消费者,要接收所有系统的所有日志消息,打印到控制台
-
第二组消费者,要接收所有系统的错误日志消息,和订单系统的所有日志消息,保存到磁盘
为了在日志系统中实现这一点,我们需要了解更复杂的TOPIC
交换机。
1 模式说明
-
RoutingKey:发送到
TOPIC
的消息不能有任意的routing键,它:-
必须是由点分隔的单词列表
-
可以有任意多个单词,最多255个字节
-
可使用
*
星号,匹配一个单词 -
可使用
#
,匹配0个或多个单词
-
-
使用特定RoutingKey发送的消息,将被传递到使用匹配Key绑定的所有队列。
2 使用示例
生产者
@Test
public void test05Topic(){
rabbitTemplate.convertAndSend("demo05.topic.exchange","order.info", "这是一条订单普通消息");
rabbitTemplate.convertAndSend("demo05.topic.exchange","order.error", "这是一条订单错误消息");
}
消费者
package com.itheima.listener;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class Demo05TopicListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue("demo05.queue1"),
exchange = @Exchange(value = "demo05.topic.exchange", type = ExchangeTypes.TOPIC),
key = "order.*"
))
public void listener1(String msg){
System.out.println("消费者1(监听order.*)收到消息msg = " + msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue("demo05.queue2"),
exchange = @Exchange(value = "demo05.topic.exchange", type = ExchangeTypes.TOPIC),
key = "*.error"
))
public void listener2(String msg){
System.out.println("消费者2(监听*.error)收到消息msg = " + msg);
}
}
8. 小结
要使用SpringAMQP收发消息,准备工作:
-
添加spring-amqp的依赖坐标
-
无论是生产者还是消费者,都需要:配置RabbitMQ的信息
spring:
rabbitmq:
host: 192.168.200.130 #RabbitMQ服务的ip
port: 5672 #RabbitMQ服务的端口
username: itcast #RabbitMQ的帐号
password: 123321 #RabbitMQ的密码
virtual-host: / #RabbitMQ的VirtualHost,如果不设置,就使用默认的/
简单模式收发消息:
-
发消息:
rabbitTemplate.convertAndSend("队列名称", 消息内容)
-
收消息:在任意一个bean对象里增加方法
@RabbitListener(queueToDeclare=@Queue("队列名称"))
public void listener(String msg){
//msg就是收到的消息内容
}
工作队列收发消息:一个队列可以有多个消费者,共同处理消息。提升消费能力,处理消息堆积
-
发消息:
rabbitTemplate.convertAndSend("队列名称", 消息内容)
-
收消息:在任意一个bean对象里增加多个方法,每个方法就是一个消费者
@RabbitListener(queueToDeclare=@Queue("队列名称"))
public void listener1(String msg){
//msg就是收到的消息内容
}@RabbitListener(queueToDeclare=@Queue("队列名称"))
public void listener2(String msg){
//msg就是收到的消息内容
}
有交换机以后,生产者发消息用的方法是:
//发消息时的路由key必须是精确的,不要加通配符
rabbitTemplate.convertAndSend("交换机名", "路由key", 消息内容);
有交换机以后,消费者收消息的配置:
-
告诉RabbitMQ,首先:如果没有队列、没有交换机,要帮我们自动创建
FanoutExchange,扇出,用于发布订阅模式(广播模式)。一条消息可以被多个消费者接收
DirectExchange,路由,用于Routing模式。一条消息根据路由key,发送到RoutingKey相等的队列上
TopicExchange,用于Topic模式。一条消息,根据路由key,发送到RoutingKey匹配的队列上
-
告诉RabbitMQ,然后:xxx交换机,你把符合路由key的消息,路由到指定的队列里。我要监听这个队列
@RabbitListener(bindings=@QueueBinding(
value = @Queue("队列名称"),
exchange = @Exchange(value="交换机名称", type=ExchangeTypes.交换机类型),
key = {"路由key1", "路由key2"}
))
public void listener(String msg){
}
四、其它内容
1. @Bean方式声明队列和交换机【掌握】
1 队列和交换机的声明方式
要使用RabbitMQ发送消息的话,就必须提前声明好队列和交换机。
而声明队列和交换机的方式是多种多样的:
-
手动创建:在RabbitMQ控制台页面上,直接手动创建队列和交换机,并进行绑定
这种方式需要在控制台上页面创建并绑定,然后再编写程序,不太方便
-
@Bean方式:使用@Bean的方式声明交换机和队列,在程序启动运行时,由代码进行声明
这种方式配置比较麻烦
-
注解方式:使用注解方式声明交换机和队列,在监听消息时,由代码进行声明
使用相对简单,在监听消息时,一个注解综合性配置消息队列、交换机并进行绑定
2 @Bean声明方式的示例
我们以使用最多的Topic为例,演示一下@Bean方式的交换机与队列声明
生产者声明队列和交换机
package com.itheima.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DemoRabbitConfig {
/**
* 声明一个名称为demo.topic.exchange的交换机
*/
@Bean
public TopicExchange topicExchange(){
return ExchangeBuilder.topicExchange("demo.topic.exchange").build();
}
/**
* 声明一个名称为demo.topic.queue1的队列
*/
@Bean
public Queue topicQueue1(){
return QueueBuilder.durable("demo.topic.queue1").build();
}
/**
* 声明一个名称为demo.topic.queue2的队列
*/
@Bean
public Queue topicQueue2(){
return QueueBuilder.durable("demo.topic.queue2").build();
}
/**
* 将交换机demo.topic.exchange
* 和队列demo.topic.queue1
* 绑定起来,路由key是:demo.*
*/
@Bean
public Binding topicQueue1Binding(Queue topicQueue1, TopicExchange topicExchange){
return BindingBuilder.bind(topicQueue1).to(topicExchange).with("demo.*");
}
/**
* 将交换机demo.topic.exchange
* 和队列demo.topic.queue2
* 绑定起来,路由key是:#.key
*/
@Bean
public Binding topicQueue2Binding(Queue topicQueue2, TopicExchange topicExchange){
return BindingBuilder.bind(topicQueue2).to(topicExchange).with("#.key");
}
}
生产者发送消息
@Test
public void test05(){
rabbitTemplate.convertAndSend("demo.topic.exchange", "demo.1", "消息demo.1");
rabbitTemplate.convertAndSend("demo.topic.exchange", "xxx.key", "消息xxx.key");
}
消费者监听消息
package com.itheima.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class Demo06Listener {
@RabbitListener(queues = "demo.topic.queue1")
public void listener1(String msg){
System.out.println("消费者1(从队列demo.topic.queue1)收到消息msg = " + msg);
}
@RabbitListener(queues = "demo.topic.queue2")
public void listener2(String msg){
System.out.println("消费者2(从队列demo.topic.queue2)收到消息msg = " + msg);
}
}
测试
先运行消费者代码,开始监听队列。如果队列不存在,会自动创建并进行绑定
再运行生产者代码,发送消息
2. 消息json格式转换器
1 说明
使用RabbitTemplate发送消息时,Spring会采用JDK的序列化技术对消息内容进行序列化:
-
生产者发出的消息内容会被序列化成字节数组,再发送出去。
-
消费者收到的消息,也是字节数组,Spring会进行反序列化还原
而JDK的序列化技术存在一些问题:
-
序列化的字节数组,通常体积比较大
-
序列化和反序列化容易产生安全漏洞
-
可读性差
我们可以配置一个消息转换器:把消息转换成json格式字符串发送出去,消费者接收到json
2 示例
1. 添加json转换的依赖坐标
注意:需要在生产者和消费者双方都添加。我们这里可以直接添加到父工程的pom.xml里
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
2. 配置消息转换器
注意:需要在生产者和消费者双方都添加。我们在两个模块的引导类里添加:
/**
* 配置json消息转换器,不要导错了:
* org.springframework.amqp.support.converter.MessageConverter
* org.springframework.amqp.support.converter.Jackson2JsonMessageConverter
*/
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
3. 测试
-
关闭消费者:暂时不接收消息,等发送消息后,我们要先去控制台上查看消息内容的格式
-
生产者发送消息
package com.itheima;
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;
import java.util.HashMap;
import java.util.Map;
@SpringBootTest
public class Demo07SerializeTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void test(){
Map<String, Object> msg = new HashMap<>();
msg.put("id", 2);
msg.put("title", "山东要热成灿东了 高温黄色预警高挂局部可达39℃");
rabbitTemplate.convertAndSend("serialize.queue", msg);
}
}
3. 在RabbitMQ控制台上查看消息:查看收到的消息,是json格式的
4. 启动消费者,接收到消息内容
package com.itheima.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
@Slf4j
@Component
public class Demo07SerializeListener {
@RabbitListener(queuesToDeclare = @Queue("serialize.queue"))
public void handleSerializeQueue1(Map<String,Object> msg){
log.info("从{}接收到消息:{}", "serialize.queue1", msg);
}
}
3. 小结
使用@Bean的方式声明队列、交换机和绑定关系:
发消息 发消息
//声明交换机
@Bean
public XxxExchange exchange(){
return ExchangeBuilder.xxxExchange("交换机名").build();
}
//声明队列
@Bean
public Queue queue(){
return QueueBuilder.durable("队列名称").build();
}
//声明队列和交换机的绑定关系
@Bean
public Binding queueBinding(XxxExchange exchange, Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("路由key通配符");
}
发消息
rabbitTemplate.convertAndSend("交换机名", "消息的路由key", 消息内容);
收消息
@RabbitListener(queueToDeclare=@Queue("队列名"))
public void listener(String msg){
}
消息转换器:
-
如果不配置消息转换器:默认情况是
生产者把消息对象序列化成字节,传输到MQ。MQ里存储的字节码;消费者收到字节码后反序列化成Java对象
-
可以配置json格式的消息转换器:
生产者把消息对象转换成json字符串,传输到MQ。MQ存储的是json字符串。消费者收到json字符串再还原成Java对象
大总结
什么是MQ:是消息队列
MQ的作用:服务之间数据交互时的一种方式。和Feign对比
-
异步:实现服务之间的异步通信
-
削峰:MQ可以堆积消息,可以应对流量洪峰
-
解耦:实现服务之间耦合性的进一步降低
注意:MQ消息队列和即时通信是两种不同的技术
MQ:用于服务之间的异步数据交互。和Feign对比,Feign是同步通信,MQ是异步通信
即时通信:用于聊天的,单聊、群聊等等。通常要借助于第三方服务,比如:环信云,融联云等等
常见的MQ:
-
RabbitMQ:性能好,延迟低
-
RocketMQ:稳定可靠,可以做到消息0丢失
-
Kafka:吞吐量大,可以实现海量数据交互传输,通常用于大数据领域
RabbitMQ实现了AMQP协议
-
AMQP协议是:高级消息队列协议,规定了MQ应该遵循的规范。规定了
生产者:发送消息的一方
中间件:Broker,指的是MQ
交换机:Exchange,用于把消息路由到队列
队列:Queue,真正可以堆积缓存消息的队列
消费者:接收消息的一方
-
RabbitMQ实现了AMQP协议:也有以上AMQP的角色,除此之外还有
虚拟主机VirtualHost:同一虚拟主机里的队列和交换机可以绑定。不同虚拟主机之间是互相隔离的
Java操作RabbitMQ收发消息:SpringAMQP
-
是Spring提供一套API规范,用于收发消息的。默认提供了操作RabbitMQ的类和方法
RabbitMQ的消息模式:
-
简单队列:生产者直接发消息到MQ队列;消费者直接从MQ队列里接收消息
-
工作队列:生产者直接发消息到MQ队列;可以有多个消费者直接从MQ队列里接收消息
可以增加一个队列的消费能力,快速处理消息
-
广播模式:生产者发送的消息,可以被多个消费者同时都收到
生产者把消息发送到FanoutExchange交换机;
FanoutExchange把消息路由到绑定的所有队列
每个队列都可以有一个消费者接收消息
-
路由模式:生产者发送的消息,可以自主决定被哪些消费者接收到
生产者把消息发送到DirectExchange交换机;
DirectExchange根据消息的RoutingKey,路由到 相等的队列上
每个队列都可以有一个消费者接收消息
-
Topic模式:生产者发送的消息,可以自主决定被哪些消费者接收到
生产者把消息发送到DirectExchange交换机;
DirectExchange根据消息的RoutingKey,路由到 路由key相匹配的 队列上
每个队列都可以有一个消费者接收消息
以Topic模式为例,收发消息:@RabbitListener声明交换机和队列
发消息
rabbitTemplate.convertAndSend("交换机名", "路由key", 消息内容);
收消息
@RabbitListener(binding=@QueueBinding(
value = @Queue("队列名称"),
exchange = @Exchange(value="交换机名称", type=ExchangeType.交换机类型),
key = {"a.*", "#.b"}
))
public void listener(String msg){
}
以Topic模式为例,收发消息:@Bean声明交换机和队列
-
声明队列和交换机
@Configuration
public class RabbitConfig{
@Bean
public TopicExchange exchange(){
return ExchangeBuilder.topicExchange("交换机名").build();
}
@Bean
public Queue queue(){
return QueueBuilder.durable("队列名称").build();
}
@Bean
public Binding binding(TopicExchange exchange, Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("a.*");
}
}
发消息
rabbitTemplate.convertAndSend("交换机名", "路由key", 消息内容);
收消息
@RabbitListener(queueToDeclare = @Queue("队列名称"))
public void listener(String msg){
}
消息转换器:可以把消息内容转换成json,通过MQ进行传输
-
先添加依赖坐标:给生产者和消费者都添加
-
再配置消息转换器:给生产者和消费者都配置
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}