目录
四、RabbitMQ之Work Queues模型(工作队列)
一、RabbitMQ的基本概念了解
1.什么是MQ
MQ(消息队列),通过典型的生产者和消费者模型,生产者不断地向消息队列中生产消息,消费者不断地从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接受,没有业务逻辑的进入,轻松的实现了系统的解耦。
MQ别名消息中间件,通过利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
2.MQ有哪些
老牌的:ActiveMQ,RabbitMQ
炽手可热:Kafka
阿里研发的:RocketMQ
3.不同MQ特点
(1)ActiveMQ
优点:
完全支持JMS规范,API丰富,多种集群架构模式
缺点:
性能受约束,只适合中小型企业
(2)Kafka
优点:
kafka主要特点是基于pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。性能非常高。
缺点:
不支持事务,对消息重复、丢失、错误没有严格要求,不适合做重要数据的mq
(3)RocketMQ
优点:
性能不错,基于kafka开发,但是对可靠性做了优化
缺点:
开源的RocketMQ不支持事务,需要花钱购买才支持
(4)RabbitMQ
优点:
基于AMQP协议实现,AMQP天生就适合mq。还能与spring做无缝对接。对数据的一致性和可靠性是最高的。
缺点:
源码使用Erlang语言开发的
二、Rabbit的下载和安装
官网地址:https://www.rabbitmq.com/#getstarted
我们使用docker来下载:
systemctl start docker
docker pull rabbitmq:3.8-management
docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3.8-management
注:如果你下载不下来,试试这个:
vi /etc/resolv.conf
#在里面添加如下
nameserver 8.8.8.8
nameserver 114.114.114.114
然后查看:
http://192.168.210.132:15672/#/
账号名和密码:guest
三、RabbitMQ之Helloworld模型(直连)
1.直连模型
这是第一种模型:直连模型
P:生产者
C:消费者
注:一个生产者只允许一个消费者使用(业务场景:登录发短信)
queue:消息队列,红色部分类似于一个邮箱,可以缓存消息。生产者向其中投递消息,消费者从其中取出消息
2.直连模型的实例:生产者
(1)创建项目
(2)添加依赖
<!--引入rabbitmq的相关依赖-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.2</version>
</dependency>
(3)rabbitmq中创建虚拟主机和用户
先来创建虚拟主机:
再来添加一个用户:
此处我们的用户就多了一个创建的ems:
将ems用户绑定到ems虚拟主机:
我们点击用户name:
至此我们ems用户和ems主机就
绑定上了:
(4)设置一下目录结构
懂的都懂:
(5)创建一个生产者
Provider:
package helloworld;
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.concurrent.TimeoutException;
public class Provider {
//生产消息
@Test
public void testSendMessage() throws IOException, TimeoutException {
//创建连接mq的连接工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("192.168.210.132");
//设置端口号
connectionFactory.setPort(5672);
//设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
//设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接中的通道
Channel channel = connection.createChannel();
//通道绑定对应消息队列
//参数1:队列名称,如果队列不存在自动创建
//参数2:用来定义队列特性是否要持久化,true持久化,false不持久化
//参数3:是否独占队列,true独占,false不独占
//参数4:是否在消费完成后自动删除队列,true自动删除,false不自动删除
//参数5:额外附加参数
channel.queueDeclare("hello",false,false,false,null);
//发布消息
//参数1:交换机名称
//参数2:队列名称
//参数3:传递消息额外参数
//参数4:消息的具体内容
channel.basicPublish("","hello",null,"hello rabbitnq".getBytes());
channel.close();
connection.close();
}
}
编写完代码后我们运行两次,再看rabbitmq:
3.直连模型的实例:消费者
Consumer:
前面和生产者一样,就是这样离变成了消费者
package helloworld;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接mq的连接工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("192.168.210.132");
//设置端口号
connectionFactory.setPort(5672);
//设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
//设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接中的通道
Channel channel = connection.createChannel();
//通道绑定对应消息队列
//参数1:队列名称,如果队列不存在自动创建
//参数2:用来定义队列特性是否要持久化,true持久化,false不持久化
//参数3:是否独占队列,true独占,false不独占
//参数4:是否在消费完成后自动删除队列,true自动删除,false不自动删除
//参数5:额外附加参数
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));
}
});
//消费端一直监听,不做关闭处理
}
}
消费完之后再来看已经没了:
4.代码优化:连接工具类封装
新建一个工具类:RabbitMqUtils
RabbitMqUtils:
package helloworld.utils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class RabbitMqUtils {
private static ConnectionFactory connectionFactory;
/**
* 类加载执行,只执行一次
*/
static {
//创建连接mq的连接工厂对象
connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("192.168.210.132");
//设置端口号
connectionFactory.setPort(5672);
//设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
//设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
}
/**
* 定义提供连接对象的方法
*
* @return
*/
public static Connection getConnection() {
try {
return connectionFactory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 关闭通道和关闭连接
*/
public static void closeConnectionAndChannel(Channel channel, Connection connection) {
try {
if (channel != null) {
channel.close();
}
if (connection != null) {
connection.close();
}
} catch (Exception e) {
}
}
}
工具类的使用:
5.API参数细节
说在前面:生产者和消费者的特性设置要严格保持一致!
(1)queueDeclared方法
*1)第一个参数:队列名称,如果队列不存在自动创建
通道修改绑定的消息队列之后,还要再修改发布消息里的队列名,才可以在新队列中发布。
*2)第二个参数:用来定义队列特性是否要持久化,true持久化,false不持久化
如果是false,重启rabbitmq的话,这个队列就没了。如果不持久化,队列里还没消费的消息也就消失了。
注:就算设置了持久化,重启之后虽然队列还在,但是里面的消息还是没了,我们需要额外设置如下参数
MessageProperties.PERSISTENT_TEXT_PLAIN
*3)第三个参数:是否独占队列,true独占,false不独占。一般设置成false
*4)第四个参数:是否在消费完成后自动删除队列,true自动删除,false不自动删除
当然消费者也要变成true,当消费完后关闭消费者,这个队列就会自动删除。
四、RabbitMQ之Work Queues模型(工作队列)
1.工作队列任务模型
Work Queues也叫任务模型。(工作中用的比较多)
(1)使用场景
当消息处理比较耗时时,可能生产消息的速度远远大于消息的消费速度,长此以往消息越堆积越多,无法及时处理。这个时候用任务模型。
让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费就会消失,因此任务不会被重复执行。
(2)模型解释
P:生产者。任务的发布者
C1:消费者1,领取任务并且完成任务。假设完成速度比较慢
C2:消费者2,领取任务并且完成任务。假设完成速度比较快
2.开发生产者
(1)先来重启一下rabbitmq
然后登陆 http://192.168.210.132:15672/#/
通过点击队列名进去删除掉之前的队列:
(2)开发生产者
Provider:
package workqueue;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import helloworld.utils.RabbitMqUtils;
import java.io.IOException;
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 queue").getBytes());
}
//关闭资源
RabbitMqUtils.closeConnectionAndChannel(channel,connection);
}
}
然后我们可以看到十个消息被生产了:
测试成功后就可以吧这个队列删除了
(3)开发消费者
Customer1和Customer2基本一致,除了sout的文字不同
package workqueue;
import com.rabbitmq.client.*;
import helloworld.utils.RabbitMqUtils;
import java.io.IOException;
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));
}
});
}
}
开发好后,我们先启动消费者,再启动生产者,可以看到是轮询消费:
(4)能者多劳策略+消息永不丢失
上述模式有两个问题:
- 轮询消费,如果两个消费者速度不一样,慢的消费者会积压很多任务,快的消费者空转
- 10个任务一次性给到两个消费者,各5个任务。如果第一个消费者消费3个宕机了,后面2个任务就丢失了。
改造消费者Customer1和Customer2:
package workqueue;
import com.rabbitmq.client.*;
import helloworld.utils.RabbitMqUtils;
import java.io.IOException;
public class Customer1 {
public static void main(String[] args) throws IOException {
//通过工具类获取连接对象
Connection connection = RabbitMqUtils.getConnection();
//获取连接中的通道
final Channel channel = connection.createChannel();
//每次只能消费一个消息,避免一次性拿到5个,中途宕机导致后续消息丢失问题
channel.basicQos(1);
//通道绑定对应消息队列
channel.queueDeclare("work", true, false, false, null);
//消费消息
//参数1:队列名称
//参数2:消息自动确认,我们取消自动确认,进行手动确认
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("消费者-1:"+new String(body));
//参数1:确认队列中哪个具体消息
//参数2:是否开启多个消息同时确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
测试结果:
五、RabbitMQ之Fanout模型(广播)
1.广播模型的流程
- 可以有多个消费者
- 每个消费者有自己的queue
- 每个队列都要绑定到交换机
- 生产者发送的消息,只能发送到交换机。交换机决定要发给哪个队列,生产者无法决定
- 交换机把消息发送给绑定过的所有队列
- 队列的消费者都能拿到消息,实现一条消息被多个消费者消费
2.开发生产者
Provider:
package fanout;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import helloworld.utils.RabbitMqUtils;
import java.io.IOException;
public class Provider {
public static void main(String[] args) throws IOException {
//通过工具类获取连接对象
Connection connection = RabbitMqUtils.getConnection();
//获取连接中的通道
Channel channel = connection.createChannel();
//将通道声明给指定交换机
//参数1:交换机名称
//参数2:交换机类型,fanout:广播类型
channel.exchangeDeclare("logs","fanout");
//发送消息
//参数1:交换机名称
//参数2:队列名称
//参数3:传递消息额外参数
//参数4:消息的具体内容
channel.basicPublish("logs","",null,"fanout type message".getBytes());
//关闭资源
RabbitMqUtils.closeConnectionAndChannel(channel,connection);
}
}
3.开发消费者
Customer1,Customer2,Customer3:
package fanout;
import com.rabbitmq.client.*;
import helloworld.utils.RabbitMqUtils;
import java.io.IOException;
public class Customer1 {
public static void main(String[] args) throws IOException {
//通过工具类获取连接对象
Connection connection = RabbitMqUtils.getConnection();
//获取连接中的通道
final Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs","fanout");
//临时队列
String quueueName = channel.queueDeclare().getQueue();
//绑定交换机和队列
channel.queueBind(quueueName,"logs","");
//消费消息
channel.basicConsume(quueueName, 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));
}
});
}
}
测试发现,三个消费者都能收到广播的消息:
六、RabbitMQ之路由(Routing)
Fanout广播下,一条消息会被所有消费者消费。但是有时候我们希望不同消息被不同消息队列消费,这个时候可以用Direct模型(路由直连)。
1.Direct模型(路由直连)
(1)模型图
- P:生产者,向Exchange发送消息,发送消息时会指定一个RoutingKey
- X:Exchange(交换机),接受生产者的消息,然后把消息递交给RoutingKey完全匹配的队列
- C1:消费者1,其所在队列指定了需要RoutingKey为error的消息
- C2:消费者2,其所在队列指定了需要RoutingKey为info、error、warning的消息
(2)Direct模式的流程
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
- 消息的发送方在向Exchange发送消息时,也必须指定消息的RoutingKey
- Exchange不再把消息交给每一个指定的队列,而是根据消息的RoutingKey进行判断,只有队列的RoutingKey与消息的RoutingKey完全一致,才会接受消息
(3)开发生产者
Provider:
package direct;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import helloworld.utils.RabbitMqUtils;
import java.io.IOException;
/**
* [一句话描述该类的功能]
*
* @author : [xupeng]
* @version : [v1.0]
* @createTime : [2021/7/7 11:45]
*/
public class Provider {
public static void main(String[] args) throws IOException {
//通过工具类获取连接对象
Connection connection = RabbitMqUtils.getConnection();
//获取连接中的通道
final Channel channel = connection.createChannel();
String exchangeName = "logs_direct";
//通过通道声明交换机
//参数1:交换机名称
//参数2:路由直连模式
channel.exchangeDeclare(exchangeName, "direct");
//发送消息
String routingKey = "error";
channel.basicPublish(exchangeName,routingKey,null,("direct模式发布的基于routingKey:"+routingKey+"发送的消息").getBytes());
//关闭资源
RabbitMqUtils.closeConnectionAndChannel(channel,connection);
}
}
(4)开发消费者
Consumer1、Consumer2:
package direct;
import com.rabbitmq.client.*;
import helloworld.utils.RabbitMqUtils;
import java.io.IOException;
/**
* [一句话描述该类的功能]
*
* @author : [xupeng]
* @version : [v1.0]
* @createTime : [2021/7/7 11:52]
*/
public class Consumer1 {
public static void main(String[] args) throws IOException, IOException {
//通过工具类获取连接对象
Connection connection = RabbitMqUtils.getConnection();
//获取连接中的通道
final Channel channel = connection.createChannel();
String exchangeName = "logs_direct";
//通道声明交换机以及交换的类型
channel.exchangeDeclare(exchangeName, "direct");
//创建一个临时队列
String queue = channel.queueDeclare().getQueue();
//基于routingKey绑定队列和交换机
//参数1:队列名
//参数2:交换机名
//参数3:routingKey
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("消费者1:"+new String(body));
}
});
}
}
package direct;
import com.rabbitmq.client.*;
import helloworld.utils.RabbitMqUtils;
import java.io.IOException;
/**
* [一句话描述该类的功能]
*
* @author : [xupeng]
* @version : [v1.0]
* @createTime : [2021/7/7 11:52]
*/
public class Consumer2 {
public static void main(String[] args) throws IOException, IOException {
//通过工具类获取连接对象
Connection connection = RabbitMqUtils.getConnection();
//获取连接中的通道
final Channel channel = connection.createChannel();
String exchangeName = "logs_direct";
//通道声明交换机以及交换的类型
channel.exchangeDeclare(exchangeName, "direct");
//创建一个临时队列
String queue = channel.queueDeclare().getQueue();
//基于routingKey绑定队列和交换机
//参数1:队列名
//参数2:交换机名
//参数3:routingKey
channel.queueBind(queue, exchangeName, "error");
channel.queueBind(queue, exchangeName, "info");
channel.queueBind(queue, exchangeName, "warning");
//获取消费的消息
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));
}
});
}
}
测试:
2.Topic订阅模型(路由通配符)
Topic类型的Exchange与Direct相比,都是可以根据routingKey把消息路由到不同的队列,只不过Topic类型Exchange可以让队列在绑定routingKey的时候使用通配符。
这种模型routingKey一般是由一个或多个单词组成,多个单词之间使用"."分隔。
(1)模型图
我们可看到通配符有"*"和"#"这两种:
- *:匹配不多不少,恰好一个词
- #:匹配一个或多个
(2)开发生产者
Provider:
package topic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import helloworld.utils.RabbitMqUtils;
import java.io.IOException;
/**
* [一句话描述该类的功能]
*
* @author : [xupeng]
* @version : [v1.0]
* @createTime : [2021/7/7 13:52]
*/
public class Provider {
public static void main(String[] args) throws IOException {
//通过工具类获取连接对象
Connection connection = RabbitMqUtils.getConnection();
//获取连接中的通道
final Channel channel = connection.createChannel();
String exchangeName = "topics";
//通过通道声明交换机
//参数1:交换机名称
//参数2:路由直连模式
channel.exchangeDeclare(exchangeName, "topic");
//发送消息
String routingKey = "user.save.list";
channel.basicPublish(exchangeName,routingKey,null,("topic模式发布的基于routingKey:"+routingKey+"发送的消息").getBytes());
//关闭资源
RabbitMqUtils.closeConnectionAndChannel(channel,connection);
}
}
(3)开发消费者
Consumer1,Consumer2:
package topic;
import com.rabbitmq.client.*;
import helloworld.utils.RabbitMqUtils;
import java.io.IOException;
/**
* [一句话描述该类的功能]
*
* @author : [xupeng]
* @version : [v1.0]
* @createTime : [2021/7/7 11:52]
*/
public class Consumer1 {
public static void main(String[] args) throws IOException, IOException {
//通过工具类获取连接对象
Connection connection = RabbitMqUtils.getConnection();
//获取连接中的通道
final Channel channel = connection.createChannel();
String exchangeName = "topics";
//通道声明交换机以及交换的类型
channel.exchangeDeclare(exchangeName, "topic");
//创建一个临时队列
String queue = channel.queueDeclare().getQueue();
//基于routingKey绑定队列和交换机
//参数1:队列名
//参数2:交换机名
//参数3:routingKey
channel.queueBind(queue, exchangeName, "user.*");
//获取消费的消息
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));
}
});
}
}
package topic;
import com.rabbitmq.client.*;
import helloworld.utils.RabbitMqUtils;
import java.io.IOException;
/**
* [一句话描述该类的功能]
*
* @author : [xupeng]
* @version : [v1.0]
* @createTime : [2021/7/7 11:52]
*/
public class Consumer2 {
public static void main(String[] args) throws IOException, IOException {
//通过工具类获取连接对象
Connection connection = RabbitMqUtils.getConnection();
//获取连接中的通道
final Channel channel = connection.createChannel();
String exchangeName = "topics";
//通道声明交换机以及交换的类型
channel.exchangeDeclare(exchangeName, "topic");
//创建一个临时队列
String queue = channel.queueDeclare().getQueue();
//基于routingKey绑定队列和交换机
//参数1:队列名
//参数2:交换机名
//参数3:routingKey
channel.queueBind(queue, exchangeName, "user.#");
//获取消费的消息
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));
}
});
}
}
测试:
七、打赏请求
如果本篇博客对您有所帮助,打赏一点呗,谢谢了呢~