MQ概念
MQ
(Message Quene) : 翻译为消息队列
,通过典型的 生产者
和消费者
模型,生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步
的,而且只关心消息的发送和接收,没有业务逻辑的侵入,轻松的实现系统间解耦。别名为 消息中间件
,通过利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
MQ种类
目前还是有很多主流的消息中间件,例如ActiveMQ
、RabbitMQ
,大数据常用的Kafka
,RocketMQ
等。
RabbitMQ
该消息中间件是基于AMQP
协议,采用erlang语言开发,是部署最广泛的开源消息中间件,是最受欢迎的开源消息中间件之一。
官网:RabbitMQ
RabbitMQ的安装
1、安装包下载:官方下载地址
2、下载的安装包包括(在centos7下安装):
3、步骤:
1、将上面两个安装包放到centos下的root目录
2、安装Erlang依赖包
- rpm -ivh erlang-22.0.7-1.el7.x86_64.rpm
3、安装RabbitMQ安装包
- yum install -y rabbitmq-server-3.7.18-1.el7.noarch.rpm
4、安装好后将配置文件复制到指定位置
- cp /usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example /etc/rabbitmq/rabbitmq.config
5、修改配置文件
- vim /etc/rabbitmq/rabbitmq.config
修改后如下图所示:
6、启动RabbitMQ中的插件管理
- rabbitmq-plugins enable rabbitmq_management
7、启动RabbitMQ服务
systemctl start rabbitmq-server 启动
systemctl restart rabbitmq-server 重启
systemctl stop rabbitmq-server 停止
8、查看服务是否启动
- systemctl status rabbitmq-server
9、这时候在外部是访问不了的,这时候需要关闭centos的防火墙
- systemctl disable firewalld
- systemctl stop firewalld
10、访问Web界面
- http://192.168.1.25:15672
11、默认账号密码都是guest
如下图:
Web界面总览
主界面
参数介绍:
- connections:无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况
- channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。
- Exchanges:交换机,用来实现消息的路由与转发
- Queues:队列,即消息队列,消息存放在队列中,等待消费,消息被消费后被移除队列。
添加用户
用户权限选择:
-
超级管理员(administrator)
可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。
-
监控者(monitoring)
可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
-
策略制定者(policymaker)
可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。
-
普通管理者(management)
仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
-
其他
无法登陆管理控制台,通常就是普通的生产者和消费者。
添加虚拟主机
1、每个虚拟主机就是一个独立的访问路径,不同用户使用不同路径,各自有自己的队列、交换机,互相不会影响。
2、创建好虚拟主机,给相应用户添加访问权限
RabbitMQ应用场景
-
异步处理
例如:在电商系统中,用户完成付款下单后,需要发送订单生成消息和积分累加消息,通过消息中间件的异步处理,可以同时解决这两件时间,并行的方式能提高处理的时间 -
应用解耦
例如:系统在用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口.
但这种做法有一个缺点:当库存系统出现故障时,订单就会失败。 订单系统和库存系统高耦合,引入消息队列后
订单系统:
用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。库存系统:
订阅下单的消息,获取下单消息,进行库的操作。 就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失 -
流量削峰
例如:在秒杀活动中,一般都会是在短时间内大量的流量访问,导致服务器承受压力过大,导致应用挂掉,应用消息队列后,可实现:可以控制活动人数,超过此一定阀值的订单直接丢弃 可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单并处理)
RabbitMQ入门使用
RabbitMQ支持的处理模型(第6种涉及微服务暂不使用)
1、建立Maven项目,导入依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.2</version>
</dependency>
2、第一种模型(直连)测试:
理解:
- P:生产者,也就是要发送消息的程序
- C:消费者:消息的接受者,会一直等待消息到来。
- queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
封装获取ConnectionFactory对象的方法
public class ConnectionConfig {
public static ConnectionFactory create(){
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.1.25");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/test");
return connectionFactory;
}
}
生产者
//获取连接工厂
ConnectionFactory connectionFactory = ConnectionConfig.create();
//创建链接对象
Connection connection = connectionFactory.newConnection();
//创建通道
Channel channel = connection.createChannel();
/**
* '参数1':用来声明通道对应的队列
* '参数2':用来指定是否持久化队列
* '参数3':用来指定是否独占队列
* '参数4':用来指定是否自动删除队列
* '参数5':对队列的额外配置
*
* **/
channel.queueDeclare("hello",true,false,false,null);
for (int i=0;i<10;i++){
channel.basicPublish("","hello",null,(i+"hello,rabbitmq").getBytes());
}
channel.close();
connection.close();
消费者
ConnectionFactory connectionFactory = ConnectionConfig.create();
Connection connection = connectionFactory.newConnection();
//创建通道
Channel channel = connection.createChannel();
channel.queueDeclare("hello",true,false,false,null);
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));
}
});
3、第二种模型(work queue)测试
当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work 模型:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。
理解:
- P:生产者:任务的发布者
- C1:消费者-1,领取任务并且完成任务,假设完成速度较慢
- C2:消费者-2:领取任务并完成任务,假设完成速度快
生产者
ConnectionFactory connectionFactory = ConnectionConfig.create();
//创建链接
Connection connection = connectionFactory.newConnection();
//创建通道
Channel channel = connection.createChannel();
/**
* '参数1':用来声明通道对应的队列
* '参数2':用来指定是否持久化队列
* '参数3':用来指定是否独占队列
* '参数4':用来指定是否自动删除队列
* '参数5':对队列的额外配置
*
* **/
channel.queueDeclare("hello2",false,false,false,null);
for (int i=0;i<30;i++){
channel.basicPublish("","hello2",null,(i+"欢迎抢夺rabbitmq").getBytes());
}
channel.close();
connection.close();
消费者-1
ConnectionFactory connectionFactory = ConnectionConfig.create();
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello2",false,false,false,null);
channel.basicConsume("hello2",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));
}
});
消费者-2
ConnectionFactory connectionFactory = ConnectionConfig.create();
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello2",false,false,false,null);
channel.basicConsume("hello2",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try{
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("消费者2:"+new String(body));
}
});
默认情况下,RabbitMQ将按顺序将每个消息发送给下一个使用者。平均而言,每个消费者都会收到相同数量的消息。
由上面的测试可发现,消费者2处理消息较慢,但还是平均分配一半消息给他处理,但实际是想实现处理快的消费者能者多劳,处理慢的消费者就尽量分配少的消息
解决方法:消息自动确认机制
//一次只接受一条消息,这样能实现能者多劳的效果
channel.basicQos(1);
channel.queueDeclare("hello2",false,false,false,null);
channel.basicConsume("hello2",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try{
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("消费者2:"+new String(body));
//手动确认消息
channel.basicAck(envelope.getDeliveryTag(),false);
4、第三种模型(fanout)测试
理解:
在广播模式下,消息发送流程是这样的:
- 可以有多个消费者
- 每个消费者有自己的queue(队列)
- 每个队列都要绑定到Exchange(交换机)
- 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
- 交换机把消息发送给绑定在交换机上的所有队列
- 队列上的消费者都能拿到消息。实现一条消息被多个消费者消费
生产者
ConnectionFactory connectionFactory = ConnectionConfig.create();
//创建链接
Connection connection = connectionFactory.newConnection();
//创建通道
Channel channel = connection.createChannel();
//定义交换机名为log,类型为fanout(扇出)
channel.exchangeDeclare("log", "fanout"); //广播 一条消息多个消费者同时消费
//将消息发送到交换机上
for (int i=0;i<20;i++){
channel.basicPublish("log", "", null, "你好,这是通过交换机广播的信息".getBytes());
}
channel.close();
connection.close();
消费者-1
ConnectionFactory connectionFactory = ConnectionConfig.create();
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("log","fanout");
//创建临时的队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定到交换机上
channel.queueBind(queue,"log","");
//处理消息
channel.basicConsume(queue,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));
}
});
消费者-2
ConnectionFactory connectionFactory = ConnectionConfig.create();
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
//绑定交换机
channel.exchangeDeclare("log","fanout");
//创建临时的队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定到交换机上
channel.queueBind(queue,"log","");
//处理消息
channel.basicConsume(queue,false,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));
}
});
5、第四种模型Direct(直连)测试:
第三种模型(fanout)中,一条消息会被订阅到交换机上的所有队列消费,在某些场景下,并不适用,我们希望在某些场景下不同的消息被不同的队列消费。
在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 的消息
生产者
ConnectionFactory connectionFactory = ConnectionConfig.create();
//创建链接
Connection connection = connectionFactory.newConnection();
//创建通道
Channel channel = connection.createChannel();
//定义交换机名为log,类型为direct
channel.exchangeDeclare("log_direct", "direct");
for (int i=0;i<20;i++){
channel.basicPublish("log_direct", "info", null, "你好,这是通过直连交换机广播的info信息".getBytes()); //rout-key为info类型
channel.basicPublish("log_direct", "error", null, "你好,这是通过直连交换机广播的error信息".getBytes()); //rout-key为error类型
}
channel.close();
connection.close();
消费者-1(消费info类型信息)
ConnectionFactory connectionFactory = ConnectionConfig.create();
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
//绑定交换机
channel.exchangeDeclare("log_direct","direct");
//创建临时的队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定到交换机上,并指定routing-key,这样方便接收指定的消息
channel.queueBind(queue,"log_direct","info");
//处理消息
channel.basicConsume(queue,false,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));
}
});
消费者-2(消费error类型信息)
ConnectionFactory connectionFactory = ConnectionConfig.create();
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
//绑定交换机
channel.exchangeDeclare("log_direct","direct");
//创建临时的队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定到交换机上
channel.queueBind(queue,"log_direct","error");
//处理消息
channel.basicConsume(queue,false,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));
}
});
6、第五种模型-订阅模型(Topic)测试:
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符
!这种模型Routingkey
一般都是由一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
统配符
* 匹配不多不少恰好1个词
# 匹配一个或多个词
生产者
ConnectionFactory connectionFactory = ConnectionConfig.create();
//创建链接
Connection connection = connectionFactory.newConnection();
//创建通道
Channel channel = connection.createChannel();
//定义交换机名为log_topic,类型为topic
channel.exchangeDeclare("log_topic", "topic");
for (int i=0;i<20;i++){
channel.basicPublish("log_topic", "info.message", null, "你好,这是通过直连交换机广播的info信息".getBytes());
channel.basicPublish("log_topic", "info.message.m2", null, "你好,这是通过直连交换机广播的info2信息".getBytes());
channel.basicPublish("log_topic", "error.message", null, "你好,这是通过直连交换机广播的error信息".getBytes());
channel.basicPublish("log_topic", "error.message.m3", null, "你好,这是通过直连交换机广播的error2信息".getBytes());
}
channel.close();
connection.close();
消费者-1(通配符为*)
ConnectionFactory connectionFactory = ConnectionConfig.create();
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
//绑定交换机
channel.exchangeDeclare("log_topic","topic");
//创建临时的队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定到交换机上
channel.queueBind(queue,"log_topic","info.*");
//处理消息
channel.basicConsume(queue,false,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));
}
});
消费者-2(通配符为#)
ConnectionFactory connectionFactory = ConnectionConfig.create();
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
//绑定交换机
channel.exchangeDeclare("log_topic","topic");
//创建临时的队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定到交换机上
channel.queueBind(queue,"log_topic","error.#");
//处理消息
channel.basicConsume(queue,false,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));
}
});
SpringBoot中使用RabbitMQ
1、新建Springboot项目
2、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3、配置文件
spring.application.name=springboot_rabbitmq
spring.rabbitmq.host=192.168.1.25
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/test
4、测试
RabbitTemplate
用来简化操作 使用时候直接在项目中注入即可使用
第一种模型
生产者
@Autowired
private RabbitTemplate rabbitTemplate;
//第一种hello world模型
@Test
void test01() {
rabbitTemplate.convertAndSend("hello","hello word");
}
消费者
@Component
@RabbitListener(queuesToDeclare = @Queue("hello"))
public class consumer {
@RabbitHandler
public void handler(String message){
System.out.println("接收到的消息:"+message);
}
}
第二种模型( work)
生产者
//第二种work模型使用
@Test
public void test02(){
for (int i=0;i<10;i++){
rabbitTemplate.convertAndSend("work","hello word");
}
消费者
@Component
public class WorkConsumer {
@RabbitListener(queuesToDeclare = @Queue("work"))
public void handler(String message){
System.out.println("消费者1: "+message);
}
@RabbitListener(queuesToDeclare = @Queue("work"))
public void handler2(String message){
System.out.println("消费者2: "+message);
}
}
第三种模型(Fanout)
生产者
//第三种:Fanout 广播模型
@Test
public void test03(){
for (int i=0;i<10;i++){
rabbitTemplate.convertAndSend("logs","","这是广播方式");
}
}
消费者
@Component
public class FanoutConsumer {
@RabbitListener(bindings = @QueueBinding(
value = @Queue, //创建临时的队列
exchange = @Exchange(name = "logs",type = "fanout") //设置要绑定的交换机和类型
))
public void handler(String message){
System.out.println("消费者一:消息是: "+message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue, //创建临时的队列
exchange = @Exchange(name = "logs",type = "fanout") //设置要绑定的交换机和类型
))
public void handler2(String message){
System.out.println("消费者二:消息是: "+message);
}
}
第四种模型(Route)
生产者
//Route 路由模型
@Test
public void test04(){
for (int i=0;i<10;i++){
rabbitTemplate.convertAndSend("logs_direct","info","这是info的信息");
rabbitTemplate.convertAndSend("logs_direct","error","这是error的信息");
}
}
消费者
@Component
public class DirectConsumer {
@RabbitListener(bindings = {
@QueueBinding(
value=@Queue(),
key = {"info","error"},
exchange=@Exchange(name = "logs_direct",type = "direct")
)
})
public void handler(String message){
System.out.println("message:"+message);
}
@RabbitListener(bindings = {
@QueueBinding(
value=@Queue(),
key = {"info"},
exchange=@Exchange(name = "logs_direct",type = "direct")
)
})
public void handler2(String message){
System.out.println("message2:"+message);
}
}
第五种模型(Topic动态路由模型)
生产者
//Topic 订阅模型(动态路由模型)
@Test
public void test05(){
for (int i=0;i<10;i++){
rabbitTemplate.convertAndSend("logs_topic","info.message","这是info的信息");
rabbitTemplate.convertAndSend("logs_topic","error.message","这是error的信息");
}
}
消费者
@Component
public class TopicConsumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
key = {"info.*"},
exchange= @Exchange(type = "topic",name = "logs_topic")
)
})
public void handler(String message){
System.out.println("message: "+message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
key = {"error.#"},
exchange= @Exchange(type = "topic",name = "logs_topic")
)
})
public void handler2(String message){
System.out.println("message2 : "+message);
}
}
RabbitMQ集群
本节后续再更新吧