注:本篇文章整理了部分网上优秀的rabbitmq博客的部分知识点,在此不一一列出,主要为整理b站编程不良人老师的教学过程,(很好的老师,对新手很友好,给大家安利一波)
RabbitMQ
详细教程带你学习RabbitMQ,我们学习每一项技术要带着问题来学,我们为什么要学习RabbitMQ,RabbitMQ是什么,他的应用场景有哪些,我们在哪些业务会用到消息中间件;如何部署RabbitMQ ?以及最后我们如何使用代码操作RabbitMQ;
RabbtiMQ是什么?
- RabbitMQ是基于AMQP协议的一个消息中间件,它在基础的消息队列 (生产者发送消息至消息中间件,消息中间件将消息发送至消费者) 基础上引入交换机的概念,生产者可选择将消息发送至交换机,由交换机按照指定的路由规则发送至消费者。
消息中间件(RabbitMQ)有哪些使用场景?
- 异步处理
- 系统解耦
- 流量削峰
- 消息通讯
什么是AMQP
AMQP(高级消息队列协议)是一种应用层传输的协议标准,他针对消息中间件而生,基于此协议的客户端与消息中间可支持不同产品,不同语言之间的通信。
AMQP包含一些重要的核心成员:
- ConnectionFacotry 连接工厂 负责创建连接
- Connection 连接 负责与消息中间件进行网络连接
- channel 信道 是建立在Connection之上的轻量级连接,对消息队列的读写操作都是在此进行。
- broker 中间件 存储消息的中间件。rabbitmq就是。
- virtualHost 虚拟机 负责提供隔离的生产环境,多个虚拟主机之间没有关联。
- exchange 交换机 根据设置的路由规则将接收到的消息发送至消费者
rabbitmq常见使用方式
rabbitmq包含几种常用的路由方式,根据不同使用场景来决定使用哪种连接方式
(代码部分会逐个进行编写)
- 直连 生产者生产消息直接发送至消息队列,消费者直接从消息队列获取进行消费
- work queue 工作队列,此场景下,生产者生产消息发送至消息队列,多个消费者根据负载均衡规则进行获取消息(默认轮询),消息只可被消费一次,(类似做核酸,我=消息,工作人员=消费者,我只可以在一个工作人员处做核酸)
- fanout 广播,与work queue一样一个生产者对应多个消费者,但区别是fanout是将一个消息发送至全部消费者,实现广播功能(所有人都能听到)。需要注意的是从fanout开始到以下的使用方式,rabbitmq并不是直接将消息发送至消息队列,而是先发送至交换机,由交换机进行路由规则匹配
- direct 路由,direct也可以是一对多的关系,但发送与不发送取决于生产者制定的路由与消费者是否匹配上,(例如打个比方,生产者发送一条消息,并附加了一个参数 1,也就意味着消费者接收消息制定的参数也是1,如果是0或者是2都不会接收此条消息)
- topic 路由(升级版),他的原理与direct一致,但是direct的路由方式相对死板不够灵活,topic引入通配符来进行匹配路由,可以使得通信更加灵活。
Java代码操作rabbitmq (包含几种工作场景)
注:docker部署rabbit比较繁琐 放在文章末尾,需要的同学先去docker部署。
新建Maven项目 导入 rabbitmq依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.2</version>
</dependency>
/**
* 创建 mq 连接工厂
* @return
*/
public static Connection getConnection(){
// 创建连接mq的连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq 的主机 端口 虚拟机名
connectionFactory.setHost("mq所在的服务主机");
//rabbitmq 通讯端口
connectionFactory.setPort(5672);
//虚拟机名
connectionFactory.setVirtualHost("/test-v");
//设置虚拟主机对应的用户名 密码
connectionFactory.setUsername("test-user");
connectionFactory.setPassword("123");
Connection connection = null ;
try {
connection = connectionFactory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
工作场景一:直连
该模式生产者直接发送消息至消费者
//先启动消费者 实时监听,然后生产者发送消息 ,可以看到消费者是可以立即获取到消息的。
//生产者代码
public static void main(String[] args) throws Exception {
// 获取mq 通讯连接
Connection connection = getConnection();
//获取通道
Channel channel = connection.createChannel();
//通道绑定一个消息队列
// 参数说明: 队列名称(不存在则创建) 是否持久化(true重启服务后仍然存在) 是否独占序列(true 其它连接不允许访问)
// 消息被消费完是否删除(true) 其它(参数) 暂时用不到
channel.queueDeclare("hello",true,false,false,null);
//发布消息
// 参数说明: 交换机名称 队列名称 传递消息额外设置 参数具体内容
// 注:第一个参数交换机为 "" 此模型为直连模型 直接由生产者发送至消费者,不存在交换机。
channel.basicPublish("","hello",null,"hello world".getBytes(StandardCharsets.UTF_8));
channel.close();
connection.close();
}
//消费者代码
public static void main(String[] args) throws Exception {
// rabbit获取连接
Connection connection = RabbitDemo.getConnection();
// 通道
Channel channel = connection.createChannel();
//绑定队列
//参数说明:1 队列名称(与生产者对应) 2 是否持久化(与生产者对应) 3 是否独占序列(必须与生产者对应)
// 4 消息被消费完是否删除(与生产者对应) 5 其它(参数)暂时用不到
channel.queueDeclare("hello",true,false,false,null);
//持续监听绑定队列 然后消费消息
// 参数说明:1 队列名称 2 开启消息自动确认机制 false的话消费完该消息还会存储在队列中 不消失
channel.basicConsume("hello",true,new DefaultConsumer(channel){
@Override // 参数说明 其它都暂时用不到 最后一个是收到生产者发送消息的byte流
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body is "+ new String(body));
}
});
}
工厂场景二:workqueue
该工作场景 是一个生产者对应多个消费者实现的,多消费者同时监听生产者,但是消息只可以被一个消费者消费
//生产者 此处生产者与第一种模式一致 循环发送10次数据
public static void main(String[] args) {
Connection connection = getConnection();
try {
Channel channel = connection.createChannel();
//通道绑定一个消息队列
channel.queueDeclare("hello",true,false,false,null);
//发布消息
for (int i = 0; i < 10; i++) {
channel.basicPublish("","hello",null,("hello world"+i).getBytes(StandardCharsets.UTF_8));
}
channel.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//创建两个消费者
public static void main(String[] args) {
Connection connection = getConnection();
Channel channel = null;
try {
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));
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
//第二个消费者
public static void main(String[] args) {
Connection connection = getConnection();
Channel channel = null;
try {
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));
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
此时进行验证 将两个消费者先进行启动监听 然后启动生产者发送数据 查看消费者消费情况
我这里很明显可以看到两个消费者交替消费消息,但此时有个疑问 假如我的第一个消费者处理的业务要比第二个消费者慢很多吗 还会是交替消费吗
// 第一个消费者睡眠2s 重启两个消费者 查看消费状况
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("我是第一个消费者:"+new String(body));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
最终的消费结果仍然是一人五个 ,但是可以清晰看到第二个消费者早就消费完五个了,第一个消费者还在一个一个消费,也就是说此时的消费策略是提前将任务平均分配到所有监听的消费者。
那么代码是如何设置的呢 下面我们来看一张图片
basicConsume函数的第二个参数 这里在第一个工作场景进行了参数说明 第二个参数是是否开启消息自动确认,这个自动确认机制也就是说,当代码执行到这句,也就是监听到消息就立刻确认,告诉mq我已经确认拿到了,这种情况mq并不会管消费者的执行逻辑,也就是你执行完成与否与我无瓜,我继续为所有监听中的消费者轮询发布消息。
这种方式可能会导致一些问题,例如消费者二拿到了五个消息,执行到了第三个就挂掉了,此时后两个消息就会丢失。那么肯定有另一种方案,也就是当消费者执行完毕才对消息进行确认收到,那么此时消费情况是如何呢,我们来看代码。
// 消费者一 修改部分一一注释
public static void main(String[] args) {
Connection connection = getConnection();
try {
Channel channel = connection.createChannel();
channel.queueDeclare("hello",true,false,false,null);
// 此处设置每次限制消费一
channel.basicQos(1);
// 第二个参数修改为false取消自动确认
channel.basicConsume("hello",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));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//此处设置手动确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
//消费者二 与一类似
public static void main(String[] args) {
Connection connection = getConnection();
try {
Channel channel = connection.createChannel();
channel.queueDeclare("hello",true,false,false,null);
channel.basicQos(1);
channel.basicConsume("hello",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);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
我们可看到消费者二消费了九条,消费者一消费了一条,此时查看mq发现队列内消息为0,说明消费成功
广播工作模式(fanout)
广播 顾名思义,一个生产者对应多个消费者,一条消息会被所有监听该生产者的消费者监听到,直接上代码
// 生产者 代码
public static void main(String[] args) throws IOException {
Connection connection = getConnection();
Channel channel = connection.createChannel();
//此处通道调用队列方法改为调用交换机
// 参数 交换机名 交换机类型
channel.exchangeDeclare("test-exchange","fanout");
for (int i = 0; i < 10; i++) {
channel.basicPublish("test-exchange","",null,("hello world"+i).getBytes(StandardCharsets.UTF_8));
}
}
、
// 消费者代码
public static void main(String[] args) throws IOException {
Connection connection = getConnection();
Channel channel = connection.createChannel();
// 通道绑定交换机, 名字与类型对应上
channel.exchangeDeclare("test-exchange","fanout");
//获取临时队列
String queue = channel.queueDeclare().getQueue();
//参数说明 临时队列名 交换机名, 路由参数(此时用不到,后面会讲)
channel.queueBind(queue,"test-exchange","");
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("消费者一:"+new String(body));
}
});
}
public static void main(String[] args) throws IOException {
Connection connection = getConnection();
Channel channel = connection.createChannel();
// 通道绑定交换机, 名字与类型对应上
channel.exchangeDeclare("test-exchange","fanout");
//获取临时队列
String queue = channel.queueDeclare().getQueue();
//参数说明 临时队列名 交换机名, 路由参数(此时用不到,后面会讲)
channel.queueBind(queue,"test-exchange","");
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("消费者二:"+new String(body));
}
});
}
实现广播功能,此处测试两个消费者,多个也可以实现。
Direct Topic
此处两个工作模式一起讲解,因为原理都是一致的
basicPublish 第二个参数 路由key 生产者设置的key需要与所有广播的消费者对应上,才会发送消息,否则不发送
// 生产者
public static void main(String[] args) throws Exception {
Connection connection = getConnection();
Channel channel = connection.createChannel();
//此处交换机类型 设置direct
channel.exchangeDeclare("test-direct","direct");
for (int i = 0; i < 10; i++) {
// 第二个参数 路由key
channel.basicPublish("test-direct","key",null,("hello world"+i).getBytes(StandardCharsets.UTF_8));
}
channel.close();
connection.close();
}
//消费者一
public static void main(String[] args) throws IOException {
Connection connection = getConnection();
Channel channel = connection.createChannel();
// 通道绑定交换机
channel.exchangeDeclare("test-direct","direct");
//获取临时队列
String queue = channel.queueDeclare().getQueue();
//参数说明 临时队列名 交换机名, 路由key 与生产者对应则可以接收数据
channel.queueBind(queue,"test-direct","key");
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("消费者一:"+new String(body));
}
});
}
//消费者二
public static void main(String[] args) throws IOException {
Connection connection = getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("test-direct","direct");
//获取临时队列
String queue = channel.queueDeclare().getQueue();
// 此处设置key1 与key不对应 则此消费者接收不到消息
channel.queueBind(queue,"test-direct","key1");
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("消费者二:"+new String(body));
}
});
}
注:此处 可以设置多个 根据场景随意设置
channel.queueBind(queue,"test-direct","key1");
channel.queueBind(queue,"test-direct","key2");
channel.queueBind(queue,"test-direct","key3");
topic
// topic 与fanout一致 就是routerkey 由具体的 变成抽象的 我们看代码
//生产者
public static void main(String[] args) throws Exception {
Connection connection = getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("test-topic","topic");
//此处路由key设置 test.a 那么 消费者的key需要设置test.a 或者 test.*
// 消费者设置test.* 则可以接收所有的test.加随意字符
String routerKey = "test.a";
for (int i = 0; i < 10; i++) {
channel.basicPublish("test-direct",routerKey,null,("hello world"+i).getBytes(StandardCharsets.UTF_8));
}
channel.close();
connection.close();
}
//消费者代码
public static void main(String[] args) throws IOException {
Connection connection = getConnection();
Channel channel = connection.createChannel();
// 通道绑定交换机, 名字与类型对应上
channel.exchangeDeclare("test-topic","topic");
//获取临时队列
String queue = channel.queueDeclare().getQueue();
// 第三个参数 路由key 需要与生产者的通配符匹配
// 具体通配符大家可以网上搜索,此处不作详细解答
// 使用通配符就是为了使得多个交换机多个消费匹配起来更加的//方便灵活
channel.queueBind(queue,"test-topic","test.*");
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("消费者一:"+new String(body));
}
});
}
Linux docker单机部署rabbitmq
准备环境:centos7
docker (没安装同学去搜一下,命令安装很简单)
1、镜像拉取 docker pull rabbitmq:management (一定是management 这个版本类型的,这个是带可视化工具的,可以通过web管理操作)
2、创建容器
// 用户名 密码
docker run -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=123456
// 容器名 镜像名字 : 镜像版本
--name test-rabbit -p 15672:15672 -p 5672:5672 -d rabbitmq:management
3 、docker ps 查看容器是否运行 运行的话 地址栏输入 ip:15672 则会打开rabbitmq 管理页面
4、创建用户与虚拟主机
输入刚才创建容器时的name 密码
进入管理页面 我们创建一个用户
点击 admin -> users -> add a user -> add user
填写用户名 密码 tags是该用户对应权限 administrator 为超级管理员权限
我们可以看到创建好的用户第三列 can access virtual host 是没有值的 也就是说新创建的用户不可以操作任何虚拟主机,我们接下来创建一个虚拟主机 来绑定该用户,虚拟主机之间是相互隔离的生产开发环境,互不影响
需要创建一个虚拟机 然后分配给用户使用 , 点击虚拟机 填写一个虚拟机名即可,
创建完成 点击用户名字,进入配置页面 ,将刚才的虚拟机分配给该用户。
点击 选择 虚拟机 然后确认 ,即可看到 记号3 处出现。
备注
大家主要了解工作方法工作场景,有合适的业务可以使用上即可,不用死扣代码,有疑问的话 可以私我, 后面会写一篇文章,springboot操作rabbit