MQ概念
MQ,即Message Queue,消息队列。顾名思义,存储消息的队列。
它就像一个邮箱,我们往里面投递邮件,然后就去忙别的了。剩下的事交给邮局,我们知道它最终会把邮件送到收件人手里。
在这里我们的RabbitMQ就是一个邮箱,而我们要投递的消息就是一封邮件。RabbitMQ与现实中邮局的区别在于它不处理纸张,它接收,存储转发二进制数据 - 消息。
为什么用MQ?
-
削峰填谷
业务请求压力曲线就像一座山峰,有瞬时的高峰,也有低谷,服务器配置如果按照高峰压力配置,在低谷时浪费了服务器资源,若按低峰压力配置,则处理不了高峰的请求,导致崩溃。
MQ可以缓存瞬时的高压请求,分散一部分请求压力到空闲期处理。平缓服务器压力曲线,使服务器更加稳定。
-
异步处理
通过将复杂耗时的业务异步处理,提高响应速度。
例如前端用户点击生成一个统计报表,涉及的数据量非常大,可能耗时几十秒,让用户等待体验就很差,可以直接响应“任务下发成功”,后台查询统计处理,等处理好后再修改任务状态。
-
系统解耦
不通服务之间在直接调用的场景下,如果一方修改了地址,那么可能所有连接它的服务都要修改,负担极大。
通过MQ双方不再直接通信,而是统一和MQ打交道,互相看不到对方的存在。协商好格式,后续的服务客户端都不影响现有服务运行。
两种模型
消息队列有两种模型:
-
点对点模型
使用队列(Queue) 作为消息通信载体;满足生产者与消费者模式,一条消息只能被一个消费者使用。
-
发布订阅模型
使用主题(Topic) 作为消息通信载体,类似于广播模式;发布者发布一条消息,该消息通过主题传递给所有的订阅者。
RabbiMQ介绍
RabbitMQ是Erlang语言开发的消息队列,实现了AMQP。
AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是一个提供统一消息服务的应用层标准高级消息队列协议
它源于2004年伦敦的一个金融部门,当时他们想要去解决分布式环境下的通信问题,意识到消息通信才是分布式计算的解决方案。
名词与角色
-
Broker
接收和分发消息的应用代理,如RabbitMQ服务器。
-
Connection
publisher/consumer 和 broker 之间的 TCP 连接 。
-
Channel
Channel 是在 connection 内部建立的逻辑连接,极大减少建立TCP连接的开销 。
-
Exchange
消息交换机,根据分发规则,根据匹配规则,分发消息到 queue 中去 。
-
Queue
消息最终存储的位置,等待 consumer 取走 。
-
Binding
绑定,它的作用就是把 Exchange 和 Queue 按照路由规则绑定起来。
使用Docker安装RabbitMQ
-
拉取镜像
docker pull rabbitmq -
运行
docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq -
安装管理界面插件
# 查看运行的容器 docker ps # 进入容器 docker exec -it 镜像ID /bin/bash # 打开rabbitmq_management的插件 rabbitmq-plugins enable rabbitmq_management # 退出容器 exit
然后就可打开Web控制台http://localhost:15672/登录查看了,默认账户guest/guest
Hello World
先介绍下这里的术语:
-
Producer生产者
生产意为发送,一个发送消息的程序就是生产者。
-
Queue队列
队列就是RabbitMQ中邮箱的名称,本质是一个巨大的消息缓存。生产者可以往里发消息,消费者可以从这里接收消息
-
Consumer消费者
消费和接收的意思相近,消费者就是一个主要接收消息的程序。
引入客户端依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.1</version>
</dependency>
连接工具类
发送和接收都需要建立和RabbitMQ的连接,首先我们封装一个ConnectionUtil连接工具
package org.example;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 连接工具类
*/
public class ConnectionUtil {
/**
* 创建频道
*/
public static Channel createChannel() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
return channel;
}
}
/**
* 创建保活频道
*/
public static Channel createAliveChannel() throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
return channel;
}
}
Producer
首先需要声明一个队列,然后就可以往里发送消息了,声明队列是幂等的,只有队列不存在时才会创建,注意使用try-with-resources体获取的channel。
// 使用工具类获取Channel,后续示例忽略
final Channel channel = ConnectionUtil.createChannel();
String QUEUE_NAME = "hello";
/**
* 声明队列
* 参数解释:
* 1、队列名称
* 2、队列里面的消息是否进行持久化(保存到磁盘) 默认把消息存在内存中
* 3、该队列是否只供一个消费者进行消费 true表示可以多个消
费者消费,false表示只能一个消费者消费一般为false,保证能够共享
* 4、是否自动删除 在最后一个消费者断开连接后,该队列是否自动删除
* 5、其他参数,后面学习会用到
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World." + LocalDateTime.now();
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
Consumer
消费者只能从队列里取到消息,因此也同样需要声明队列,这里要和发布者保持一致。但是这里channel并不能使用try-with-resource获取,因为我们希望消费者一直监听,如果使用自动关闭资源,那所有链接都被关闭就很尴尬了。
然后我们订阅这个队列,并且提供一个回调,服务器就会异步向我们推送给消息。
一个最简单的发送订阅就完成了。
// 使用工具类获取Channel,后续示例忽略
final Channel channel = ConnectionUtil.createAliveChannel();
String QUEUE_NAME = "hello";
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
delivery.getEnvelope().getDeliveryTag();
System.out.println(" [x] Received '" + message + "'");
};
// 消费
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
});
Work Queue工作队列
在这个章节我们会创建工作队列来分配耗时任务给多个工作者 。工作队列的核心思想是避免立即执行资源密集型任务,并不得不等待它完成。相反我们将任务封装为消息并发送到队列,后台的工作进程将它取出,如果启动多个工作者,任务将在他们之前共享。
Producer
在这里我们并没有真是的耗时的任务要处理,像是渲染PDF或者处理图片等等,因此我们用Thread.sleep()来模拟耗时任务,我们用消息中的.来代表任务复杂度,每一个点将耗时1秒来处理,例如这样一个消息Hello...将会耗时3秒处理。
boolean durable = true;
String QUEUE_NAME = "worker-queue";
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
for (int i = 0; i < 10; i++) {
String message;
// 偶数 重任务
if (i % 2 == 0) {
message = "Hello World.1.2.3.4.5.6.7.8.9.10.;" + LocalDateTime.now();
} else {
message = "Hello World.1;" + LocalDateTime.now();
}
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
在这里我们将声明队列的durable参数设置为true,发送消息时也设置MessageProperties.PERSISTENT_TEXT_PLAIN来实现持久化,以避免当RabbitMQ宕机时消息丢失。
Consumer
消费者需要做一些小改造,它需要为消息的每个点模拟处理一秒。
String QUEUE_NAME = "worker-queue";
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
doWork(message);
// 手动确认
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
System.out.println(" [x] Received '" + message + "'");
};
boolean autoAck = false; // acknowledgment is covered below
channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> {
});
这里我们还将订阅时的自动提交参数改为false, 在回调里手动提交basicAck,这样可以避免当消费者在意外挂掉导致的消息丢失。
设置了手动提交后,当服务器接收提交超时(默认30秒)后,它就会将该消息重新加入队列。如果此时有其他消费者,它会马上将消息发给其他消费者
模拟耗时处理方法如下
private static void doWork(String message) {
for (char c : message.toCharArray()) {
if (c == '.') {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
当我们启动两个Consumer,服务器会将消息轮询发给消费者。你可能注意到我们发送消息的时候,偶数总是耗时的任务,奇数总是简单任务,这样会导致其中一个压力巨大,另一个却游手好闲。
为了避免这种情况我们可以使用basicQos方法设置prefetchCount=1,这个动作就是告诉RabbitMQ,一次不要推送超过一个消息。换句话说,就是不要在他还在处理时推送消息,而是推给其他空闲的消费者。
int prefetchCount = 1;
channel.basicQos(prefetchCount);
Fanout
在前面的章节中,每一个消息会发给一个消费者,在这一章中,我们要做完全不一样的事情,我们会将消息发给多个消费者,这种模式就是发布/订阅模式
在RabbitMQ的模型中,生产者从不直接和队列打交道,它只和exchange打交道。exchange可以理解为交换机,负责接收消息,根据规则再将消息分发送到指定队列。
在之前的章节中我们并不知道exchange的存在,没有指定exchange也能发送消息。这是因为,当我们发布时,用了空字符串指定使用默认的exchange-direct exchange。
// exchangeName为“”,使用默认的direct exchange
channel.basicPublish("", "hello", null, message.getBytes());
这里发布时的第一个参数是exchange的名称, 第二个参数是routingKey,当和我们使用fanout 类型exchange时,这个值会被忽略。
RabbitMQ中有有四种exchange,fanout,direct,topic,header。在本章中我们使用fanout类型的exchange,fanout直译为扇出,就像一把扇子,源头是一个点,向外扩展,形象的理解为广播。
Producer
在这里我们会往命名为logs的exchange发送日志信息。
String EXCHANGE_NAME = "logs";
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
String message = "info: Hello World!";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
Consumer
在上一个例子Worker中,我们指定了队列的名称"hello", "worker-queue"",然后将两个消费者指向了这个队列,此时这个队列中的消息就被两个消费者共享了,因此队列命名很重要。但在本章节中,我们希望每个消费者都可以读到所有的消息。我们需要
-
当消费者连接时,需要有一个全新的空队列,名称可以是随机的,最好由服务器指定。
-
当消费者断开时,队列可以自动删除。
RabbitMQ提供了一个无参方法来声明匿名队列,除此之外还要让exchange往这个队列里发送消息,队列和exchange之间的关系称为绑定,绑定可以简单理解为,队列对这个exchange的消息感兴趣。
启动两个Consumer,此时他们都能收到消息。
String EXCHANGE_NAME = "logs";
// 声明Exchange和Quene
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
// 回调
DeliverCallback deliverCallback = ((consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
});
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
Direct
在上一个例子fanout中,exchange会给所有的消费者发送信息。在这一章节中,我们会加入一个特性:消费者可以只订阅其中的一个子级。例如我们有些组件只消费错误日志,而有的组件消费所有的日志。这个特性是通过队列和exchange绑定关系的额外参数:路由键routingKey或者称为bindingKey来实现的。
Producer
在这里我们将会使用direct exchange,direct exchange的算法也很简单,当消息发送的routingKey和队列绑定的routingKey匹配上时,消息就被发送过去了。
String EXCHANGE_NAME = "direct_logs";
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 此处可修改为 info,warning
String rootingkey = "error";
String message = "This is log";
channel.basicPublish(EXCHANGE_NAME, rootingkey, null, message.getBytes(StandardCharsets.UTF_8));
Consumer
启动多个消费者,每个消费者绑定的routingKey列表不一致。我们也可以让每个消费者绑定的routingKey一摸一样,不过那样就和fanout没区别了
String EXCHANGE_NAME = "direct_logs";
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String queueName = channel.queueDeclare().getQueue();
// 通过routingKey过滤exchange数据,可以有多个绑定关系
channel.queueBind(queueName, EXCHANGE_NAME, "error");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
Topic
如果我们想在direct 的基础上,实现更高的灵活度,例如有的消费不单只关心error日志,还想针对日志的来源类型进行过滤,就可以使用topic exchange。发送给exchange时指定的的routingKey不能是任意字符,它必须是一些单词由·分割,如“kernel.error.log”。topic支持带通配符的routingKey绑定。
-
*可以替代一个单词 -
#可以替代0个或多个单词
我们也可以不用通配符,不过这种情况下它就和directexchange没区别了
Producer
String EXCHANGE_NAME = "topic_logs";
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
String routingKey = "lazy.orange.bird";
String message = "test-message";
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
Consumer
String EXCHANGE_NAME = "topic_logs";
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
String queueName = channel.queueDeclare().getQueue();
// 灵活调换routingKey *.*.rabbit
String bindingKey = "*.orange.*";
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
在这里我们启动两个消费者。C1订阅*.orange.*,C2订阅*.*.rabbit和lazy.#,当生产者发送的routingKey为quick.orange.rabbit,消息会同时发送给C1和C2,而quick.orange.fox仅匹配C1,lazy.brown.fox仅匹配C2。lazy.pink.rabbit同时匹配C2的两个绑定,但它仅会被发送给C2一次。如果都不匹配,消息将会被丢弃。
RPC
rpc涉及很少,这里就不介绍了
以上就是全部内容啦
资料参考或引用自以下链接