RabbitMQ的学习
哔哩哔哩链接:https://www.bilibili.com/video/BV1dE411K7MG
一、消息队列MQ
1.什么是MQ
MQ(Message Queue)
: 翻译为消息队列
,通过典型的生产者和消费者模型,生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,轻松的实现系统间解耦。别名为 消息中间件
通过利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
2.MQ有哪些
当今市面上有很多主流的消息中间件,如老牌的ActiveMQ
、RabbitMQ
,炙手可热的Kafka
,阿里巴巴自主开发RocketMQ
等。
3.不同MQ的特点
(1) ActiveMQ
ActiveMQ
是Apache
出品,最流行的,能力强劲的开源消息总线。它是一个完全支持JMS规范的的消息中间件。丰富的API,多种集群架构模式让ActiveMQ
在业界成为老牌的消息中间件,在中小型企业颇受欢迎!
(2) Kafka
Kafka
是LinkedIn
开源的分布式发布-订阅消息系统,目前归属于Apache顶级项目
。Kafka主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。
(3) RocketMQ
RocketMQ
是阿里开源的消息中间件,在阿里内部被称为MetaQ
。它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。
(4) RabbitMQ
RabbitMQ
是使用Erlang
语言开发的开源消息队列系统,基于AMQP协议
来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。
RabbitMQ比Kafka可靠,Kafka更适合IO高吞吐的处理,一般应用在大数据日志处理或对实时性(少量延迟),可靠性(少量丢数据)要求稍低的场景使用,比如ELK日志收集
。
二、初识RabbitMQ
基于AMQP协议,erlang语言开发,是部署最广泛的开源消息中间件,是最受欢迎的开源消息中间件之一。RabbitMQ的官网:https://www.rabbitmq.com/
。
1.AMQP 协议
AMQP(advanced message queuing protocol)
在2003年时被提出,最早用于解决金融领不同平台之间的消息传递交互问题。顾名思义,AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)
。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。这使得实现了AMQP的provider天然性就是跨平台的。以下是AMQP协议模型:
如图所示,生产者(Publisher
)将消息发送给虚拟主机(Virtual host
)中的交换机(Exchange
),交换机和消息队列(Message Queue
)之间有绑定关系,消费者(Consumer
)通过消息队列来消费消息。
2.RabbitMQ的工作原理
RabbitMQ的基本结构:
组成部分说明:
Broker:消息队列服务进程,此进程包括两个部分:Exchange和Queue
Exchange:消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过滤。
Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的
Producer:消息生产者,即生产方客户端,生产方客户端将消息发送
Consumer:消息消费者,即消费方客户端,接收MQ转发的消息。
生产者发送消息流程:
1、生产者和Broker建立TCP连接。
2、生产者和Broker建立通道。
3、生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
4、Exchange将消息转发到指定的Queue(队列)
消费者接收消息流程:
1、消费者和Broker建立TCP连接
2、消费者和Broker建立通道
3、消费者监听指定的Queue(队列)
4、当有消息到达Queue时Broker默认将消息推送给消费者。
5、消费者接收到消息。
6、ack回复
3.RabbitMQ的安装
1.首先前往RabbitMQ官网下载,RabbitMQ下载地址:https://www.rabbitmq.com/download.html
2.因为RabbitMQ是用erlang语言开发的,所以要先安装好语言环境。安装RabbitMQ需要先安装erlang
语言的依赖和erlang内存管理的socat
依赖(需要安装包的可以通过我的微信公众号进行获取),
3.此处我们使用CentOS7
安装RabiitMQ
,未安装Linux虚拟机的可以去看我的博客,点击前往
4.首先我们输入mkdir rabbitmq
创建一个rabbitmq目录,之后进入该目录上传rabbitmq安装包和依赖。之后输入 rpm -ivh erlang-22.0.7-1.el7.x86_64.rpm
,之后 rpm -ivh socat-1.7.3.2-2.el7.x86_64.rpm
,之后 rpm -ivh rabbitmq-server-3.7.18-1.el7.noarch.rpm
进行安装。
5.在安装之后,rabbitmq默认会读取/etc/rabbitmq
下的rabbitmq.config
配置文件,但是rabbitmq在安装时并没有给我们提供这个文件,但是它的安装目录(此处为/usr/share/doc/rabbitmq-server-3.7.18
)中给我们提供了一个配置文件的模板rabbitmq.config.example
,所以我们可以复制该文件并复制到/etc/rabbitmq
这个目录下,同时命名为rabbitmq.config
。我们输入
cp /usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example /etc/rabbitmq/rabbitmq.config
补充:搜索某个文件可用find / -name 文件名
,如find / -name rabbitmq.config.example
,返回的是目录所在的位置,此处为
/usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example
6.之后需要对配置文件做一些修改,启用guest
角色登录,表示我们要使用guest
来宾用户登录到rabbitmq的后台管理界面(guest具有全部的权限,即管理员)。我们输入vi rabbitmq.config
修改配置文件如下:
补充:在vim里搜索关键字
例如搜索 the写法:/the + 回车
/+关键字
,回车即可。此为从文档当前位置向下查找关键字,按n
键查找关键字下一个位置;?+关键字
,回车即可。此为从文档挡圈位置向上查找关键字,按n
键向上查找关键字;
7.之后启动rabbitmq中的插件管理开启rabbitmq的管理界面,我们输入rabbitmq-plugins enable rabbitmq_management
rabbitmq-plugins enable:启动插件
rabbitmq-plugins list:列出所有插件
rabbitmq-plugins disable:关闭插件
8.之后就是启动rabbitmq了,在rabbitmq安装后会存在一个rabbitmq-server
的服务,我们通过systemctl
命令就可以启动rabbitmq,命令如下:
systemctl start rabbitmq-server:启动RabbitMQ
systemctl restart rabbitmq-server:重启RabbitMQ
systemctl stop rabbitmq-server:停止RabbitMQ
systemctl status rabbitmq-server:查看RabbitMQ状态
rabbitmqctl help:查看更多命令,用来在不使用web管理界面情况下命令操作RabbitMQ
9.之后我们就可以使用rabbitmq给我们提供的管理界面进行管理了,管理界面默认的端口为15672
,但是我们需要先进行防火墙的配置,开启端口号的访问或关闭防火墙,具体如下:
查看防火墙状态,firewall-cmd --state 或 systemctl status firewalld
关闭防火墙,systemctl stop firewalld.service
禁止防火墙开机启动,systemctl disable firewalld.service
防火墙开机启动,systemctl start firewalld
防火墙开放端口号,firewall-cmd --add-service=http –permanent,
如我们要开启8001端口:firewall-cmd --add-port=8001/tcp --permanent
重启防火墙,firewall-cmd --reload
查看开放的端口号,firewall-cmd --list-all
注意开启某个端口后要重启防火墙才能生效
10.之后在浏览器输入http://192.168.166.136:15672/
访问rabbitmq提供的管理界面,并输入我们前面开启的来宾用户guest
,其用户名和密码都是guest
,点击登录
11.登录成功,rabbitmq管理主界面如下:
4.RabbitMQ管理界面
(1)主界面
connections
:无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况
channels
:通道,建立连接后,会形成通道,消息的投递获取依赖通道
Exchanges
:交换机,用来实现消息的路由
Queues
:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列
(2)Admin用户和虚拟主机管理
i) 添加用户
Tags
选项,其实是指定用户的角色,可选的有以下几个:
超级管理员(administrator)
:可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作
监控者(monitoring)
:可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
策略制定者(policymaker)
:可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)
普通管理者(management)
:仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理
其他
:无法登陆管理控制台,通常就是普通的生产者和消费者
ii) 创建虚拟主机
iii) 绑定虚拟主机和用户
创建好虚拟主机,我们还要给用户添加访问权限,点击添加好的用户,进入用户设置界面
之后设置权限以及绑定可操作的虚拟主机
三、RabbitMQ使用
1. AMQP协议回顾
- 生产者通过通道发送消息,每个生产者对应一个虚拟主机,需要将虚拟主机和用户绑定之后才有访问权限。
- 消息要不要放到交换机中取决于所使用的消息模型,消息不放到交换机时会直接放到消息队列中。
- 消费者和生产者是解耦的,它只关心消息队列中有没有相应的消息,消费者消费消息时也需要连接虚拟主机。
2. AMQP支持的消息模型
目前共有7种消息模型(rabbitmq3.7版本支持前六种,rabbitmq3.5版本支持前五种),官网地址:https://www.rabbitmq.com/getstarted.html
3.消息模型介绍
首先使用ideal新建一个maven项目,并在pom.xml文件中引入rabbitmq依赖,
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.2</version>
</dependency>
第一种模型(直连)
在上图的模型中,有以下概念:
P
:生产者,也就是要发送消息的程序
C
:消费者:消息的接受者,会一直等待消息到来
queue
:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息
该模型中只有一个生产者和一个消费者,生产者将消息发送到消息队列,生产者对消息队列进行监听,从消息队列取出消息进行消费。
(1) 在rabbitmq管理界面创建虚拟主机和用户
在Admin
选项中
- 创建名为
/ems
虚拟主机,虚拟主机都是以/
开头的 - 创建名为
ems
的用户 - 点击
ems
的用户名与/ems
虚拟主机进行绑定
(2) 开发生产者
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Provider {
// 生产消息
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 创建连接MQ的连接工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置连接RabbitMQ主机
connectionFactory.setHost("192.168.166.137");
// 设置端口号(amqp)
connectionFactory.setPort(5672);
// 设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
// 设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("ems");
// 获取连接
Connection connection = connectionFactory.newConnection();
// 获取连接中通道
Channel channel = connection.createChannel();
/* 通道绑定对应消息队列
// 参数1:queue队列名称,不存在会自动创建
// 参数2:durable队列是否持久化(重启RabbitMQ后队列是否还存在),
如果开启队列会存在,但是队列中的消息仍会丢失(可通过下面发布消息时的设置进行解决)
// 参数3:exclusive是否独占队列
// 参数4:autoDelete否在消费完成后自动删除队列
// 参数5:arquments额外附加参数
*/
channel.queueDeclare("hello", false, false, false, null);
/* 发布消息
// 参数1:exchange交换机名称
// 参数2:routingKey队列名称
// 参数3:props传递消息额外设置,
如果为MessageProperties.PERSISTENT_TEXT_PLAIN:重启RabbitMQ后消息仍会存在
// 参数4:body消息的具体内容,是一个byte类型的数组
*/
channel.basicPublish("","hello",null,"hello RabbitMQ".getBytes());
// 关闭通道和连接
channel.close();
connection.close();
}
}
运行代码,查看rabbitmq管理界面,发现消息队列中已经有了一条消息,且消息还未进行消费
(3) 开发消费者
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
// 注意需要是main函数,因为消费者要一直监听。不能是单元测试方法,
// 因为单元测试不支持多线程,即单元测试会将其他线程杀死
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接MQ的连接工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置连接RabbitMQ主机
connectionFactory.setHost("192.168.166.137");
// 设置端口号
connectionFactory.setPort(5672);
// 设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
// 设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("ems");
// 获取连接
Connection connection = connectionFactory.newConnection();
// 获取连接中通道
Channel channel = connection.createChannel();
/* 通道绑定对应消息队列
// 参数1:queue队列名称,不存在会自动创建
// 参数2:durable队列是否持久化(重启RabbitMQ后队列是否还存在),
如果开启队列会存在,但是队列中的消息仍会丢失(可在发布消息时的额外设置进行解决)
// 参数3:exclusive是否独占队列
// 参数4:autoDelete否在消费完成后自动删除队列
// 参数5:arquments额外附加参数
// 注意:要保证生产者和消费者队列的参数一致
*/
channel.queueDeclare("hello", false, false, false, null);
// 参数1:队列名
// 参数2:是否开启消息的自动确认机制。如果为true,如果消费者出现宕机的情况会导致未被消费的消息的丢失
// 我们一般设置为false,表示关闭自动确认,需要手动确认
// 设置为false后需要手动确认消息,此处我们先设置为true
// 参数3:消费消息时的回调接口
channel.basicConsume("hello", true, new DefaultConsumer(channel) {
@Override
// body:从队列中取出的消息
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("Message: " + new String(body));
}
});
// 关闭通道和连接,不关闭的话会一直监听队列,我们不建议关闭,
// 因为可能消息还未被消费,信道和连接就已经关闭了
//channel.close();
//connection.close();
}
}
运行代码,发现消息者已经消费了消息,且消费者也一直处于监听队列的状态
(4) 封装工具类
将生产者和消费者中重复的代码封装为工具类
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class RabbitmqUtil {
// 创建连接MQ的连接工厂对象
public static ConnectionFactory connectionFactory;
// 静态代码块中的内容只在类加载的时候执行一次
static {
connectionFactory = new ConnectionFactory();
}
// 提供连接对象的方法
public static Connection getConnection() {
try {
// 设置连接RabbitMQ主机
connectionFactory.setHost("192.168.166.137");
// 设置端口号(AMQP)
connectionFactory.setPort(5672);
// 设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
// 设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("ems");
// 获取连接
return connectionFactory.newConnection();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// 关闭通道和连接的方法
public static void closeChannelAndConnection(Channel channel, Connection connection)
throws IOException, TimeoutException {
if (channel != null) {
channel.close();
}
if (connection != null) {
connection.close();
}
}
}
api操作细节
1.指定通道绑定的queue队列名称,不存在会自动创建
2.如果队列不持久化,则rabbitmq重启后会队列会消失,同时队列中的消息也会消失
如果开启了队列的持久化,
D表示开启了队列的持久化,注意消息的持久化需要在发布消息的时候通过附加额外参数进行设置,如下:
3.一般不设置通道独占队列
4.设置队列是否在被消费者消费完全部
消息之后进行删除
如果设置为true
,如下:
AD
表示自动删除该队列(当队列中没有消息且没有消费者监听该队列时)
当我们断掉消费者对该队列的监听时,会删除该队列
第二种模型(work queue)
Work queues
,也被称为Task queues
,任务模型。当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。
此时就可以使用work 模型
:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。
角色:
- P:生产者:任务的发布者
- C1:消费者-1,领取任务并且完成任务,假设完成速度较慢
- C2:消费者-2:领取任务并完成任务,假设完成速度快
(1) 开发生产者
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Provider2 {
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
/* 通道绑定对应消息队列
// 参数1:queue队列名称,不存在会自动创建
// 参数2:durable队列是否持久化(重启RabbitMQ后队列是否还存在),
如果开启队列会存在,但是队列中的消息仍会丢失(可通过下面发布消息时的设置进行解决)
// 参数3:exclusive是否独占队列
// 参数4:autoDelete否在消费完成后自动删除队列
// 参数5:arquments额外附加参数
*/
channel.queueDeclare("work", true, false, false, null);
for (int i = 0; i < 10; i++) {
/* 发布消息
// 参数1:exchange交换机名称
// 参数2:routingKey队列名称
// 参数3:props传递消息额外设置,
如果为MessageProperties.PERSISTENT_TEXT_PLAIN:重启RabbitMQ后消息仍会存在
// 参数4:body消息的具体内容,是一个byte类型的数组
*/
channel.basicPublish("", "work", null, (i + " hello work queque").getBytes());
}
// 关闭资源
RabbitmqUtil.closeChannelAndConnection(channel, connection);
}
}
(2) 开发消费者1
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer2_1 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
/* 通道绑定对应消息队列
// 参数1:queue队列名称,不存在会自动创建
// 参数2:durable队列是否持久化(重启RabbitMQ后队列是否还存在),
如果开启队列会存在,但是队列中的消息仍会丢失(可在发布消息时的额外设置进行解决)
// 参数3:exclusive是否独占队列
// 参数4:autoDelete否在消费完成后自动删除队列
// 参数5:arquments额外附加参数
// 注意:要保证生产者和消费者队列的参数一致
*/
channel.queueDeclare("work", true, false, false, null);
// 参数1:队列名
// 参数2:是否开启消息的自动确认机制。如果为true,如果消费者出现宕机的情况会导致未被消费的消息的丢失
// 我们一般设置为false,表示关闭自动确认,需要手动确认
// 设置为false后需要手动确认消息,此处我们先设置为true
// 参数3:消费消息时的回调接口
channel.basicConsume("work", true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者1:"+new String(body));
}
});
}
}
(3) 开发消费者2
消费者2的代码与消费者1的代码时一样的,只是类名不同而已
(4) 测试结果
先运行Consumer2_1
和Consumer2_2
,再运行Provider2
。默认RabbitMQ将按顺序将每个消息发送给下一个使用者,无论两个消费者对消息的处理速度是否一致,其能消耗的消息数都是平均分配的,这种分发消息的方式称为循环(轮询)。
注意:在开启消息的自动确认机制的情况下,队列中的消息是在一开始就已经把全部消息平均分给每个消费者了(此时消息可能还没有被消费者全部消费完),并不是一个一个地轮询进行分配,所以这时候就容易出现问题,如果一个消费者在还未消费完全部消息时出现宕机的情况,就会导致未被消费的消息的丢失,所以我们一般不开启消息的自动确认机制,而采用手动确认的方式。
(5) 消息自动确认机制
当多个消费者处理消息的速度不同时,可以关闭自动确认并设置每次能消费的消息个数来实现能者多劳
。
注意:
1.为了不让队列一次性的把消息全部分配给消费者,我们要设置队列一次只能分配一个消息给消费者,防止消费者出现宕机的情况,导致丢失未被消费的消息;
2.关闭自动确认消息后需要手动确认消息,因为这时如果一个消息已经被消费,队列也不会知道该消息已经进行消费了,如果不手动确认的话,这时队列会把这个消息划分到未被确认的部分(Unacked
)。
书写Consumer2_3
,我们让其沉睡1s模拟性能较差的消费者,代码如下:
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer2_3 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
final Channel channel = connection.createChannel();
// 设置通道每次只能消费1个消息
channel.basicQos(1);
/* 通道绑定对应消息队列
// 参数1:queue队列名称,不存在会自动创建
// 参数2:durable队列是否持久化(重启RabbitMQ后队列是否还存在),
如果开启队列会存在,但是队列中的消息仍会丢失(可在发布消息时的额外设置进行解决)
// 参数3:exclusive是否独占队列
// 参数4:autoDelete否在消费完成后自动删除队列
// 参数5:arquments额外附加参数
// 注意:要保证生产者和消费者队列的参数一致
*/
channel.queueDeclare("work", true, false, false, null);
// 参数1:队列名
// 参数2:是否开启消息的自动确认机制。如果为true,如果消费者出现宕机的情况会导致未被消费的消息的丢失
// 我们一般设置为false,表示关闭自动确认,需要手动确认
// 设置为false后需要手动确认消息
// 参数3:消费消息时的回调接口
channel.basicConsume("work", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 模拟性能较差的消费者
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者2:" + new String(body));
// 手动确认
// 参数1:确认队列中哪个具体消息
// 参数2:是否开启多个消息同时确认
// 如果不确认消息的话,消息会被划分到未被确认的部分(Unacked)
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
Consumer2_4
则没有沉睡1s的代码,先后启动Consumer2_3
和Consumer2_4
这两个消费者,之后启动Provider2
生产者,测试结果如下,这就实现了我们能者多劳的目的
第三种模型(Fanout)
fanout:扇出,也称为广播
在广播模式下,消息发送流程是这样的:
- 可以有多个消费者
- 每个消费者有自己的
queue
(队列) - 每个队列都要绑定到
Exchange
(交换机) - 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
- 交换机把消息发送给绑定过的
所有队列
- 队列的消费者都能拿到消息,实现一条消息被多个消费者消费
(1) 开发生产者
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Provider3 {
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数1:交换机名称,不存在会自动创建
// 参数2:交换机类型(fanout为广播类型)
channel.exchangeDeclare("logs","fanout");
// 发送消息
/* 发布消息
// 参数1:exchange交换机名称
// 参数2:fanout中routingKey没意义
// 参数3:props传递消息额外设置,
如果为MessageProperties.PERSISTENT_TEXT_PLAIN:重启RabbitMQ后消息仍会存在
// 参数4:body消息的具体内容,是一个byte类型的数组
*/
channel.basicPublish("logs", "", null, ("fanout type message").getBytes());
// 关闭资源
RabbitmqUtil.closeChannelAndConnection(channel, connection);
}
}
(2) 开发消费者1/2/3
3个消费者的代码几乎完全一样,不同的只有类名和第23行的输出信息不同。
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer3_1 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数1:交换机名称,不存在会自动创建
// 参数2:交换机类型(fanout为广播类型)
channel.exchangeDeclare("logs","fanout");
// 临时队列
String queueName=channel.queueDeclare().getQueue();
// 绑定交换机和队列
channel.queueBind(queueName,"logs","");
// 消费消息
channel.basicConsume(queueName,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者1:"+new String(body));
}
});
}
}
先后启动Consumer3_1、Consumer3_2、Consumer3_3
三个消费者,之后启动Provider3
生产者,结果如下,发现每个消费者都接收到了发送的广播消息
第四种模型(Routing之订阅模型-Direct)
在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key) - 消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。 - Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key
进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息
流程:
图解:
P
:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。X
:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列C1
:消费者,其所在队列指定了需要routing key 为error
的消息C2
:消费者,其所在队列指定了需要routing key 为info、error、warning
的消息
(1) 开发生产者
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Provider4 {
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数1:交换机名称,不存在会自动创建
// 参数2:交换机类型(direct为路由模式)
channel.exchangeDeclare("logs_direct", "direct");
String routingKey = "info";
/* 发布消息
// 参数1:exchange交换机名称
// 参数2:指定匹配routingKey
// 参数3:props传递消息额外设置,
如果为MessageProperties.PERSISTENT_TEXT_PLAIN:重启RabbitMQ后消息仍会存在
// 参数4:body消息的具体内容,是一个byte类型的数组
*/
channel.basicPublish("logs_direct", routingKey, null,
("这是direct模型发布的基于routingkey:[" + routingKey + "] 发送的消息").getBytes());
// 关闭资源
RabbitmqUtil.closeChannelAndConnection(channel, connection);
}
}
(2) 开发消费者1
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer4_1 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数1:交换机名称
// 参数2:交换机类型(direct为路由模式)
channel.exchangeDeclare("logs_direct", "direct");
// 得到一个临时队列,我们一般使用临时队列分给绑定每个交换机,即没有消息就会自动删除
String queueName = channel.queueDeclare().getQueue();
// 基于routingKey绑定交换机和队列
// 参数1:队列名称 参数2:交换机 参数3:routingKey
channel.queueBind(queueName, "logs_direct", "error");
// 消费消息
// 参数1:队列名称 参数2:是否开启自动确认 参数3:消费消息时的回调接口
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者1:" + new String(body));
}
});
}
}
(3) 开发消费者2
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer4_2 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数1:交换机名称
// 参数2:交换机类型(direct为路由模式)
channel.exchangeDeclare("logs_direct", "direct");
// 得到一个临时队列,我们一般使用临时队列分给绑定每个交换机,即没有消息就会自动删除
String queueName = channel.queueDeclare().getQueue();
// 基于routingKey绑定交换机和队列
// 参数1:队列名称 参数2:交换机 参数3:routingKey
channel.queueBind(queueName, "logs_direct", "error");
channel.queueBind(queueName, "logs_direct", "info");
channel.queueBind(queueName, "logs_direct", "warning");
// 消费消息
// 参数1:队列名称 参数2:是否开启自动确认 参数3:消费消息时的回调接口
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者2:" + new String(body));
}
});
}
}
(4) 测试结果
先后启动Consumer4_1
和Consumer4_2
消费者,之后启动Provider4
生产者,当生产者发送Route key为info
的消息时,只有消费者2接收到了消息
当生产者发送Route key为error
的消息时,消费者1和消费者2都接收到了消息
第五种模型(Routing之订阅模型-Topic)
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key的时候使用通配符!
这种模型Routingkey 一般都是由一个或多个单词组成,多个单词之间以.
分割,例如: item.insert
统配符:
*
:匹配恰好1个单词#
:匹配0个或多个单词
如:
audit.#
:匹配audit.irs.corporate
或者audit.irs
等audit.*
:只能匹配audit.irs
*.audit.#
:中间必须是audit,audit前有一个或多个单词,后有一个单词
(1) 开发生产者
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Provider5 {
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数1:交换机名称,不存在会自动创建
// 参数2:交换机类型(topic为动态路由)
channel.exchangeDeclare("topics", "topic");
String routingKey = "user.save";
/* 发布消息
// 参数1:exchange交换机名称
// 参数2:指定匹配routingKey
// 参数3:props传递消息额外设置,
如果为MessageProperties.PERSISTENT_TEXT_PLAIN:重启RabbitMQ后消息仍会存在
// 参数4:body消息的具体内容,是一个byte类型的数组
*/
channel.basicPublish("topics", routingKey, null,
("这是topic动态路由模型,routingkey:[" + routingKey + "]").getBytes());
// 关闭资源
RabbitmqUtil.closeChannelAndConnection(channel, connection);
}
}
(2) 开发消费者1
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer5_1 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数1:交换机名称
// 参数2:交换机类型(topic为动态路由)
channel.exchangeDeclare("topics", "topic");
// 得到一个临时队列,我们一般使用临时队列分给绑定每个交换机,即没有消息就会自动删除
String queueName = channel.queueDeclare().getQueue();
// 基于通配符形式routingKey绑定交换机和队列
// 参数1:队列名称 参数2:交换机 参数3:routingKey
channel.queueBind(queueName, "topics", "user.*");
// 消费消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者1:" + new String(body));
}
});
}
}
(3) 开发消费者2
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer5_2 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitmqUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数1:交换机名称
// 参数2:交换机类型(topic为动态路由)
channel.exchangeDeclare("topics", "topic");
// 得到一个临时队列,我们一般使用临时队列分给绑定每个交换机,即没有消息就会自动删除
String queueName = channel.queueDeclare().getQueue();
// 基于通配符形式routingKey绑定交换机和队列
// 参数1:队列名称 参数2:交换机 参数3:routingKey
channel.queueBind(queueName, "topics", "user.#");
// 消费消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者2:" + new String(body));
}
});
}
}
(4) 测试结果
先后启动Consumer5_1
和Consumer5_2
消费者,之后启动Provider5
生产者,
-
当生产者发送Route key为
user.save
的消息时,消费者1和消费者2都接收到了消息 -
当生产者发送Route key为
user.save.aaa
的消息时,只有消费者2接收到了消息
第六种模型(RPC)
RPC模型示意图:
基本概念:
Callback queue 回调队列,客户端向服务器发送请求,服务器端处理请求后,将其处理结果保存在一个存储体中。而客户端为了获得处理结果,那么客户在向服务器发送请求时,同时发送一个回调队列地址reply_to。
Correlation id 关联标识,客户端可能会发送多个请求给服务器,当服务器处理完后,客户端无法辨别在回调队列中的响应具体和那个请求时对应的。为了处理这种情况,客户端在发送每个请求时,同时会附带一个独有correlation_id属性,这样客户端在回调队列中根据correlation_id字段的值就可以分辨此响应属于哪个请求。
流程说明:
- 当客户端启动的时候,它创建一个匿名独享的回调队列。
- 在 RPC 请求中,客户端发送带有两个属性的消息:一个是设置回调队列的 reply_to 属性,另一个是设置唯一值的 correlation_id 属性。
- 将请求发送到一个 rpc_queue 队列中。
- 服务器等待请求发送到这个队列中来。当请求出现的时候,它执行他的工作并且将带有执行结果的消息发送给 reply_to 字段指定的队列。
- 客户端等待回调队列里的数据。当有消息出现的时候,它会检查 correlation_id 属性。如果此属性的值与请求匹配,将它返回给应用
面试题:
避免消息堆积?
1) 采用workqueue,多个消费者监听同一队列。
2)接收到消息以后,而是通过线程池,异步消费。
如何避免消息丢失?
1) 消费者的ACK机制。可以防止消费者丢失消息。但是,如果在消费者消费之前,MQ就宕机了,消息就没了?
2)可以将消息进行持久化。要将消息持久化,前提是:队列、Exchange都持久化
四、在SpringBoot中使用RabbitMQ
1. 搭建环境
(1) 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
(2) 配置文件设置
spring:
application:
name: rabbitmq_springboot
rabbitmq:
host: 192.168.166.137
port: 5672
username: ems
password: ems
virtual-host: /ems
#当然如果consumer只是接收消息而不发送,就不用配置template相关内容。
template: #有关AmqpTemplate的配置
retry: #失败重试
enabled: true #开启失败重试
initial-interval: 10000ms #第一次重试的间隔时长
max-interval: 300000ms #最长重试间隔,超过这个间隔将不再重试
multiplier: 2 #下次重试间隔的倍数,此处是2即下次重试间隔是上次的2倍
exchange: topic.exchange #缺省的交换机名称,此处配置后,发送消息如果不指定交换机就会使用这个
publisher-confirms: true #生产者确认机制,确保消息会正确发送,如果发送失败会有错误回执,从而触发重试
2. 第一种hello world模型使用
(1) 开发生产者
import com.rabbitmq.RabbitMQApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitMQApplication.class)
public class Test01 {
// 注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// hello world
@Test
public void testHelloWorld(){
/**
* 参数1:队列名称
* 参数2:发送的消息
*/
rabbitTemplate.convertAndSend("hello","hello world");
}
}
我们运行,发现并没有创建hello队列,因为此时并没有消费者,所以创建这个队列并没有任何意义,即rabbitmq并不会创建hello队列(同理,交换机也不会创建)。所以我们还需开发消费者
(2) 开发消费者
package com.rabbitmq.hello;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/*
* @RabbitListener表示开启一个消费者的监听,可以加在类和方法上
* queuesToDeclare表示监听的队列,没有会创建
* @Queue设置队列的信息
* value:监听的队列,如果只有这个参数,则创建的队列默认是持久化、非独占、不自动删除的队列
* durable:是否开启持久化
* exclusive:是否独占队列
* autoDelete:是否自动删除
*/
@Component
@RabbitListener(queuesToDeclare = @Queue(value = "hello", durable = "true", exclusive = "true", autoDelete = "true"))
public class HelloConsumer {
// @RabbitHandler表示从队列中取出消息的回调方法为被它所注解的方法
@RabbitHandler
public void received(String message){
System.out.println("message:" + message);
}
}
再次运行生产者,发现hello队列已经创建并且消费者已经接收到了消息
3. 第二种work模型使用
(1) 开发生产者
// 注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// work
@Test
public void testWork() {
for (int i = 0; i < 10; i++) {
/**
* 参数1:队列名称
* 参数2:发送的消息
*/
rabbitTemplate.convertAndSend("work", "work模型" + i);
}
}
(2) 开发消费者
package com.rabbitmq.hello;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
public class WorkConsumer {
/*
第1个消费者
@RabbitListener表示开启一个消费者的监听,可以加在类和方法上
@Queue设置队列的信息
value:监听的队列,如果只有这个参数,则创建的队列默认是持久化、非独占、不自动删除的队列
durable:是否开启持久化
exclusive:是否独占队列
autoDelete:是否自动删除
*/
@RabbitListener(queuesToDeclare = @Queue(value = "work", durable = "true", exclusive = "false", autoDelete = "true"))
public void receive1(String message) {
System.out.println("message1: " + message);
}
/*
第2个消费者
@RabbitListener表示开启一个消费者的监听,可以加在类和方法上
@Queue设置队列的信息
value:监听的队列,如果只有这个参数,则创建的队列默认是持久化、非独占、不自动删除的队列
durable:是否开启持久化
exclusive:是否独占队列
autoDelete:是否自动删除
*/
@RabbitListener(queuesToDeclare = @Queue(value = "work", durable = "true", exclusive = "false", autoDelete = "true"))
public void receive2(String message) {
System.out.println("message2: " + message);
}
}
启动运行生产者,我们发现work模式默认是公平调度,如果需要实现能者多劳要额外配置
4. 第三种Fanout广播模型
(1) 开发生产者
// 注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// fanout 广播
@Test
public void testFanout(){
// 注意参数多了个exchange
rabbitTemplate.convertAndSend("logs", "", "Fanout模型发送的消息");
}
(2) 开发消费者
package com.rabbitmq.hello;
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 FanoutConsumer {
@RabbitListener(bindings = @QueueBinding(value = @Queue, // 创建临时队列(不指定队列名字)
exchange = @Exchange(name = "logs", type = "fanout") // 绑定的交换机和类型
))
public void reveive1(String message) {
System.out.println("message1: " + message);
}
@RabbitListener(bindings = @QueueBinding(value = @Queue, // 创建临时队列(不指定队列名字)
exchange = @Exchange(name = "logs", type = "fanout") // 绑定的交换机和类型
))
public void reveive2(String message) {
System.out.println("message2: " + message);
}
}
启动运行生产者,我们发现每个消费者都接收到了消息
5. 第四种Route路由模型
(1) 开发生产者
// 注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// route 路由模式
@Test
public void testRoute(){
/**
* 参数1:交换机
* 参数2:routingKey
* 参数3:发送的消息
*/
rabbitTemplate.convertAndSend("directs","info","发送info的key的路由信息");
}
(2) 开发消费者
package com.rabbitmq.hello;
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 RouteConsumer {
@RabbitListener(bindings = @QueueBinding(
value = @Queue, // 创建临时队列(不指定队列名字)
// name和value的效果一样
exchange = @Exchange(value = "directs", type = "direct"), // 自定义交换机名称和类型
key = {"info","error"} // routeKey
))
public void receive1(String message) {
System.out.println("message1: " + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue, // 创建临时队列(不指定队列名字)
// name和value的效果一样
exchange = @Exchange(value = "directs", type = "direct"), // 自定义交换机名称和类型
key = "error" // routeKey
))
public void receive2(String message) {
System.out.println("message2: " + message);
}
}
启动运行生产者,我们发现只有第一个消费者接收到了消息,因为他的routingKey与生产者的routingKey匹配
6. 第五种Topic订阅模型(动态路由模型)
(1) 开发生产者
// 注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// topic 动态路由 (订阅模式)
@Test
public void testTopic(){
rabbitTemplate.convertAndSend("topics","user.save","user.save 路由信息");
}
(2) 开发消费者
package com.rabbitmq.hello;
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 TopicConsumer {
@RabbitListener(bindings = @QueueBinding(
value = @Queue,// 创建临时队列(不指定队列名字)
// name和value的效果一样
exchange = @Exchange(value = "topics", type = "topic"),// 自定义交换机名称和类型
/**
* - `*`:匹配恰好1个单词
* - `#`:匹配0个或多个单词
*/
key = {"user.save", "user.*"}// routeKey
))
public void receive1(String message) {
System.out.println("message1: " + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue,// 创建临时队列(不指定队列名字)
// name和value的效果一样
exchange = @Exchange(value = "topics", type = "topic"),// 自定义交换机名称和类型
/**
* - `*`:匹配恰好1个单词
* - `#`:匹配0个或多个单词
*/
key = {"order.#", "produce.#", "user.*"}// routeKey
))
public void receive2(String message) {
System.out.println("message2: " + message);
}
}
启动运行生产者,我们发现两个消费者接收到了消息,因为他们的routingKey与生产者的routingKey匹配
五、RabbitMQ的应用场景
1.异步处理
(1) 场景说明
用户注册后,需要发注册邮件和注册短信,传统的做法有两种: 1.串行的方式 2.并行的方式
(2) 串行方式
将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西
(3) 并行方式
将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间。
(4) 消息队列
假设三个业务节点分别使用50ms,串行方式使用时间150ms,并行使用时间100ms。虽然并行已经提高的处理时间,但是,前面说过,邮件和短信对我正常的使用网站没有任何影响,客户端没有必要等着其发送完成才显示注册成功,应该是写入数据库后就返回. 消息队列: 引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理
由此可以看出,引入消息队列后,用户的响应时间就等于写入数据库的时间+写入消息队列的时间(可以忽略不计),引入消息队列后处理后,响应时间是串行的3倍,是并行的2倍
2. 应用解耦
(1) 场景
双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口.
(2) 缺点
当库存系统出现故障时,订单就会失败。 订单系统和库存系统高耦合. 引入消息队列
(3) 订单系统
用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
(4) 库存系统
订阅下单的消息,获取下单消息,进行库操作。 就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失
3.流量削峰
(1) 场景
秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。
(2) 作用
- 可以控制活动人数,超过此一定阀值的订单直接丢弃(我为什么秒杀一次都没有成功过呢^^)
- 可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)
(3) 注意
- 用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面.
- 秒杀业务根据消息队列中的请求信息,再做后续处理
六、RabbitMQ的集群
1. 普通集群(副本集群)
默认情况下:RabbitMQ代理操作所需的所有数据/状态都将跨所有节点复制。这方面的一个例外是消息队列,默认情况下,消息队列位于一个节点上,尽管它们可以从所有节点看到和访问
(1) 架构图
核心解决问题: 当集群中某一时刻master节点宕机,可以对Queue中信息,进行备份
注意:
- 普通集群方式只能减轻消费者对主节点的压力,即可以给消费者绑定从节点,但是从节点仅仅只是主节点的一个副本,同步的是交换机和队列的相关数据,从节点中并没有存储队列中的消息(虽然能看到消息的个数)。当消费者通过从节点去获取队列消息时,从节点需要向主节点去获取队列的相关信息。
- 如果当主节点宕机时,会导致队列中消息的丢失(如果没有进行持久化),且从节点也不能切换为主节点,此时从节点也并不能对外提供服务,因为并没有存储队列中的消息,即并不能实现高可用
- 而后面的镜像集群可以实现高可用,消息会在每个节点之间进行自动同步,且如果其中一个节点不可用,并不会导致消息丢失或服务不可用的情况,提升MQ集群的整体高可用性。所以一般我们使用镜像集群而不用普通集群方式
- 那么为什么我们还要去学普通集群呢?
因为我们的镜像集群需要在我们的普通集群的基础之上进行配置。
(2) 集群搭建
- 集群规划
node1: 10.15.0.3 mq1 master 主节点
node2: 10.15.0.4 mq2 repl1 副本节点
node3: 10.15.0.5 mq3 repl2 副本节点
- 步骤
首先克隆三台机器,之后修改三台机器的ip地址,输入vi /etc/sysconfig/network-scripts/ifcfg-ens37
,修改IPADDR
那一栏的ip地址,修改为不一致即可,之后是主机名的修改,我们输入vim /etc/hostname
删除原先的主机名,并且修改为自定义的主机名,此处我们分别为mq1、mq2、mq3。之后重新启动,输入reboot
。最后是主机名和ip映射,我们在三台机器上分别输入vim /etc/hosts
加入:
10.15.0.3(此为你配置的IP地址) mq1(此为你配置的主机名)
10.15.0.4(此为你配置的IP地址) mq2(此为你配置的主机名)
10.15.0.5(此为你配置的IP地址) mq3(此为你配置的主机名)
或者我们也可以通过scp
命令进行,先在一台机器上配置好主机名和ip映射,之后输入scp /etc/hosts root@mq2:/etc/
,表示将此台机器上的/etc/hosts
文件(可以写多个文件,以空格隔开即可)传输到另一台机器mq2的/etc
目录下,注意:要在同一个网段上才可以(如192.168.136.xxx
)。
之后需要在三个机器上安装rabbitmq,并同步erlang.cookie文件,即修改集群的erlang.cookie文件一致,这是rabbitmq要求搭建集群所需要的,详情可查看官网,地址:https://www.rabbitmq.com/clustering.html
对于linux系统来说,erlang cookie文件是在/var/lib/rabbitmq/.erlang.cookie
,
注意:erlang.cookie文件是在启动rabbitmq之后就会存在的,
我们只要拷贝任意一台机器上的erlang.cookie文件到其他机器上即可,如我们在第一台机器上依次执行:
scp /var/lib/rabbitmq/.erlang.cookie root@mq2:/var/lib/rabbitmq/
scp /var/lib/rabbitmq/.erlang.cookie root@mq3:/var/lib/rabbitmq/
之后查看erlang.cookie文件是否一致:
第一台机器node1: cat /var/lib/rabbitmq/.erlang.cookie
第二台机器node2: cat /var/lib/rabbitmq/.erlang.cookie
第三台机器node3: cat /var/lib/rabbitmq/.erlang.cookie
后台启动rabbitmq所有节点执行如下命令
rabbitmq-server -detached
-detached
表示以后台的方式进行启动,这种方式启动不能访问rabbitmq的管理页面
。我们之后查看集群状态,任意节点执行:
rabbitmqctl cluster_status
发现当前每个节点还没有搭建集群,节点此时只有自己
之后我们就可以搭建集群了,我们先在node2和node3分别执行加入集群命令:
1.关闭 rabbitmqctl stop_app
2.加入集群 rabbitmqctl join_cluster rabbit@mq1
(注意,mq1表示主机名,不能写ip地址)
3.启动服务 rabbitmqctl start_app
之后在任意一个节点执行查看集群状态命令(rabbitmqctl cluster_status
),如果出现如下显示,集群搭建成功:
Cluster status of node rabbit@mq3 …
[{nodes,[{disc,[rabbit@mq1,rabbit@mq2,rabbit@mq3]}]}, 集群的所有节点
{running_nodes,[rabbit@mq1,rabbit@mq2,rabbit@mq3]}, 正在运用的节点
{cluster_name,<<“rabbit@mq1”>>}, 集群的名字
{partitions,[]},
{alarms,[{rabbit@mq1,[]},{rabbit@mq2,[]},{rabbit@mq3,[]}]}]
之后我们就可以登录每个rabbitmq节点的管理界面,展示如下状态:
之后如果我们在主节点新增一个交换机,则从节点都会进行同步。同理,队列也会进行同步,但是要注意:队列信息虽然会进行同步,但是队列中的消息并不会进行同步(虽然能看到消息的个数),当消费者通过从节点获取队列中的消息时,从节点需要向主节点去获取消息
2. 镜像集群
镜像队列机制就是将队列在三个节点之间设置主从关系,消息会在三个节点之间进行自动同步,且如果其中一个节点不可用,并不会导致消息丢失或服务不可用的情况,提升MQ集群的整体高可用性。官网地址:https://www.rabbitmq.com/ha.html
(1) 集群架构图
(2) 配置集群架构
- 策略说明
rabbitmqctl set_policy [-p <vhost>] [--priority <priority>] [--apply-to <apply-to>] <name> <pattern> <definition>
[]: 表示可省略的参数
-p Vhost: 可选参数,针对指定vhost(节点)下的queue进行设置
[--priority <priority>]:表示策略
[--apply-to <apply-to>]:指定策略用于何处,如交换机还是队列
Name: policy(策略)的名称
Pattern: queue(队列)的匹配模式(正则表达式)
Definition:镜像定义,包括三个部分ha-mode, ha-params, ha-sync-mode
ha-mode:指明镜像队列的模式,有效值为 all/exactly/nodes
all:表示在集群中所有的节点上进行镜像
exactly:表示在指定个数的节点上进行镜像,节点的个数由ha-params指定
nodes:表示在指定的节点上进行镜像,节点名称通过ha-params指定
ha-params:指定ha-mode模式需要用到的参数
ha-sync-mode:进行队列中消息的同步方式,有效值为automatic(自动)和manual(手动)
priority:可选参数,policy的优先级,数字越高优先级越高
- 查看当前策略
rabbitmqctl list_policies
- 添加策略
rabbitmqctl set_policy ha-all '^hello' '{"ha-mode":"all","ha-sync-mode":"automatic"}'
说明:
ha-all表示策略名,自定义的
策略正则表达式为 “^” 表示所有匹配所有队列名称 ^hello:匹配hello开头队列
当我们停掉主节点,发现rabbitmq已经自动切换为了其他从节点为主节点,依旧可以对外提供服务。如果这时重新启动之前的主节点,他也不会变为主节点,而是变为了从节点。
- 删除策略
rabbitmqctl clear_policy ha-all(策略名,自定义的)
源码获取
至此,我们的RabbitMQ的学习
就讲解完成了,源码素材可以通过关注我的微信公众号 我爱学习呀嘻嘻
,回复关键字RabbitMQ源码素材
进行获取哦。