初始 MQ
同步调用存在的问题
耦合度高
每次加入新的需求,都要修改原来的代码
性能下降
调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和。
资源浪费
调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
级联失败
如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务群故障
同步调用的优点:
- 时效性较强,可以立即得到结果
同步调用的问题:
- 耦合度高
- 性能和吞吐能力下降
- 有额外的资源消耗
- 有级联失败问题
异步调用方案
异步调用常见实现就是事件驱动模式
当有新的需要订阅事件
优势一:服务解耦
优势二:性能提升,吞吐量提高
优势三:服务没有强依赖,不担心级联失败问题,不浪费资源
优势四:流量削峰
异步通信的优点:
- 耦合度低
- 吞吐量提升
- 故障隔离
- 流量削峰
异步通信的缺点:
- 依赖于Broker的可靠性、安全性、吞吐能力
- 架构复杂了,业务没有明显的流程线,不好追踪管理
什么是 MQ
MQ (MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
几种常见MQ的对比:
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
快速上手 / 安装
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址:www.rabbitmq.com/
使用 docker pull rabbitmq镜像,创建容器并运行
运行容器
- --name my-rabbitmq: 这个参数设置容器的名称为 my-rabbitmq
- -p 5672:5672 5672 端口用于 AMQP 协议通信
- -p 15672:15672: 15672 端口用于 RabbitMQ 管理界面
- -e RABBITMQ_DEFAULT_USER=myuser 设置了 RabbitMQ 的默认用户名和密码
- -e RABBITMQ_DEFAULT_PASS=mypassword
- --hostname my-rabbitmq-host: 设置了容器的主机名为 my-rabbitmq-host。主机名是容器在网络中的标识符
- rabbitmq:3-management: 这是要运行的 Docker 镜像的名称和标签。
bash
docker run \
--name my-rabbitmq \
--hostname my-rabbitmq \
-e rabbitmq_default_user=admin \
-e rabbitmq_default_pass=1234 \
-p 5672:5672 \
-p 15672:15672 \
-d rabbitmq
开启管理平台
bash
docker exec -it my-rabbitmq bash
rabbitmq-plugins enable rabbitmq_management
做完这个,但是无法打开通道,需要修改配置文件
bash
docker exec -it 容器ID bash
cd /etc/rabbitmq/conf.d/
echo management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf
exit
docker restart 容器ID
如果部署到服务器上,那么密码设置的 admin 和1234是不可以访问的
默认是 guest 密码也是 guest
设置访问密码
bash
# 添加账户
rabbitmqctl add_user admin 1234
# 设置角色
rabbitmqctl set_user_tags admin administrator
# 设置权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
管理平台介绍
这是总览页面,包含节点,以及连接信息
连接页面
这里做消息的接收-发送等
在这里做管理。
这里是添加用户,配置角色和密码
但是添加之后,是没有任何的访问权限的,这里有一个虚拟主机的概念,避免两个用户访问时,出现信息错乱。
添加虚拟主机
在用户界面,点击进去,配置虚拟主机。
每一个用户应该具备自己的虚拟主机。将数据隔离开。
消息发送者将消息发送到路由队列,路由队列负责将消息存入到队列中,消费者可以从队列中读取数据。虚拟主机是对路由和队列进行逻辑分组,隔离数据。
RabbitMQ中的几个概念:
- channel:操作MQ的工具
- exchange:路由消息到队列中
- queue:缓存消息
- virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
常见消息模型
MQ的官方文档中给出了5个MQ的Demo示例,对应了几种不同的用法:
- 基本消息队列(BasicQueue)
- 工作消息队列(WorkQueue)
发布订阅(Publish、Subscribe),又根据交换机类型不同分为三种:
- Fanout Exchange:广播
- Direct Exchange:路由
- Topic Exchange:主题
官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:
- publisher:消息发布者,将消息发送到队列queue
- queue:消息队列,负责接受并缓存消息
- consumer:订阅队列,处理队列中的消息
简单的实现队列
依赖
xml
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
发送者、生产者
建立连接
java
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("localhost");
factory.setPort(5672);
factory.setVirtualHost("/"); // 虚拟主机
factory.setUsername("admin");
factory.setPassword("123456");
// 1.2.建立连接
Connection connection = factory.newConnection();
连接之后查看管理界面,发现有一个连接了
创建通道,走完可以看到控制界面有通道了
java
// 2.创建通道Channel
Channel channel = connection.createChannel();
接下来创建队列
java
// 3.创建队列--队列名称
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
发送消息,并关闭通道
java
// 4.发送消息
String message = "hello, rabbitmq!";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:【" + message + "】");
// 5.关闭通道和连接
channel.close();
connection.close();
接收者、消费者
第一步还是建立连接,创建通道、创建队列
接下来我们订阅消息
使用 channel.basicConsume(queueName, true, consumer) 方法订阅指定名称的队列 queueName 中的消息,其中第二个参数 true 表示自动确认消息,即在接收到消息后,消息将被认为已经被消费,因此不需要手动确认。
consumer 是一个实现了 Consumer 接口的对象,用于处理接收到的消息。
在 handleDelivery 方法中,重写了 DefaultConsumer 类的 handleDelivery 方法,用于处理接收到的消息。
java
// 4.订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
基本消息队列的消息发送流程:
- 建立connection
- 创建channel
- 利用channel声明队列
- 利用channel向队列发送消息
基本消息队列的消息接收流程:
- 建立connection
- 创建channel
- 利用channel声明队列
- 定义consumer的消费行为handleDelivery()
- 利用channel将消费者与队列绑定
SpringAMQP
依赖
xml
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
SpringAmqp的官方地址:spring.io/projects/sp...
AMQP(协议)
Advanced Message Queuing Protocol,是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。
Spring AMQP
Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现。
SpringBoot 发送消息,接收消息
配置参数
yml
spring:
rabbitmq:
host: localhost # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: admin # 用户名
password: 123456 # 密码
发送消息
java
@SpringBootTest
public class PublisherTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void name() {
String queueName = "simple.queue";
rabbitTemplate.convertAndSend(queueName, "消息已发送!");
}
}
接收消息
java
@Component
public class SpringRabbitMqListener {
// 监听名为 "simple.queue" 的 RabbitMQ 队列
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg) {
// 当队列中有消息到达时,该方法将被调用,并打印消息内容
System.out.println("msg = " + msg);
}
}
SpringAMQP如何接收消息?
- 引入amqp的starter依赖
- 配置RabbitMQ地址
- 定义类,添加@Component注解
- 类中声明方法,添加@RabbitListener注解,方法参数就时消息
注意:消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能
Work Queue 工作队列
Work queue,工作队列,可以提高消息处理速度,避免队列消息堆积
多个消费者同时处理一个通道的消息
但是如果直接这么设置啊,会出现消费预取限制的问题。他们会提前把消息拿出来,存进去,再处理。也就是平分消息的。这种方式没有考虑到消费者的能力哈,有的消费快,有的消费慢
修改application.yml文件,设置preFetch这个值,可以控制预取消息的上限:
yml
spring:
rabbitmq:
# RabbitMQ 监听器配置
listener:
# 简单消息监听器配置
simple:
# 每个消费者最多预取的消息数量,即消费者在确认之前可以接收的未确认消息的最大数量
prefetch: 1 # 设置为1,表示每个消费者一次只接收一条未确认消息
Work模型的使用:
多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
通过设置prefetch来控制消费者预取的消息数量
发布( Publish )、订阅( Subscribe )
发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机)。
常见exchange类型包括:
- Fanout:广播
- Direct:路由
- Topic:话题
注意:exchange负责消息路由,而不是存储,路由失败则消息丢失
FanoutExchange
SpringAMQP提供了声明交换机、队列、绑定关系的API,例如:
队列绑定交换机
java
@Configuration
public class FanoutConfig {
// 定义一个 FanoutExchange的交换机,名字是kunkun.fanout
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("kunkun.fanout");
}
// 定义一个队列 fanoutQueue1,名字是kun.queue1
@Bean
public Queue fanoutQueue1() {
return new Queue("kun.queue1");
}
// 定义一个绑定,将 kun.queue1 绑定到 kunkun.fanout 的交换机
@Bean
public Binding fanoutBind1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
return BindingBuilder
.bind(fanoutQueue1) // 绑定 fanoutQueue1
.to(fanoutExchange); // 绑定到 fanoutExchange
}
// 绑定多个到交换机......
@Bean
public Queue fanoutQueue2() {
return new Queue("kun.queue2");
}
@Bean
public Binding fanoutBind2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
return BindingBuilder
.bind(fanoutQueue2)
.to(fanoutExchange);
}
}
程序运行,查看控制台
已经有交换机了
查看队列及绑定关系
接下来我们准备程序:
分别是监听名为 "kun.queue1" 和 "kun.queue2" 的队列,接收消息并处理。
java
@Component
public class SpringRabbitMqListener {
// 监听名为 "kun.queue1" 的队列,接收消息并处理
@RabbitListener(queues = "kun.queue1")
public void listenWorkSimpleQueue1(String msg) throws InterruptedException {
System.out.println("消息者1:" + msg); // 打印接收到的消息
}
// 监听名为 "kun.queue2" 的队列,接收消息并处理
@RabbitListener(queues = "kun.queue2")
public void listenWorkSimpleQueue2(String msg) throws InterruptedException {
System.err.println("消息者2:" + msg); // 打印接收到的消息
}
}
发送信息,参数一从队列改成交换机,实现一条消息多个消费者接收。
java
// 发送消息到名为 "kunkun.fanout" 的 FanoutExchange,这会将消息广播到所有绑定到该 Exchange 的队列
@Test
void name() {
rabbitTemplate.convertAndSend("kunkun.fanout", "", "hello ever one!"); // 发送消息
}
交换机的作用是什么?
- 接收publisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- 不能缓存消息,路由失败,消息丢失
- FanoutExchange的会将消息路由到每个绑定的队列
声明队列、交换机、绑定关系的Bean是什么?
- Queue
- FanoutExchange
- Binding
发布订阅-DirectExchange
Direct Exchange 会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes)。
每一个Queue都与Exchange设置一个BindingKey
发布者发送消息时,指定消息的RoutingKey
Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
创建队列及交换机
java
@Component
public class SpringRabbitMqListener {
// 对列名:direct.queue1
// 交换机为:kunkun.direct,类型为路由模式,并将队列帮到到此交换机
// 使用路由键 "red" 和 "yellow" 绑定到队列 "direct.queue1",接收消息并处理
@RabbitListener(bindings =
@QueueBinding(value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "kunkun.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenWorkSimpleQueue1(String msg) {
System.out.println("消息者接收到路由模式的消息:" + msg); // 打印接收到的消息
}
}
发送信息
在参数二中指定key,匹配相同的key的消息队列
java
@SpringBootTest
public class PublisherTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void name() throws InterruptedException {
rabbitTemplate.convertAndSend("kunkun.direct", "blue", "hello ever one!");
}
}
描述下Direct交换机与Fanout交换机的差异?
Fanout交换机将消息路由给每一个与之绑定的队列
Direct交换机根据RoutingKey判断路由给哪个队列
如果多个队列具有相同的RoutingKey,则与Fanout功能类似
基于@RabbitListener注解声明队列和交换机有哪些常见注解?
@Queue
@Exchange
发布订阅-TopicExchange
跟之前的代码是一样的
区别的是概念不一样,再指定key的时候
TopicExchange与DirectExchange类似,区别在于 routingKey 必须是多个单词的列表,并且以 . 分割。
注意:需要将类型改为 ExchangeTypes.TOPIC
Queue与Exchange指定BindingKey时可以使用通配符:
- #:代指0个或多个单词
- *:代指一个单词
java
@RabbitListener(bindings =
@QueueBinding(value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "china.#"))
public void listenTopicQueue1(String msg) {
System.out.println("消费者1接收到Topic消息:【" + msg + "】");
}
消息转换器
在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送。
当我们把数据发送之后,我们看一下控制台
java
@Test
void name() throws InterruptedException {
HashMap<String, Object> map = new HashMap<>();
map.put("name", "刘诗诗");
map.put("age", 21);
rabbitTemplate.convertAndSend("object.queue", map);
}
我们能看到,数据是java序列化之后的。
修改为 JSON 序列化方式
Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter
来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。
如果要修改只需要定义一个MessageConverter 类型的Bean即可。推荐用JSON方式序列化,步骤如下: 我们在publisher服务引入依赖
xml
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
我们在publisher服务声明MessageConverter:
java
// org.springframework.amqp.support.converter;
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
SpringAMQP中消息的序列化和反序列化是怎么实现的?
利用MessageConverter实现的,默认是JDK的序列化
注意发送方与接收方必须使用相同的MessageConverter