RabbitMQ
文章目录
1.安装
下载linux下的erlang压缩包,rabbitMQ-server压缩包
因为RabbitMQ底层是用Erlang语言来写的,所以安装RabbitMQ之前要先安装Erlang环境
1.安装Erlang环境
#rpm -ivh erlang-23.2.3-1.el7.x86_64.rpm
2.用yum 安装的方式安装socat 插件
# yum install -y socat
3.安装RabbitMQ
#rpm -ivh rabbitmq-server-3.8.11-1.el7.noarch.rpm
4.检查rabbitmq是否启动
#ps -ef|grep rabbit
如图表示没有启动
5.启动RabbitMQ
#rabbitmq-server -detached
6.启动rabbit之后查看后台服务
7.关闭RabbitMQ
#rabbitmqctl stop
2.启动与停止
直接启动
#rabbitmq-server -detached
直接关闭
#rabbitmqctl stop
启动服务
#systemctl start rabbitmq-server
查看rabbit服务状态
#systemctl status rabbitmq-server
设置开启启动rabbitmq
#systemctl enable rabbitmq-server
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IRJi69Oo-1657363169530)(https://raw.githubusercontent.com/yuan-boss/blog_file/master/images/202207091837683.png)]
关闭开机自启动
#systemctl stop rabbitmq-server
3.rabbitmq默认插件
#rabbitmq-plugins list
1.启动控制台插件
#rabbitmq-plugins enable rabbitmq_management
2.启动插件之后在宿主机访问控制台
服务器ip+端口(15672)
默认 rabbitmq 给我们提供了一个guest的账户 ,密码也是guest
但是现在 rabbitmq 主机没有在windows上 所以我们必须使用ip访问,不能用localhost,所以我们需要使用命令再创建一个rabbitmq的管理员用户:
#rabbitmqctl add_user root root
管理员用户创建好之后,再给用户管理员的角色:
#rabbitmqctl set_user_tags root administrator
再使用root用户就可以登录控制台了
登录控制台之后,别忘记给用户设置权限
2.Docker安装RabbitMQ
$ docker run -d --hostname my-rabbit --name some-rabbit -e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=root -p 15672:15672 -p 5672:5672 -p 25672:25672 -p 61613:61613 -p 1883:1883 rabbitmq:3-management
3.生产者
1.创建连接工厂,给连接工厂配置账号密码等信息
2.通过连接工厂创建了连接
3.通过连接创建一个通道
4. 为通道声明队列(队列名字,对列是否持久化,是否具有排他性,最后一个消费者消费完之后是否自动删除队列,该队列携带什么参数)
5.准备消息内容
6.发送消息给队列
7.关闭通道
8.关闭连接
声明队列的核心代码解释
/**
* @Params1 队列的名称
* @Params2 是否要持久化durable,默认是不持久化,为false,所谓持久化就是重启服务之后是否会保留队列,非持久化队列会存盘,但是随着服务器重启 会丢失
* @Params3 排他性,是否独占独立
* @Params4 是否自动删除,随着最后一个消费者消费完消息之后是否把队列删除
* @Params5 携带一些附加参数
*/
channel.queueDeclare(queueName,false,false,false,null);
4.面试题
1.为什么RabbitMQ是基于通道(channal)处理,而不是连接(connection)?
connection是一个短连接,短连接会经过三次握手四次挥手,这个过程很慢,耗费资源,耗时长,连接开关会造成很大的性能开销
所以connection在 TCP/IP 的基础之上开发一个长连接的信道,即为channal,channal是网络信道,几乎所有的操作都在chanal中进行,一个connection可以开启多个channal,从而大大提高了性能
2.Rabbitmq为什么需要信道,为什么不是TCP直接通信
1、TCP的创建和销毁,开销大,创建要三次握手,销毁要4次分手。
2、如果不用信道,那应用程序就会TCP连接到Rabbit服务器,高峰时每秒成千上万连接就会造成资源的巨大浪费,而且==底层操作系统每秒处理tcp连接数也是有限制的,==必定造成性能瓶颈。
3、信道的原理是一条线程一条信道,多条线程多条信道同用一条TCP连接,一条TCP连接可以容纳无限的信道,即使每秒成千上万的请求也不会成为性能瓶颈。
3.queue队列到底在消费者创建还是生产者创建?
1:一般建议是在rabbitmq操作面板创建。这是一种稳妥的做法。
2∶按照常理来说,确实应该消费者这边创建是最好,消息的消费是在这边。这样你承受一个后果,可能我生产在生产消息可能会丢失消息。
3:在生产者创建队列也是可以,这样稳妥的方法,消息是不会出现丢失。
4:如果你生产者和消费都创建的队列,谁先启动谁先创建,后面启动就覆盖前面的
4.可以存在没有交换机的队列吗?
不可能存在,虽然没有指定交换机,但是一定会存在一个默认的交换机,因为没有交换机队列就不会存在消息
交换机负责消息的接受,队列不会接收消息,是交换机投递消息给队列,而不是队列去接收消息
//6.发送消息给队列
/**
* @Params1 交换机(最好指定一个叫交换机名字,不然就会使用默认的交换机)
* @Params2 队列,路由key
* @Params3 消息的状态控制
* @Params4 消息主题
*/
channel.basicPublish("",queueName, null,message.getBytes());
5.模式
1.简单模式(default)
通过默认交换机投递消息给指定队列
2.发布订阅模式(fanout)
通过交换机与队列进行绑定,交换机会将消息投递到所有与之绑定的队列
类似于一些粉丝关注一个博主之后,这个博主发布视频,所有订阅这个博主的粉丝都会看到这个视频
3.路由模式(direct)
这个模式是发布订阅模式的延伸,本质还是将交换机与队列进行绑定,但是绑定的同时还要加上 Routing Key ,当交换机投递消息时,带上某个路由key,这条消息就会投递到有这个路由key的队列
可以理解为交换机投递信息给队列的时候要根据某个条件投递,交换机只会投递给满足条件的队列
场景:
当一个用户注册某个平台的账号,会把注册通知发送到邮箱,短信与微信,会对用户造成很大的不便,如果指定某个路由key,在发送注册通知的时候在路由key中带上emai,那么这个通知就只会发到带有路由key “email”的队列中
4.主题模式(tocpic)
这个模式是路由模式的延伸
就是在路由模式的基础上,添加模糊条件绑定功能,其中有#,*两种符号来控制条件
符号#:表示0个或一个或多个
符号*:表示一定要有一个并且只有一个
示例:
#.com.#: 满足这个条件的路由key是(com 或者 xxx.com.xxx或者xxx.com.xxx.xxx等等)
*.com.*:满足这个条件的路由key是(xxx.com.xxx)
#.com.*:满足这个条件的路由key是(xxx.xxx.com.x或者com.xxx)
5.headers模式
这个模式也是通过条件来进行发布消息,这个条件是就是参数
交换机先通过参数绑定队列,比如X=1绑定到queue1
在投递消息的时候携带x=1的参数即可将消息投递到queue1
6.Work模式
-
轮询分发
轮询分发就是指按均分配,消息分发默认就是轮询分发 轮询分发不依赖于消费者的处理速度,即使两个消费者处理速度相差很大,他们处理的消息数量也是一样的 轮询分发下,消费者消费的时候既可以设置为手动,应可以设置为自动应答
-
公平分发
公平分发就是按劳分配 消息分发取决于消费者的处理速度,如果消费者处理快,那这个消费者就能处理更多的消息,反之处理更少的消息 公平分发下,消费者消费的时候必须设置为手动应答,如果autoAck参数设置为true,那就会变成轮询分发 //设置为手动应答 finalChannel.basicAck(message.getEnvelope().getDeliveryTag(),false); 另外在消费者消费之前,我们还需要设置一个qos指标 //定义指标,qos=1 finalChannel.basicQos(1); 里面的参数过大的话消费者的消费就容易变成平均消费 比如10条消息,将指标设置为10的话,两个消费者都是各消费5条消息
思考:
1.假设选择 fanout 模式的交换机,发布消息的时候带上路由key,是否只会向带有该路由key的队列发送信息呢?
不会,fanout模式下不会根据路由key投递,就算带上路由key,也没有意义,fanout模式下路由key无效
2.假设选择headers模式的交换机,发布消息的时候带上路由key,是否会向带有该路由key的队列发送信息呢?
不会,即使带上路由key,也不会投递信息到有该路由key的队列,header模式下只有参数条件有效
3.假设生产者发送消息给一个不存在的交换机,能成功吗?
不能成功,会报错(......reply-code=404, reply-text=NOT_FOUND - no exchange ...... )
4.假设消费者消费一个不存在的队列,能成功吗?
不能成功,会报错(...... reply-code=404, reply-text=NOT_FOUND - no queue......)
5.basicPublish()方法的第二个参数到底是路由key 还是 队列名?
如一下代码:
/**
* @Params1 交换机(最好指定一个叫交换机名字,不然就会使用默认的交换机)
* @Params2 队列,路由key
* @Params3 消息的状态控制
* @Params4 消息主题
*/
for (int i = 0; i < 10; i++) {
String message = ""+(i+1);
channel.basicPublish("",queueName, null,message.getBytes());
}
解答:
如果第一个参数交换机指定了,那第二个参数就是指路由key
如果第一个参数交换机未指定,默认为默认交换机,那么第二个参数表示队列名
6.SpringBoot整合RabbitMQ(direct模式)
整合流程
- 1.在application.yml文件中配置用户名与密码,虚拟主机地址,切记服务器需要开放5672端口
- 2.使用配置类声明交换机,队列并且绑定交换机与队列
- 3.生产者发送消息
- 4.消费者通过消费方法上的
@RabbitListener(queues = {"duanxin.direct.queue"})
注解时刻监听队列 - 5.先启动消费者服务等待接收信息(注意流程2一般是在消费者模块中配置)
- 6.启动生产者服务
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
生产者
1.application.yml配置
server:
port: 8080
spring:
rabbitmq:
username: root
password: root
host: 175.178.151.38
port: 5672
virtual-host: /
2.配置类(声明交换机,队列,绑定交换机与队列)
package com.yuan.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @module:
* @description:
* @author: yuan_boss
* @create: 2022-07-08 16:13
**/
@Configuration
public class DirectRabbitMqConfiguration {
//1.声明注册direct模式的交换机
@Bean
public DirectExchange directExchange(){
return new DirectExchange("direct_order_exchange",true,false);
}
//2.声明队列: sms.direct.queue,email.direct.queue,duanxin.direct.queue
@Bean
public Queue directSmsQueue(){
return new Queue("sms.direct.queue",false);
}
@Bean
public Queue directEmailQueue(){
return new Queue("email.direct.queue",false);
}
@Bean
public Queue directDuanxinQueue(){
return new Queue("duanxin.direct.queue",false);
}
//3.完成绑定关系(队列和交换机完成绑定关系)
@Bean
public Binding directSmsBinding(){
return BindingBuilder.bind(directSmsQueue()).to(directExchange()).with("sms");
}
@Bean
public Binding directEmailBinding(){
return BindingBuilder.bind(directEmailQueue()).to(directExchange()).with("email");
}
@Bean
public Binding directDuanxinBinding(){
return BindingBuilder.bind(directDuanxinQueue()).to(directExchange()).with("duanxin");
}
}
3.业务类----生成订单消息
package com.yuan.service;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.UUID;
/**
* @module:
* @description:
* @author: yuan_boss
* @create: 2022-07-08 15:56
**/
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 模拟用户下单
*/
public void makerOrder(String userid,String productId,int num){
//1.根据商品id查询库存是否充足
//2.保存订单
String orderId = UUID.randomUUID().toString();
System.out.println("订单生成成功:"+orderId);
//3.通过MQ完成消息的分发
/**
* @Params1 交换机
* @Params2 路由key/队列名(如果没指定交换机,那就是队列名,如果指定了交换机,那就是路由key)
* @Params3 消息内容
*/
String exchangeName = "fanout_order_exchange";
String routingKey = "";
rabbitTemplate.convertAndSend(exchangeName,routingKey,orderId);
}
/**
* 模拟用户下单
*/
public void makerOrderDirect(String userid,String productId,int num){
//1.根据商品id查询库存是否充足
//2.保存订单
String orderId = UUID.randomUUID().toString();
System.out.println("订单生成成功:"+orderId);
//3.通过MQ完成消息的分发
/**
* @Params1 交换机
* @Params2 路由key/队列名(如果没指定交换机,那就是队列名,如果指定了交换机,那就是路由key)
* @Params3 消息内容
*/
String exchangeName = "direct_order_exchange";
rabbitTemplate.convertAndSend(exchangeName,"email",orderId);
rabbitTemplate.convertAndSend(exchangeName,"duanxin",orderId);
}
}
消费者
1.application.yml配置
server:
port: 8081
spring:
rabbitmq:
username: root
password: root
host: 175.178.151.38
port: 5672
virtual-host: /
2.配置类(声明交换机,队列,绑定交换机与队列)
package com.yuan.service.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @module:
* @description:
* @author: yuan_boss
* @create: 2022-07-08 16:13
**/
@Configuration
public class DirectRabbitMqConfiguration {
//1.声明注册direct模式的交换机
@Bean
public DirectExchange directExchange(){
return new DirectExchange("direct_order_exchange",true,false);
}
//2.声明队列: sms.direct.queue,email.direct.queue,duanxin.direct.queue
@Bean
public Queue smsQueue(){
return new Queue("sms.direct.queue",false);
}
@Bean
public Queue emailQueue(){
return new Queue("email.direct.queue",false);
}
@Bean
public Queue duanxinQueue(){
return new Queue("duanxin.direct.queue",false);
}
//3.完成绑定关系(队列和交换机完成绑定关系)
@Bean
public Binding smsBinding(){
return BindingBuilder.bind(smsQueue()).to(directExchange()).with("sms");
}
@Bean
public Binding emailBinding(){
return BindingBuilder.bind(emailQueue()).to(directExchange()).with("email");
}
@Bean
public Binding duanxinBinding(){
return BindingBuilder.bind(duanxinQueue()).to(directExchange()).with("duanxin");
}
}
3.业务类—接收订单信息
注意:消费者类上的@RabbitListener(queues = {"duanxin.direct.queue"})
注解可以取代@RabbitHandler
- DirectDuanxinConsumer
@RabbitListener(queues = {"duanxin.direct.queue"})
@Service
public class DirectDuanxinConsumer {
@RabbitHandler
public void receiveMessage(String message){
System.out.println("duanxin direct ---接收到了订单信息:————>"+message);
}
}
- DirectEmailConsumer
@RabbitListener(queues = {"email.direct.queue"})
@Service
public class DirectEmailConsumer {
@RabbitHandler
public void receiveMessage(String mess age){
System.out.println("email direct ---接收到了订单信息:————>"+message);
}
}
- DirectSMSConsumer
@RabbitListener(queues = {"sms.direct.queue"})
@Service
public class DirectSMSConsumer {
@RabbitHandler
public void receiveMessage(String message){
System.out.println("sms direct ---接收到了订单信息:————>"+message);
}
}
思考
1.声明交换机,队列,绑定交换机与队列的配置类如果只写一个,应该写在哪个模块呢?
其实两个模块都可以写上,不过最合适是写在消费者模块
因为消费者模块是最先启动的服务,要一直监听其绑定的队列,如果监听的队列不存则会报错
7.高级阶段(TTL,死信队列)
TTL,全称为 Time to Live ,即生存时间
TTL队列过期
TTL就是给一个队列设置过期时间,当一个消息在指定时间内没有被消费,将会被队列移除,TTL一般与死信队列搭配使用,移除的消息是放在死信队列中
TTL队列就是在声明队列的时候传一个map产出,该map的key为`x-message-ttl`
@Bean
public Queue ttlDirectQueue(){
Map<String,Object> args = new HashMap<>();
args.put("x-message-ttl",5000);//这里一定是int类型,单位是毫秒
return new Queue("ttl.direct.queue",true,false,false,args);
}
对某条消息设置过期时间
我们不仅可以将整个队列设置TTL,从而给该队列里的所有消息设置TTL
还可以在发送消息的时候单独对某个消息进行设置TTL(其中的代码核心是MessagePostProcessor类,该类可以对你发的消息进行加工,设置过期时间,编码等信息,如下代码
/**
* 模拟用户下单,TTL队列,单纯给某一条消息设置过期时间
*/
public void makerOrderMessageTtl(String userid,String productId,int num){
//1.根据商品id查询库存是否充足
//2.保存订单
String orderId = UUID.randomUUID().toString();
System.out.println("订单生成成功:"+orderId);
//3.通过MQ完成消息的分发
/**
* @Params1 交换机
* @Params2 路由key/队列名(如果没指定交换机,那就是队列名,如果指定了交换机,那就是路由key)
* @Params3 消息内容
*/
String exchangeName = "ttl_direct_exchange";
String routingKey = "ttlmessage";
//给消息设置过期时间
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("5000");
message.getMessageProperties().setContentType("UTF-8");
return message;
}
};
rabbitTemplate.convertAndSend(exchangeName,routingKey,orderId,messagePostProcessor);
}
队列TTL与消息TTL的思考
1.假设对一个队列设置了TTL,往这个队列投递的消息也设置了过期时间,那么这条消息是存留时间是多少?
该消息的存留时间取决于最短的过期时间
假设队列TTL为3秒,消息TTL为5秒,那么消息存留时间为3秒,因为3秒之后,队列会移除里面的所有消息
假设队列TTL为5秒,消息TTL为3秒,那么消息存留时间为3秒,因为3秒之后,该消息会自动移除
2.队列TTL与消息TTL的差异是什么?
队列TTL可以和死信队列配合,当队列里面的消息生存时间一旦超过设置的值TTL值,就称为dead message,会被投递到死信队列,消费者将无法从原来的队列中收到该消息
消息TTL则无法与死信队列配合
死信队列
DLX,全称为Dead-Letter-Exchange,可以称之为死信交换机,也有人称之为死信邮箱,当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX,绑定DLX的队列就称为死信队列,消息变成死信,可能是以下原因:
- 消息被拒绝
- 消息过期
- 队列达到最大长度
如何绑定死信队列呢?
在普通队列中设置有关死信队列的参数,`x-dead-letter-exchange`代表死信交换机,`x-dead-letter-routing-key`代表死信交换机与死信对列关联的key
@Bean
public Queue ttlDirectQueue(){
Map<String,Object> args = new HashMap<>();
args.put("x-message-ttl",5000);//这里一定是int类型,单位是毫秒
//给死信队列绑定与死信交换机绑定
args.put("x-dead-letter-exchange","dead_direct_exchange");
args.put("x-dead-letter-routing-key","dead");//fanout模式不需要配置
return new Queue("ttl.direct.queue",true,false,false,args);
}
内存磁盘预警
内存默认是服务器内存的0.4,当RabbitMQ内存占用达到0.4的时候,web监控页面会爆红,这个时候所有队列将会挂起,无法接收消息,这个时候我们可以到服务器设置RabbitMQ的内存阈值,一般设置为0.4-0.7的服务器内存
内存换页
当内存快到达阈值的时候,默认会把0.5的数据转移到磁盘,从而减轻内存压力,一般设置为0.4,0.5