1. 概述
MQ(Message Quene) : 翻译为
消息队列,通过典型的
生产者和
消费者模型,生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,轻松的实现系统间解耦。别名为
消息中间件` 通过利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
1.1 AMQP 协议
AMQP(advanced message queuing protocol)`在2003年时被提出,最早用于解决金融领不同平台之间的消息传递交互问题。顾名思义,AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。这使得实现了AMQP的provider天然性就是跨平台的。以下是AMQP协议模型:
2. 安装RabbitMQ
官网下载地址: https://www.rabbitmq.com/download.html
因为RabbitMQ是用erlang语言写的,我们要下载erlang安装包,还有依赖,我已经下载好
百度网盘地址:链接:https://pan.baidu.com/s/1pn3dzzYLb6fqLkz6cuDrBA
提取码:aaaa
上传至服务器或者虚拟机即可
# 安装erlang依赖包
rpm -ivh erlang-22.0.7-1.el7.x86_64.rpm
# 安装RabbitMQ
yum install -y rabbitmq-server-3.7.18-1.el7.noarch.rpm
# 将刚安装好的mq配置文件复制到/etc/rabbitmq下并改名
cp /usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example /etc/rabbitmq/rabbitmq.config
# 因为我们想看mq的管理页面默认是只有本机可以访问,想开启远程进入需要修改配置文件,打开配置文件
vim /etc/rabbitmq/rabbitmq.config
修改配置文件,把注释去掉即可,注意后面还有一个逗号也要去掉
#想看到管理界面,还需要启动一个插件
rabbitmq-plugins enable rabbitmq_management
#启动/重启/停止/查看 RabbitMQ的服务
systemctl start rabbitmq-server
systemctl restart rabbitmq-server
systemctl stop rabbitmq-server
systemctl status rabbitmq-server
#将防火墙从开机自启移除
systemctl disable firewalld
#将防火墙关闭
systemctl stop firewalld
访问mq管理页面,ip是你服务器的地址
地址: http://IP:15672/ 用户名密码都是guest
下面是登陆之后的样子
页面菜单的解释
connections:无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况
channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。
Exchanges:交换机,用来实现消息的路由
Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。
2.1创建用户
角色的描述
超级管理员(administrator)
可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。监控者(monitoring)
可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)策略制定者(policymaker)
可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。普通管理者(management)
仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。其他
无法登陆管理控制台,通常就是普通的生产者和消费者。
创建虚拟主机
虚拟主机
为了让各个用户可以互不干扰的工作,RabbitMQ添加了虚拟主机(Virtual Hosts)的概念。其实就是一个独立的访问路径,不同用户使用不同路径,各自有自己的队列、交换机,互相不会影响。
用户和虚拟主机绑定 ,点击我们刚创建好的用户
3. 开始写代码
RabbitMQ支持的消息模型
引入依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.2</version>
</dependency>
3.1 任务模型
当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work 模型:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。也就是一个生产者对应多个消费者
废话不多说,上才艺,货哈,易购窝里giao!giao!
//因为我们生产者消费者都要与mq创建连接,提取一个工具类
//连接工具类
public class MqUtil {
static Connection connection = null;
static ConnectionFactory connectionFactory =new ConnectionFactory();
static {
new ConnectionFactory();
connectionFactory.setHost("1.116.221.12");
connectionFactory.setPort(5672);
connectionFactory.setUsername("test1");
connectionFactory.setPassword("test1");
//虚拟主机
connectionFactory.setVirtualHost("/msg");
}
public static Connection getConnection(){
try {
connection = connectionFactory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return connection;
}
//关流方法
public static void closeMq(Connection connection,Channel channel) throws IOException, TimeoutException {
channel.close();
connection.close();
}
}
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = MqUtil.getConnection();
//获取完链接,创建通道
Channel channel = connection.createChannel();
//通道连接队列
//1队列名(如果没有则创建) 2队列是否持久化 3是否独占队列 4是否自动删除 5其他属性
channel.queueDeclare("duilie",true,false,false,null);
//往里面放信息
//1.交换机 2队列名 3其他参数 4要发送的信息
for (int i = 0; i <30 ; i++) {
channel.basicPublish("","duilie", MessageProperties.PERSISTENT_TEXT_PLAIN,("我是信息"+i).getBytes());
}
MqUtil.closeMq(connection,channel);
}
}
参数解释
channel.queueDeclare("duilie",true,false,false,null);
这个方法我的理解就是通道去创建队列 ,确保发送消息时没问题,如果有这个队列名,参数不一致的话会报错,所以如果运行过一次之后,改了参数,在运行,队列名字不改,会报错
第一个参数:队列名
第二个参数:队列是否持久化,为false就是,如果重启mq,该队列会被删除
第三个参数:是否独占队列,如果为true,我试了一下,我的理解就是,只允许一个消费者消费
第四个参数:如果为true当数据消费完并且所有消费者断开连接则删除队列
第五个参数:其他参数,一般为空,我没有去研究channel.basicPublish("","duilie", MessageProperties.PERSISTENT_TEXT_PLAIN,("我是信息"+i).getBytes());
发送消息
第一个参数: 交换机,目前这个模型使用默认交换机
第二个参数:队列名
第三个参数:如果填写null,重启mq没有消费的信息将会丢失,消息是否持久化的意思
第四个参数:要发送的消息,这里规定要传字节数组
消费者一条一条的消费,如果消费完成,会确认一下
我们模拟两个消费者消费,消费者1执行的比较慢,消费者2执行的比较快
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = MqUtil.getConnection();
//获取完链接,创建通道
Channel channel = connection.createChannel();
//指该消费者在接收到队列里的消息但没有返回确认结果之前,队列不会将新的消息分发给该消费者。队列中没有被消费的消息不会被删除,还是存在于队列中
channel.basicQos(1);
//1队列名(如果没有则创建) 2是否持久化 3是否独占队列 4是否自动删除 5其他属性
channel.queueDeclare("duilie",false,false,false,null);
/* 启动一个消费者,并返回服务端生成的消费者标识
* queue:队列名
* autoAck:true 接收到传递过来的消息后acknowledged(应答服务器),false 接收到消息后不应答服务器
* callback: 消费者对象的回调接口
* @return 服务端生成的消费者标识
*/
channel.basicConsume("duilie",false,new DefaultConsumer(channel){
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//让消费者执行的慢一点
Thread.sleep(2000);
System.out.println(new String(body));
//手动确认消息 手动确认消息的标识,是否一次确认多条
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
//消费者不要关闭流,需要一直监听
}
}
public class Consumer2 {
public static void main(String[] args) throws IOException {
Connection connection = MqUtil.getConnection();
//获取完链接,创建通道
Channel channel = connection.createChannel();
channel.basicQos(1);
//1队列名(如果没有则创建) 2是否持久化 3是否独占队列 4是否自动删除 5其他属性
channel.queueDeclare("duilie",false,false,false,null);
/* 启动一个消费者,并返回服务端生成的消费者标识
* queue:队列名
* autoAck:true 接收到传递过来的消息后acknowledged(应答服务器),false 接收到消息后不应答服务器
* callback: 消费者对象的回调接口
* @return 服务端生成的消费者标识
*/
channel.basicConsume("duilie",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
运行生产者,看下管理界面
运行我们的消费者,先运行消费者1,再运行消费者2,打开管理界面
发现所有消息已经被消费,看我们的控制台
可以看到,由于消费者1消费的比较慢,消费者1就消费的少一点,能者多劳嘛
上面这种模型,每一个消息只会被一个消费者执行,但是我想一个消息多个消费者执行呢,就用到了下面这种模型
3.2 广播模型
广播流程
- 可以有多个消费者
- 每个消费者有自己的queue(队列)
- 每个队列都要绑定到Exchange(交换机)
- 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
- 交换机把消息发送给绑定过的所有队列
- 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
创建连接获取通道,关流,代码重复,不再展示
//通道绑定交换机 1.交换机的名称(没有则创建) 2.类型 fanout代表是广播
channel.exchangeDeclare("jiaohuanji","fanout");
//把消息发送到交换机 1.交换机的名称,2.路由key(目前没有用到),3.其他参数,4.发送的消息
channel.basicPublish("jiaohuanji","",null,"我是消息".getBytes());
//绑定交换机
channel.exchangeDeclare("jiaohuanji","fanout");
//创建临时队列,广播不需要搞一个一直持久化的队列浪费资源
//获取临时队列的名字
String queueName = channel.queueDeclare().getQueue();
System.out.println("交换机的名称"+queueName);
//将临时队列绑定交换机 第三个参数:路由key(这里用不到)
channel.queueBind(queueName,"jiaohuanji","");
//处理消息
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(new String(body));
}
});
创建两个消费者,可以再管理界面看到两个临时队列
两个消费者代码一模一样,先运行消费者,在运行生产者,可以看到控制台
两个同时消费到,我们把消费者停掉,去管理界面发现,该通道也被删除
可以看到这种模型,每个消费者,同一条消息每个消费者都可以消费,我现在又有一个需求,根据消息的不同我想把消息,放入多个队列或者某一个队列,这个时候就需要用到我们的路由key了
3.3 Routing(直连)模型
流程
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个
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 的消息
我们现在有个,这样的需求,我error和info类型的日志,都要消费者1处理,消费者2只消费error类型的消息
String exchangeName="logs";
//1.交换机的名字,2.交换机类型(direct)
channel.exchangeDeclare(exchangeName,"direct");
//路由key
String routingKey="info";
//发布消息
channel.basicPublish(exchangeName,routingKey,null,("我是"+routingKey+"类型的消息").getBytes());
//这个消费者可以拿到"error"和"info"的信息
String exchangeName="logs";
Channel channel = connection.createChannel();
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//绑定队列和交换机
channel.queueBind(queue,exchangeName,"error");
channel.queueBind(queue,exchangeName,"info");
//消费消息
channel.basicConsume(queue,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));
}
});
String exchangeName="logs";
String queue = channel.queueDeclare().getQueue();
//这个消费者只消费error的消息
channel.queueBind(queue,exchangeName,"error");
channel.basicConsume(queue,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));
}
});
先运行一下生产者,把交换机创建好,在运行两个消费者,在执行生产者,发送error类型的消息,再执行生产者发送info类型的消息,结果:
可以看到,消费者1消费了路由key为error和info的,而消费者2消费了路由key为error的
3.3 Routing(订阅)模型
Topic类型的
Exchange与
Direct相比,都是可以根据
RoutingKey把消息路由到不同的队列。只不过
Topic类型
Exchange可以让队列在绑定
Routing key的时候使用通配符!这种模型
Routingkey一般都是由一个或多个单词组成,多个单词之间以”.”分割,例如:
item.insert`
统配符
* (star) can substitute for exactly one word. 匹配不多不少恰好1个词
# (hash) can substitute for zero or more words. 匹配一个或多个词
如:
audit.# 匹配audit.irs.corporate或者 audit.irs 等
audit.* 只能匹配 audit.irs
这种和上面那种相比较只是,把交换机的类型直连(direct) 换成了订阅(topic),还有消费者绑定路由key那里可以使用通配符
//生产者代码
//1.交换机的名字,2.交换机类型(topic)
channel.exchangeDeclare("topics","topic");
//路由key
String routingKey="user.save";
//消费者代码
//绑定队列和交换机
channel.queueBind(queue,exchangeName,"user.*");
4. SpringBoot中开发
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件
spring:
application:
name: springboot_rabbitmq
rabbitmq:
host: 10.15.0.9
port: 5672
username: ems
password: 123
virtual-host: /ems
boot里集成了RabbitM使用非常简单
新建一个boot项目,在测试类中,写入消息
4.1 工作模型
@SpringBootTest
class DemoApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testHello(){
for (int i = 0; i <100 ; i++) {
//参数1:队列名,参数二:发送的内容
rabbitTemplate.convertAndSend("hello","第+"+i+"只小阿giao");
}
}
}
写一个消息监听方法
@Component
public class Consumer {
//监听的队列
@RabbitListener(queuesToDeclare = @Queue("hello"))
public void receive1(String message){
System.out.println("消费者1 = " + message);
}
@RabbitListener(queuesToDeclare = @Queue("hello"))
public void receive2(String message){
System.out.println("消费者2 = " + message);
}
}
运行测试方法,结果:
可以看到是,轮询机制,每个消费者消费一条,谁也不多不少
4.2 广播模型
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testFanout() throws InterruptedException {
//1:交换机,2:路由key,3:发送的消息
rabbitTemplate.convertAndSend("logs","","这是日志广播");
}
@Component
//两个消费者
public class FanoutCustomer {
@RabbitListener(bindings = @QueueBinding(
value = @Queue,
exchange = @Exchange(name="logs",type = "fanout")
))
public void receive1(String message){
System.out.println("message1 = " + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue, //创建临时队列
exchange = @Exchange(name="logs",type = "fanout") //绑定交换机类型
))
public void receive2(String message){
System.out.println("message2 = " + message);
}
}
4.3 路由模型
@Autowiredprivate RabbitTemplate rabbitTemplate;
@Testpublic
void testDirect(){
rabbitTemplate.convertAndSend("directs","error","error 的日志信息");
}
@Component
public class DirectCustomer {
@RabbitListener(bindings ={
@QueueBinding(
value = @Queue(),
key={"info","error"},
exchange = @Exchange(type = "direct",name="directs")
)})
public void receive1(String message){
System.out.println("message1 = " + message);
}
@RabbitListener(bindings ={
@QueueBinding(
value = @Queue(),
key={"error"},
exchange = @Exchange(type = "direct",name="directs")
)})
public void receive2(String message){
System.out.println("message2 = " + message);
}
}
4.4 动态路由
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testTopic(){
rabbitTemplate.convertAndSend("topics","user.save.findAll","user.save.findAll 的消息");
}
@Component
public class TopCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
key = {"user.*"},
exchange = @Exchange(type = "topic",name = "topics")
)
})
public void receive1(String message){
System.out.println("message1 = " + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
key = {"user.#"},
exchange = @Exchange(type = "topic",name = "topics")
)
})
public void receive2(String message){
System.out.println("message2 = " + message);
}
}
5.0Mq集群
用java连接和我们普通的一样,搭建的话,等我下篇文章!奥里给!