MQ引言
什么是MQ
MQ
(Message Quene):翻译为消息队列
,通过典型的生产者
和消费者
模型生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入轻松的实现系统间解耦。别名为消息中间件
,通过利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
MQ有哪些
当今市面上有很多主流的消息中间件,如老牌的ActiveMQ
、RabbitMQ
,炙手可热的Kafka
,阿里巴巴自主开发RocketMQ
等。
不同MQ特点
-
ActiveMQ
方案成熟/性能缺陷(小公司)ActiveMQ、是Apache出品,最流行的,能力强劲的开源消息总线。它是一个完全支持JNS规范的的消息中间件。丰富的APT,多种集群架构模式让认kctivelo在业界成为老牌的消息中间件,在中小型企业颇受欢迎!
-
Kafka
性能强劲/一致性堪忧(大数据)Kafka是LinkedIn开源的分布式发布-订阅消息系统,目前归属于Apache顶级项目。Kafka主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。
-
RocketMQ
没有缺点/未完全开源(大公司)RocketMQ是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。
-
RabbitMQ
最佳平替RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由〈包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。
RabbitMQ引言
基于
AMOP
协议,erlang语言开发,是部署最广泛的开源消息中间件是最受欢迎的开源消息中间件之一。
AMQP协议
AMQP(advanced message queuing protocol)在2003年时被提出,最早用于解决金融领不同平台之间的消息传递交互问题。顾名思义,AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。这使得实现了AMQP的
provider天然性就是跨平台的。以下是AMQP协议模型:
Centos7安装配置
官网:https://www.rabbitmq.com/
由于是Centos7所以只能下载带el7标识的
**注意:**RabbitMQ的版本和Erlang的版本是有关联的,需要在官网中去查阅
-
下载Erlang安装包
erlang-23.3.4.11-1.el7.x86_64.rpm
-
下载RabbitMQ安装包
rabbitmq-server-3.8.30-1.el7.noarch.rpm
-
进入云服务器中,将文件放到/home路径下
-
安装socat
yum -y install socat
-
安装Erlang
rpm -ivh erlang-23.3.4.11-1.el7.x86_64.rpm
-
输入
erl
查看是否安装成功 -
安装RabbitMQ
rpm -ivh rabbitmq-server-3.8.30-1.el7.noarch.rpm
-
运行
cd /etc/rabbitmq
,编写配置文件如果想要使guest用户远程访问的话可以设置
那么我们guest用户的账号和密码都是
guest
vi rabbitmq.config
新建文件,写入如下[ {rabbit, [{loopback_users, []}]} ].
-
启动网页管理插件
rabbitmq-plugins enable rabbitmq_management
-
查看RabbitMQ运行状态
systemctl status rabbitmq-server
-
启动RabbitMQ
systemctl start rabbitmq-server
启动完成后再查看一下状态
-
停止RabbitMQ
如果要停止,可以使用以下命令
systemctl stop rabbitmq-server
-
防火墙设置
可以通过以下的命令进行查看防火墙状态
systemctl status firewalld
如果防火墙是开启的,你可以通过放行15672端口或者关闭防火墙
-
远程访问RabbitMq
通过IP地址:15672访问
用户名和密码如果配置了guest的话,那么就都是guest
添加新用户
使用代码添加
如果不想使用guest用户访问的话,可以通过添加新用户的方式来访问
- 添加用户,前面是账号,后面是密码
rabbitmqctl add_user admin password
- 给新用户分配操作权限
rabbitmqctl set_user_tags admin administrator
- 给新用户分配资源权限,如果是赋予超级管理员的权限的话,下面的就不用再授权了
rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
在web界面添加
RabbitMQ的使用
引入依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.9.0</version>
</dependency>
第一种模型(直连)
开发生产者
package com.lzj;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;
import java.io.IOException;
import java.util.Calendar;
import java.util.concurrent.TimeoutException;
/**
* <p>
* 生产者
* </p>
*
* @author:雷子杰
* @date:2022/10/26
*/
public class Provider {
@Test
public void testSendMessage() throws IOException, TimeoutException {
//创建连接mq的连接工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("120.79.188.192");
//设置端口号
connectionFactory.setPort(5672);
//设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
//设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("LeiZijie");
connectionFactory.setPassword("66323192");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接中通道
Channel channel = connection.createChannel();
//通道绑定对应消息队列
//参数1:队列名称,不存在则自动创建
//参数2:用来定义队列是否要持久化
//参数3:是否独占队列
//参数4:是否在消费完成后自动删除队列
//参数5:额外附加参数
channel.queueDeclare("hello",false,false,false,null);
//发布消息
//参数1:交换机名称
//参数2:队列名称
//参数3:传递消息额外配置
//参数4:消息的具体内容
//参数3可以设置成 MessageProperties.PERSISTENT_TEXT_PLAIN,那么参数就可以完成持久化
channel.basicPublish("","hello",null,"hello rabitmq".getBytes());
channel.close();
connection.close();
}
}
web中的结果
开发消费者
注意,由于消费者获取是使用的多线程方式,所以不能在test方法中。
在消费者中,最后不要关闭通道和连接,否则可能会导致消息无法获取
package com.lzj;
import com.rabbitmq.client.*;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
*
* </p>
*
* @author:雷子杰
* @date:2022/10/26
*/
public class Customer {
public static void main(String[] args) throws IOException, TimeoutException {
//创建工厂连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("120.79.188.192");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/ems");
connectionFactory.setUsername("LeiZijie");
connectionFactory.setPassword("66323192");
//创建连接对象
Connection connection = connectionFactory.newConnection();
//创建通道
Channel channel = connection.createChannel();
//通道绑定对象
channel.queueDeclare("hello",false,false,false,null);
//消费信息
//参数1:消费那个队列的消息,队列名称
//参数2:开始消息的自动确认机制
//参数3:消费时的回调接口
channel.basicConsume("hello",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)="+new String(body));
}
});
//channel.close();
//connection.close();
}
}
第二种模型(work queues)
工作队列(又名:任务队列)背后的主要思想是避免立即执行资源密集型任务而不得不等待它完成。相反,我们将任务安排在以后完成。我们将任务封装 为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当您运行许多工作人员时,任务将在他们之间共享。
这个概念在 Web 应用程序中特别有用,在这些应用程序中,无法在短暂的 HTTP 请求窗口中处理复杂的任务。
角色:
- P:生产者
- C1:消费者1
- C2:消费者2
生产者
package com.lzj.workqueues;
import com.lzj.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
/**
* <p>
*
* </p>
*
* @author:雷子杰
* @date:2022/10/26
*/
public class Provider {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("work",true,false,false,null);
for (int i = 0; i < 10; i++) {
//生产消息
channel.basicPublish("","work",null,(i+"hello work").getBytes());
}
RabbitMQUtils.closeConnection(channel,connection);
}
}
消费者1
package com.lzj.workqueues;
import com.lzj.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* <p>
*
* </p>
*
* @author:雷子杰
* @date:2022/10/26
*/
public class Customer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("work",true,false,false,null);
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));
}
});
}
}
消费者2
package com.lzj.workqueues;
import com.lzj.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* <p>
*
* </p>
*
* @author:雷子杰
* @date:2022/10/26
*/
public class Customer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("work",true,false,false,null);
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("消费者-2:"+new String(body));
}
});
}
}
能者多劳配置
将消费者中配置修改下即可
//每次只能消费一次
channel.basicQos(1);
channel.queueDeclare("work",true,false,false,null);
//参数二由true改为false
channel.basicConsume("work",false,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));
//手动确认消息
channel.basicAck(envelope.getDeliveryTag(),false);//手动确认消息
}
});
第三种模型(fanout)
fanout:扇出,也称为广播
在广播模式下,消息发送流程是这样的:
- 可以有多个消费者
- 每个消费者有自己的queue(队列)
- 每个队列都要绑定到Exchange(交换机)
- 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
- 交换机把消息发送给绑定过的所有队列
队列的消费者都能拿到消息。实现一条消息被多个消费者消费
生产者
package Fanout;
import Utils.MQConnection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Provider {
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接对象
Connection connection = MQConnection.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数:交换机名称、交换机类型(fanout为广播类型)
channel.exchangeDeclare("logs","fanout");
// 发送消息
// fanout中routingkey没意义
channel.basicPublish("logs", "", null, ("fanout type message").getBytes());
// 关闭资源
MQConnection.closeChannelAndConnection(channel, connection);
}
}
消费者(1,2,3)
由于消费者代码基本相同,只有输出的信息不同,所以这里就不写出来了
package Fanout;
import Utils.MQConnection;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = MQConnection.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数:交换机名称、交换机类型(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));
}
});
}
}
第四种模型(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 的消息
生产者
package Direct;
import Utils.MQConnection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Provider {
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接对象
Connection connection = MQConnection.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数:交换机名称(自己起)、交换机类型(direct为路由模式)
channel.exchangeDeclare("logs_direct", "direct");
// 发送消息
String routingKey = "error";
// fanout中routingkey没意义
channel.basicPublish("logs_direct", routingKey, null,
("这是direct模型发布的基于routingkey:[" + routingKey + "] 发送的消息").getBytes());
// 关闭资源
MQConnection.closeChannelAndConnection(channel, connection);
}
}
消费者1
package Direct;
import Utils.MQConnection;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = MQConnection.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数:交换机名称、交换机类型
channel.exchangeDeclare("logs_direct", "direct");
// 临时队列
String queueName = channel.queueDeclare().getQueue();
// 基于routingKey绑定交换机和队列
channel.queueBind(queueName, "logs_direct", "error");
// 消费消息
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));
}
});
}
}
消费者2
package Direct;
import Utils.MQConnection;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer2 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = MQConnection.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数:交换机名称、交换机类型
channel.exchangeDeclare("logs_direct", "direct");
// 临时队列
String queueName = channel.queueDeclare().getQueue();
// 基于routingKey绑定交换机和队列
channel.queueBind(queueName, "logs_direct", "error");
channel.queueBind(queueName, "logs_direct", "info");
channel.queueBind(queueName, "logs_direct", "warning");
// 消费消息
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));
}
});
}
}
测试结果
当生产者发送Route key为error的消息时
当生产者发送Route key为info的消息时
第五种模型(Routing之订阅模型-Topic)
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key的时候使用通配符
!这种模型Routingkey 一般都是由一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
统配符:
*
:匹配恰好1个单词#
:匹配一个或多个单词
如:
audit.#
:匹配audit.irs.corporate
或者audit.irs
等audit.*
:只能匹配audit.irs
*.audit.#
:中间必须是audit,audit前有一个或多个单词,后有一个单词
生产者
package Topic;
import Utils.MQConnection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Provider {
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接对象
Connection connection = MQConnection.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数:交换机名称(自己起)、交换机类型(topic为动态路由)
channel.exchangeDeclare("topics", "topic");
String routingKey = "user.save";
// 发送消息
// fanout中routingkey没意义
channel.basicPublish("topics", routingKey, null,
("这是topic动态路由模型,routingkey:[" + routingKey + "]").getBytes());
// 关闭资源
MQConnection.closeChannelAndConnection(channel, connection);
}
}
消费者(1,2)
消费者1和2的不同之处在于类名、第20行、第25行的名字
package Topic;
import Utils.MQConnection;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = MQConnection.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 将通道声明指定的交换机
// 参数:交换机名称、交换机类型
channel.exchangeDeclare("topics", "topic");
// 临时队列
String queueName = channel.queueDeclare().getQueue();
// 基于通配符形式routingKey绑定交换机和队列
// Consumer2设为user.#
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));
}
});
}
}
测试结果
SpringBoot整合RabbitMQ
搭建环境
-
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <version>2.7.5</version> </dependency>
-
配置文件设置
spring: rabbitmq: host: 120.79.188.192 port: 5672 username: LeiZijie password: 66323192 virtual-host: /ems
第一种hello worl模型使用
生产者
package ecnu.cn;
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;
@SpringBootTest(classes = RabbitmqSpringbootApplication.class)
@RunWith(SpringRunner.class)
public class TestRabbitMQ {
// 注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// hello world
@Test
public void test(){
rabbitTemplate.convertAndSend("hello","hello world");
}
}
消费者
package ecnu.cn.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;
@Component
// 指定监听hello队列
// 默认是持久化、非独占、不自动删除队列的
@RabbitListener(queuesToDeclare = @Queue(value = "hello", durable = "true", exclusive = "true", autoDelete = "true"))
public class Consumer {
@RabbitHandler
public void receive(String message) {
System.out.println("message: " + message);
}
}
第二种work模型使用
生产者
// 注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// work
@Test
public void testWork() {
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("work", "work模型" + i);
}
}
消费者
package ecnu.cn.Work;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class WorkConsumer {
// 第1个消费者
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive1(String message) {
System.out.println("message1: " + message);
}
// 第2个消费者
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive2(String message) {
System.out.println("message2: " + message);
}
}
Fanout广播模型
生产者
// 注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// fanout 广播
@Test
public void testFanout(){
// 注意参数多了个exchange
rabbitTemplate.convertAndSend("logs", "", "Fanout模型发送的消息");
}
消费者
package ecnu.cn.Fanout;
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);
}
}
Route路由模型
生产者
// 注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// route 路由模式
@Test
public void testRoute(){
rabbitTemplate.convertAndSend("directs","info","发送info的key的路由信息");
}
消费者
package ecnu.cn.Route;
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"}
))
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"
))
public void receive2(String message) {
System.out.println("message2: " + message);
}
}
Topic订阅模型(动态路由模型)
生产者
// 注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
// topic 动态路由 订阅模式
@Test
public void testTopic(){
rabbitTemplate.convertAndSend("topics","user.save","user.save 路由信息");
}
消费者
package ecnu.cn.Topic;
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,
exchange = @Exchange(value = "topics", type = "topic"),
key = {"user.save", "user.*"}
))
public void receive1(String message) {
System.out.println("message1: " + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue,
exchange = @Exchange(value = "topics", type = "topic"),
key = {"order.#", "produce.#", "user.*"}
))
public void receive2(String message) {
System.out.println("message2: " + message);
}
}
MQ的应用场景
异步处理
场景说明
用户注册后,需要发注册邮件和注册短信,传统的做法有两种 1.串行的方式 2.并行的方式
串行方式
将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西
并行方式
将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间。
消息队列
假设三个业务节点分别使用50ms,串行方式使用时间150ms,并行使用时间100ms。虽然并行已经提高的处理时间,但是,前面说过,邮件和短信对我正常的使用网站没有任何影响,客户端没有必要等着其发送完成才显示注册成功,应该是写入数据库后就返回. 消息队列: 引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理
由此可以看出,引入消息队列后,用户的响应时间就等于写入数据库的时间+写入消息队列的时间(可以忽略不计),引入消息队列后处理后,响应时间是串行的3倍,是并行的2倍
应用解耦
场景
双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口.
缺点
当库存系统出现故障时,订单就会失败。 订单系统和库存系统高耦合. 引入消息队列
订单系统
用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
库存系统
订阅下单的消息,获取下单消息,进行库操作。 就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失
流量削峰
场景
秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。
作用
- 可以控制活动人数,超过此一定阀值的订单直接丢弃(我为什么秒杀一次都没有成功过呢^^)
- 可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)
注意
- 用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面.
- 秒杀业务根据消息队列中的请求信息,再做后续处理
RabbitMQ的集群
普通集群(副本集群)
All data/state required for the operation of a RabbitMQ broker is replicated across all nodes. An exception to this are message queues, which by default reside on one node, though they are visible and reachable from all nodes. To replicate queues across nodes in a cluster --摘自官网
默认情况下:RabbitMQ代理操作所需的所有数据/状态都将跨所有节点复制。这方面的一个例外是消息队列,默认情况下,消息队列位于一个节点上,尽管它们可以从所有节点看到和访问
架构图
核心解决问题: 当集群中某一时刻master节点宕机,可以对Queue中信息,进行备份
集群搭建
-
集群规划
node1: 10.15.0.3 mq1 master 主节点 node2: 10.15.0.4 mq2 repl1 副本节点 node3: 10.15.0.5 mq3 repl2 副本节点
-
克隆三台机器主机名和ip映射
vim /etc/hosts加入: 10.15.0.3 mq1 10.15.0.4 mq2 10.15.0.5 mq3 node1: vim /etc/hostname 加入: mq1 node2: vim /etc/hostname 加入: mq2 node3: vim /etc/hostname 加入: mq3
-
三个机器安装rabbitmq,并同步cookie文件,在node1上执行:
scp /var/lib/rabbitmq/.erlang.cookie root@mq2:/var/lib/rabbitmq/ scp /var/lib/rabbitmq/.erlang.cookie root@mq3:/var/lib/rabbitmq/
-
查看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
-
在node2和node3执行加入集群命令:
1.关闭 rabbitmqctl stop_app 2.加入集群 rabbitmqctl join_cluster rabbit@mq1 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,[]}]}]
-
登录管理界面,展示如下状态:
-
测试集群在node1上,创建队列
-
查看node2和node3节点:
-
关闭node1节点,执行如下命令,查看node2和node3:
rabbitmqctl stop_app
镜像集群
This guide covers mirroring (queue contents replication) of classic queues --摘自官网
By default, contents of a queue within a RabbitMQ cluster are located on a single node (the node on which the queue was declared). This is in contrast to exchanges and bindings, which can always be considered to be on all nodes. Queues can optionally be made mirrored across multiple nodes. --摘自官网
镜像队列机制就是将队列在三个节点之间设置主从关系,消息会在三个节点之间进行自动同步,且如果其中一个节点不可用,并不会导致消息丢失或服务不可用的情况,提升MQ集群的整体高可用性。
集群架构图
配置集群架构
-
策略说明
rabbitmqctl set_policy [-p <vhost>] [--priority <priority>] [--apply-to <apply-to>] <name> <pattern> <definition> -p Vhost: 可选参数,针对指定vhost下的queue进行设置 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"}' 说明:策略正则表达式为 “^” 表示所有匹配所有队列名称 ^hello:匹配hello开头队列
-
删除策略
rabbitmqctl clear_policy ha-all
-
测试集群
遇到的问题
centos7yum安装RabbitMQ之后没有rabbitmq.config配置文件
官网给出的:
翻译一下就是,安装之后不会自动给你创建配置文件,你可以在一下几个地址上自己创建。
官网给出的配置例子:rabbitmq.conf.example