RabbitMQ01
一、MQ简介
1、同步和异步通讯
微服务间的通讯有同步和异步两种方式:
1、同步通讯:需要实时响应。
2、异步通讯:不需要实时响应。
2、同步调用的问题
微服务间基于Feign的调用就属于与同步方式,存在一些问题。
1、耦合度高: 每次加入新的需求,都要修改原来的代码。
2、性能下降: 调用者需要等待服务提供响应者,如果调用链过长,则响应的时间等于每次调用的时间之和。
3、资源浪费: 调用链中的每个服务都在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度
浪费系统资源。
4、级联失败: 如果服务提供者出现问题,所有调用方都会跟着出现问题,会迅速导致整个微服务集群故障。
3、异步调用
1、简介
1、异步调用常见实现就是事件驱动模式。
2、事件是指系统硬件或软件的状态出现任何重大改变。
2、优点
1、耦合度低 : 每个服务都可以灵活插拔,可替换
2、吞吐量提升 : 无需等待订阅者处理完成,响应更快速
3、故障隔离 : 服务没有直接调用,不存在级联失败问题
4、流量削峰 : 不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件
5、调用间没有阻塞,不会造成无效的资源占用
3、缺点
1、依赖于Broker的可靠性、安全性、吞吐能力
2、架构复杂,业务没有明显的流程线,不好追踪管理
4、MQ简介
MQ(MessageQueue),消息队列,存放的消息的队列。也就是事件驱动架构中的Broker。
1、追求可用性:Kafka、 RocketMQ 、RabbitMQ
2、追求可靠性:RabbitMQ、RocketMQ
3、追求吞吐能力:RocketMQ、Kafka
4、追求消息低延迟:RabbitMQ、Kafka
二、RabbitMQ的概述
一、MQ的基本结构
RabbitMQ中的一些角色:
1、publisher:生产者
2、consumer:消费者
3、exchange:交换机,负责消息路由
4、queue:队列,存储消息
5、virtualHost:虚拟主机,隔离不同租户的exchange、queue、消息的隔离
三、RabbitMA安装
1、启动容器
systemctl start docker
2、获取mq.tar的方式
1、在线拉取
docker pull rabbitmq:3.8-management
2、从本地加载
上传到虚拟机中后,使用命令加载镜像即可:
docker load -i mq.tar
3、安装
执行下面的命令来运行MQ容器:
docker run \
-e RABBITMQ_DEFAULT_USER=cwl \
-e RABBITMQ_DEFAULT_PASS=root \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3.8-management
15672 浏览器访问端口
5672 java代码连接的时候使用的端口
docker run 跑一个容器
-e 环境变量
-d 后台启动
rabbitmq 镜像名字
3.8-management 版本
五、RabbitMQ消息模型
1、常见的RabbitMQ消息模型
1、基本消息队列(BasicQueue)
2、工作消息队列(WorkQueue)
3、发布订阅(Publish、Subscribe),又根据交换机类型不同分为三种:
1、Fanout Exchange:广播
2、Direct Exchange:路由
3、Topic Exchange:主题
2、基于最基础的消息队列模型的实现
基于最基础的消息队列模型来实现的,只包括三个角色:
1、publisher:消息发布者,将消息发送到队列queue
2、queue:消息队列,负责接受并缓存消息
3、consumer:订阅队列,处理队列中的消息
1、publisher实现
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory factory = new ConnectionFactory();
//1.1 设置连接参数 主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.211.128");
factory.setPort(5672);
// 虚拟主机对资源做逻辑的分组 代表 用 / 表示
factory.setVirtualHost("/");
factory.setUsername("cwl");
factory.setPassword("root");
//1.2 建立连接
Connection connection = factory.newConnection();
//2. 创建通道Channel
Channel channel = connection.createChannel();
//3. 创建队列
String queueName ="simple.queue1";
// 声明 参数一 队列名称, 参数二 是否持久化 参数三 是否独占
// 参数四 是否自动删除 参数五 额外的参数
channel.queueDeclare(queueName,false,false,false,null);
//4. 发送消息
String message="hello rabbitmq!";
// 执行发送消息的动作 参数一 交换机 参数二 队列名 参数三 是否带参数
// 参数四 发送消息的内容
channel.basicPublish("",queueName,null,message.getBytes());
System.out.println("发送消息成功:【"+message+"】");
//5. 关闭通道
channel.close();
connection.close();
}
}
2、consumer实现
public class ConsumerTest {
@Test
public void testAcceptMessage() throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory factory = new ConnectionFactory();
//1.1 设置连接参数 主机名、端口、vhost、用户名、密码
factory.setHost("192.168.211.128");
factory.setPort(5672);
// 虚拟主机对资源做逻辑的分组 代表 用 / 表示
factory.setVirtualHost("/");
factory.setUsername("cwl");
factory.setPassword("root");
//1.2 建立连接
Connection connection = factory.newConnection();
//2. 创建通道channel
Channel channel = connection.createChannel();
//3. 创建队列 (声明队列 如果有队列 则声明, 如果没有队列,则创建出来)
String queueName = "simple.queue";
// 声明 参数一 队列名称, 参数二 是否持久化 参数三 是否独占
//参数四 是否自动删除 参数五 额外的参数
channel.queueDeclare(queueName, false, false, false, null);
//4. 订阅消息
// 参数一 队列名 参数二 是否自动应答 参数三 消费者
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
// 该方法 专门接受到消息, 并进行处理消息对应的业务
@Override
public void handleDelivery(
// 参数一 对应消费者名字
String consumerTag,
//参数二 其他的属性
Envelope envelope,
// 参数三 对应与生产者发送消息是否带的参数
AMQP.BasicProperties properties,
// body 消息体(生产者发送的消息)
byte[] body) throws IOException {
//5. 处理消息
String message=new String(body);
System.out.println("接收到的消息【" + message + "】");
}
});
System.out.println("等待接收消息...");
}
}
3、总结
基本消息队列的消息发送流程:
1、建立connection
2、创建channel
3、利用channel声明队列
4、利用channel向队列发送消息
基本消息队列的消息接收流程:
1、建立connection
2、创建channel
4、利用channel声明队列
5、定义consumer的消费行为handleDelivery()
6、利用channel将消费者与队列绑定
3、SpringAMQP
1、简介
SpringAMQP是基于RabbitMQ封装的一套模板,并且还利SpringBoot对其实现了自动装配,
使用起来非常方便。
SpringAmqp的官方地址https://spring.io/projects/spring-amqp
AMQP:Advanced Message Queuing Protocol,是用于在应用程序之间传递业务消息的
开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。
Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,
其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现。
2、实现基础消息队列功能的流程
1、在父工程中引入spring-amqp的依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、在publisher服务中利用RabbitTemplate发送消息到simple.queue队列
@SpringBootTest(classes = PublisherApplication.class)
public class SpringAmqpTest {
@Autowired
// 模板类 里面封装了发送消息的方法
private RabbitTemplate rabbitTemplate;
//发送消息
@Test
public void sendHellWorld(){
// 参数一 指定要发送消息到的队列的名称 参数二 指定要发送的消息
rabbitTemplate.convertAndSend("helloworld","hello world spring ampq");
}
}
3、在cnsumer服务中编写消费逻辑,绑定simple.queue队列
1、消息接收,配置MO地址,在consumer服务的application.yml添加配置
logging:
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
rabbitmq:
host: 192.168.211.128
port: 5672
virtual-host: /
username: cwl
password: root
2、在consumer服务的com.cwl.mq.listener包中新建一个类SpringRabbitListener
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "helloworld") // queues 制定的是要监听的消息队列名称
public void acceptMessage(String msg){
System.out.println("msg = " + msg);
}
}
3、SpringAMQP如何接收消息?
1、引入amqp的starter依赖
2、配置RabbitMQ地址
3、定义类,添加@Component注解
4、类中声明方法,添加@RabbitListener注解,方法参数就时消息
注意:消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能
4、Work Queue工作队列
1、简介
Work queue 工作队列,可以提高消息处理速度,避免队列消息堆积,也被称为(Task queue),
任务模型,让多个消费者绑定一个队列,共同消费队列中的消息。
2、work queue实现一个队列绑定多个消费者的流程
1、在publisher服务中定义测试方法,每秒产生50条消息,送到simple.queue
2、在consumer服务中定义两个消费者监听者,都监听simple.queue队列
3、消费者1每秒处理50条消息,消费者2每秒处理10条消息
3、work queue一个队列绑定多个消费者的具体实现
1、在publisher服务中添加一个测试方法,循环发送50条消息到simple.queue队列
@SpringBootTest(classes = PublisherApplication.class)
public class SpringAmqpWorkQueueTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testWorkQueue() throws InterruptedException {
String queueName = "helloworld";
String message="hello message";
for (int i = 0; i < 50; i++) {
rabbitTemplate.convertAndSend(queueName,message+i);
Thread.sleep(20);
}
}
}
2、在consumer服务中添加两个消费者,也监听simple.queue
@Component
public class SpringRabbitListener {
// 消费者1
@RabbitListener(queues = "helloworld") // queues 制定的是要监听的消息队列名称
public void workQueueMessage1(String msg) throws InterruptedException {
System.out.println("msg = " + msg);
Thread.sleep(20);
}
// 消费者2
@RabbitListener(queues = "helloworld") // queues 制定的是要监听的消息队列名称
public void workQueueMessage2(String msg) throws InterruptedException {
System.err.println("msg = " + msg);
Thread.sleep(100);
}
}
3、消费预取限制
修改consumer中的application.yml文件,设置preFetch这个值,可以控制预取消息的上限。
logging:
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
rabbitmq:
host: 192.168.211.128
port: 5672
virtual-host: /
username: cwl
password: root
listener:
simple:
prefetch: 1 # 设置预取机制的数量 每一次只能获取一个消息 消费完了之后才能下次再取
4、Work模型的使用
1、多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
2、通过设置prefetch来控制消费者预取的消息数量 默认是250
5、发布(Publish)、订阅(Subscribe)
1、简介
发布订阅模式就是允许将同一消息发送给多个消费者,实现方式是加入了exchange(交换机)
常见的exchange类型:
1、Fanout 广播
2、Direct 路由
3、Topic 主题
在订阅模型中,多了一个exchange角色:
1、Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
2、Exchange:交换机.一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递
交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange
的类型。
3、Exchange有以下3种类型:
1、Fanout:广播,将消息交给所有绑定到交换机的队列
2、Direct:定向,把消息交给符合指定routing key 的队列
3、Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
4、Consumer:消费者,与以前一样,订阅队列,没有变化
5、Queue:消息队列也与以前一样,接收消息、缓存消息。
注: Exchange(交换机)只负责转发消息,不具备存储消息的能力。因此如果没有任何队列
与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失。
2、Fanout的使用
1、实现思路
在广播模式下,消息发送流程:
1) 可以有多个队列
2) 每个队列都要绑定到Exchange(交换机)
3) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
4) 交换机把消息发送给绑定过的所有队列
5) 订阅队列的消费者都能拿到消息
在广播模式下,消息发送流程的具体实现:
1、在consumer服务中,利用代码声明队列、交换机,并将两者绑定。
2、在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
3、在publisher编写测试方法,向cwl.fanout发送消息
1、在consumer中创建一个类,声明队列和交换机:
@Configuration
public class FanoutConfig {
//1. 创建一个交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("cwl.fanout");
}
//2. 创建两个队列
@Bean
public Queue queue1(){
//设置队列名称
return new Queue("fanout.queue1");
}
@Bean
public Queue queue2(){
//设置队列名称
return new Queue("fanout.queue2");
}
//3. 绑定2个队列到同一个交换机
@Bean
public Binding binding1(){
return BindingBuilder.bind(queue1()).to(fanoutExchange());
}
@Bean
public Binding binding2(){
return BindingBuilder.bind(queue2()).to(fanoutExchange());
}
}
2、在publisher服务的SpringAmqpTest类中添加测试方法:
@SpringBootTest(classes = PublisherApplication.class)
public class FanoutTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void send(){
String msg = "fanout消息内容";
// 参数一 指定要发送的交换机的名称
// 参数二 指定routingke 这里就是空字符串 (Fanout模式就是这样)
// 参数三 指定发送的消息内容
rabbitTemplate.convertAndSend("cwl.fanout","",msg);
}
}
3、在consumer服务的SpringRabbitListener中添加两个方法,作为消费者:
@Component
public class SpringRabbitListener {
// 消费者1
@RabbitListener(queues = "fanout.queue1") // queues 制定的是要监听的消息队列名称
public void fanoutMessage1(String msg) throws InterruptedException {
System.out.println("消费者1 msg = " + msg);
}
// 消费者2
@RabbitListener(queues = "fanout.queue2") // queues 制定的是要监听的消息队列名称
public void fanoutMessage2(String msg) throws InterruptedException {
System.err.println("消费者1 msg= " + msg);
}
}
4、总结
1、交换机的作用是什么?
1.接收publisher发送的消息
2.将消息按照规则路由到与之绑定的队列
3.不能缓存消息,路由失败,消息丢失
4. FanoutExchange的会将消息路由到每个绑定的队列
2、声明队列、交换机、绑定关系的Bean是什么?
1.Queue
2.FanoutExchange
3.Binding
3、Direct使用
1、简介
1、Direct Exchange 会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes)。
2、每一个Queue都与Exchange设置一个BindingKey
3、发布者发送消息时,指定消息的RoutingKey
4、Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
在Direct模型下:
1、队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
2、消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey。
3、Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,
只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息.
2、实现思路
1、利用@RabbitListener声明Exchange、Queue、RoutingKey
2、在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
3、在publisher中编写测试方法,向cwl.direct发送消息
3、具体实现
1、在consumer的SpringRabbitListener中添加两个消费者,同时基于注解来声明队列和交换机
@Component
public class SpringRabbitListener {
// 消费者1
@RabbitListener(
bindings = @QueueBinding(
value = @Queue("direct.queue1"),
exchange = @Exchange(value = "cwl.direct", type = ExchangeTypes.DIRECT),
key = {"blue","red"}
)
)
public void directMessage1(String msg) throws InterruptedException {
System.out.println("消费者1 msg = " + msg);
}
// 消费者2
@RabbitListener(
bindings = @QueueBinding(
value = @Queue("direct.queue2"),
exchange = @Exchange(value = "cwl.direct", type = ExchangeTypes.DIRECT),
key = {"yellow","red"}
)
)
public void directMessage2(String msg) throws InterruptedException {
System.err.println("消费者2 msg= " + msg);
}
}
2、在publisher服务的SpringAmqpTest类中添加测试方法
@SpringBootTest(classes = PublisherApplication.class)
public class directTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void send(){
String msg = "direct消息内容";
//参数一 指定要发送的交换机的名称 参数二 指定的routingkey 需要指定将这个消息发给谁 blue
rabbitTemplate.convertAndSend("cwl.direct","blue",msg);
}
}
3、总结
1、描述下Direct交换机与Fanout交换机的差异?
1.Fanout交换机将消息路由给每一个与之绑定的队列
2.Direct交换机根据RoutingKey判断路由给哪个队列
3.如果多个队列具有相同的RoutingKey,则与Fanout功能类似
2、基于@RabbitListener注解声明队列和交换机有哪些常见注解?
1.@Queue
2.@Exchange
4、Topic使用
1、简介
1、TopicExchange与DirectExchange类似,都是可以根据RoutingKey把消息路由到不同的队列,
区别在于routingKey必须是多个单词的列表,并且以 . 分割。
2、Queue与Exchange指定BindingKey时可以使用通配符:
1. #:代指0个或多个单词
2. *:代指一个单词
2、实现思路
1、并利用@RabbitListener声明Exchange、Queue、RoutingKey
2、在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
3、在publisher中编写测试方法,向cwl.topic发送消息
3、具体实现
1、在publisher服务的SpringAmqpTest类中添加测试方法:
@SpringBootTest(classes = PublisherApplication.class)
public class topicTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void send(){
String msg = "topic消息内容 gaotian";
String msg2 = "topic消息内容 gaotian";
String msg3 = "topic消息内容 新闻";
// 参数一 指定要发送的交换机的名称
// 参数二 指定的routingkey 需要指定将这个消息发给谁 blue
rabbitTemplate.convertAndSend("cwl.topic","china.gaotian",msg);
rabbitTemplate.convertAndSend("cwl.topic","china.gaotian",msg2);
rabbitTemplate.convertAndSend("cwl.topic","hanggou.news",msg3);
}
}
2、在consumer服务的SpringRabbitListener中添加方法
@Component
public class SpringRabbitListener {
// 消费者1
@RabbitListener(
bindings = @QueueBinding(
value = @Queue("topic.queue1"),
exchange = @Exchange(value = "cwl.topic", type = ExchangeTypes.TOPIC),
key = {"china.#"}
)
)
public void topicMessage1(String msg) throws InterruptedException {
System.out.println("消费者1 msg = " + msg);
}
// 消费者2
@RabbitListener(
bindings = @QueueBinding(
value = @Queue("topic.queue2"),
exchange = @Exchange(value = "cwl.topic", type = ExchangeTypes.TOPIC),
key = {"#.news"}
)
)
public void topicMessage2(String msg) throws InterruptedException {
System.err.println("消费者2 msg= " + msg);
}
}
3、总结
描述下Direct交换机与Topic交换机的差异?
1、Topic交换机接收的消息RoutingKey必须是多个单词,以 **.** 分割
2、Topic交换机与队列绑定时的bindingKey可以指定通配符
3、#:代表0个或多个词
4、*:代表1个词
5、消息转换器
1、简介
1、Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现
是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。
2、默认情况下Spring采用的序列化方式是JDK序列化。众所周知,JDK序列化存在下列问题:
1、数据体积过大
2、有安全漏洞
3、可读性差
3、如果要修改只需要定义一个MessageConverter 类型的Bean即可。推荐用JSON方式序列化。
JSON的体积更小、可读性更高.
2、配置JSON转换器的步骤
1、在publisher和consumer两个服务中都引入依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
2、在publisher和consumer两个服务中的启动类上添加一个Bean即可
@Bean
public MessageConverter messageConverter(){
// 将对象转换为JSON 和将 JSON转换对象
return new Jackson2JsonMessageConverter();
}