一、RabbitMQ介绍、安装及基本操作
1、RabbitMQ介绍
-
RabbitMQ是⼀个在AMQP基础上完成的,可复用的企业消息系统。他遵循Mozilla Public License开源协议。
-
AMQP,即Advanced Message Queuing Protocol, ⼀个提供统⼀消息服务的应用层标准高级消息队列协议,是应用层协议的⼀个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有 RabbitMQ等。
-
主要特性:
- 保证可靠性 :使用⼀些机制来保证可靠性,如持久化、传输确认、发布确认
- 灵活的路由功能
- 可伸缩性:支持消息集群,多台RabbitMQ服务器可以组成⼀个集群
- 高可用性 :RabbitMQ集群中的某个节点出现问题时队列仍然可用
- 支持多种协议
- ⽀持多语言客户端
- 提供良好的管理界面
- 提供跟踪机制:如果消息出现异常,可以通过跟踪机制分析异常原因
- 提供插件机制:可通过插件进行多方面扩展
2、docker安装RabbitMQ
2.1、下载 RabbitMQ 镜像
下载最新镜像:
docker pull rabbitmq
2.2、创建并运行 RabbitMQ 容器
启动命令:
docker run -d -p 15672:15672 -p 5672:5672 \
-e RABBITMQ_DEFAULT_VHOST=my_vhost \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=admin \
--hostname myRabbit \
--name rabbitmq \
rabbitmq
参数说明:
-d
:表示在后台运行容器;-p
:将容器的端口 5672(应用访问端口)和 15672 (控制台Web端口号)映射到主机中;-e
:指定环境变量:- RABBITMQ_DEFAULT_VHOST:默认虚拟机名;
- RABBITMQ_DEFAULT_USER:默认的用户名;
- RABBITMQ_DEFAULT_PASS:默认的用户密码;
--hostname
:指定主机名(RabbitMQ 的一个重要注意事项是它根据所谓的 节点名称 存储数据,默认为主机名);--name rabbitmq
:设置容器名称;rabbitmq
:容器使用的镜像名称;
查看启动情况:
docker ps -l
设置 docker 启动的时候自动启动(可选):
docker update rabbitmq --restart=always
2.3、启动 rabbitmq_management
(web管理后台)
方法一:
docker exec -it rabbitmq /bin/bash # 进入容器内部
---------------------------------
user@7b295c46c99d /: rabbitmq-plugins enable rabbitmq_management # 开启后台
方法二:
docker exec -it rabbitmq rabbitmq-plugins enable rabbitmq_management # 一步搞定
2.4、访问RabbitMQ后台管理
- 浏览器输入地址:
http://ip:15672
即可访问后台管理页面,这里的ip
为运行 RabbitMQ 所在的服务器的 IP 地址; - 默认的用户名和密码都是
guest
(如果没有在容器创建的时候指定用户名密码); - 但由于我们启动的时候设置了默认的用户名和密码,所以我们可以使用设置的用户名和密码登录。
提示: 如果无法访问可以尝试开启防火墙 15672 端口:
firewall-cmd --zone=public --add-port=15672/tcp --permanent
firewall-cmd --reload
3、RabbitMQ逻辑结构
RabbitMQ逻辑结构 |
---|
- 用户
- 虚拟主机
- 队列
4、RabbitMQ用户管理
4.1、用户管理
4.1.2、命令行用户管理
- 在Linux中使用命令行创建用户
# 进入rabbitmq的sbin目录
cd /usr/local/rabbitmq_server-3.7.0/sbin
# 新增用户
./rabbitmqctl add_user likelong 247907lkl
- 设置用户级别
## ⽤户级别:
## 1.administrator 可以登录控制台、查看所有信息、可以对RabbitMQ进⾏管理
## 2.monitoring 监控者 登录控制台、查看所有信息
## 3.policymaker 策略制定者 登录控制台、指定策略
## 4.managment 普通管理员 登录控制台
./rabbitmqctl set_user_tags likelong administrator
4.1.3、管理后台用户管理
1、新增用户 |
---|
2、创建虚拟机 |
---|
3、删除用户 |
---|
4、用户绑定虚拟机 |
---|
5、RabbitMQ工作方式
RabbitMQ提供了多种消息的通信方式—工作模式
https://www.rabbitmq.com/getstarted.html
5.1、simple简单模式
一个队列只有一个消费者
生产者将消息发送到队列,消费者从队列取出数据
5.2、work工作模式
多个消费者监听同⼀个队列,虽然有多个消费者,但是一条消息只能由一个消费者消费 (资源的竞争)
多个消费者监听同⼀个队列,但多个消费者中只有⼀个消费者会成功的消费消息
5.3、publish/subscribe订阅模式(Fanout模式)
⼀个交换机绑定多个消息队列,每个消息队列有⼀个消费者监听(共享)
消息生产者发送的消息可以被每⼀个消费者接收 |
5.4、路由模式(Direct模式)
⼀个交换机绑定多个消息队列,每个消息队列都有自己唯⼀的key,每个消息队列有⼀个消费者监听(根据路由值精准匹配)
路由模式 |
---|
5.5、topic主题模式(路由模式的一种)
同路由模式,只不过是根据路由值进行通配符匹配。通配符有#和*;
#:可以匹配多个元素(可以是0个)
*:只能匹配一个元素
6、RabbitMQ交换机和队列管理
6.1、创建队列
创建队列 |
---|
6.2、创建交换机
创建交换机 |
---|
6.3、交换机绑定队列
交换机绑定队列 |
---|
7、消息队列三大作用
- 流量削峰
- 异步处理
- 系统解耦
通过以下demo证实其作用。
二、RabbitMQ代码实践
demo主要实现:
- 假设场景用户注册之后需要向数据库新增数据,然后调用第三方接口向用户注册所用手机号发送注册成功消息,然后调用第三方接口向用户注册所用邮箱发送注册成功邮件。
- 假设场景用户注销之后需要向数据库修改数据,然后调用第三方接口向用户注册所用手机号发送注销成功消息,然后调用第三方接口向用户注册所用邮箱发送注销成功邮件。
场景图如下:
实战:
项目主要依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
未使用RabbitMQ
项目结构:
yml配置文件:
server:
port: 9001
spring:
application:
name: provider
rabbitmq:
host: 47.96.156.51
port: 5672
virtual-host: host1
username: likelong
password: 247907lkl
controller代码如下:
注册接口:
注销接口:
模拟数据库,发送短信,发送邮箱一些耗时操作。
<IDEA父子项目的坑>
idea父项目pom文件才需要加<packaging>pom</packaging>
,如果子项目加了这个,子项目配置文件直接会失效。(自己踩的实坑)
postman测试:
注销接口:
以上代码存在的问题:在接口内的三步操作顺序执行(改库、发短信、发邮件),并且注册和注销都需要做发送短信和邮件的操作,所以在注销和注册接口内都写了相关重复的逻辑(高耦合),并且若QPS数据量太大,直接访问接口会对服务器造成极大压力 ,严重的会造成宕机。
使用RabbitMQ
由此,引入RabbitMQ中间件进行改造,首先:
- 实现异步处理:上述代码可以得知,发送消息和发送邮件是同步执行,必须前面执行完后面才执行,但是根据需求,后者并不需要前者执行完才能执行,完全可以异步执行,异步执行可以大大减少消耗时间。并且用户只需要真实的注册和注销完成即可,通知只是次要的,所有接口里面实现库的操作即可,消息的操作直接交给消息监听器去处理即可。
- 实现系统解耦:上述代码可以得知,注册和注销都存在消息的处理(重复,高耦合),使用RabbitMQ可以将重复处理的代码交给消息监听器处理,接口只实现每个接口的核心代码即可。
- 实现流量削峰:当大量请求走向应用服务时,服务器压力太大,可以使用RabbitMQ,让请求先走向消息中间件,再让应用服务器去消费中间件里的消息。
改造后的场景图如下:
注销同理。
模块化工程,服务拆分
模拟分布式,调用A服务接口,接口向RabbitMQ发送消息,B服务再去处理消息,减小A服务的服务器压力
目录结构如下:
编写common模块代码,存放消息队列需要的常量。
在provider服务新增一个controller类
注册接口:
注销接口:
相比上面的同步执行改库——发送消息——发送邮件,删除后面两步,引入rabbitTemplate,调用他的convertAndSend方法向交换机发送消息,使用常量里设置的交换机的值,和路由键的值。
路由键是6种模式中路由模式和主题模式所特有,而路由模式是绝对匹配
,而主题模式是通配符匹配
,此处讲讲通配符匹配规则:
通配符有#和*
#:可以匹配任意个数的元素
*:只能匹配一个元素
所以在主题模式下,该交换机的路由值,会匹配到邮件、短信两个队列的路由值。
在消费者服务创建topic配置,配置交换机和队列的绑定关系
注意加上@Bean和@Configuration注解,交给Spring管理。
新建两个监听器(监听邮件队列和短信队列)
取出生产者发送到消息队列的消息,判断类型是注册还是注销,然后走具体的处理。
@RabbitListener注解的类表示该类为监听器,参数为队列名称, @RabbitHandler表示该方法会去处理监听器获取的消息的逻辑。
postman再次测试:
核心代码部分一秒钟即可执行完成并返回结果。
而消费者服务在此后异步进行了短信逻辑和邮件逻辑的处理,如上图。
三、消息队列常见问题
每种MQ都要从三个角度来分析: 生产者弄丢数据、消息队列弄丢数据、消费者弄丢数据
消息的可靠性:从
生产者发送消息
——消息队列存储消息
——消费者消费消息
的整个过程中消息的安全性及可控性。
- 生产者
- 消息队列
- 消费者
1、生产者弄丢数据
1.1、RabbitMQ事务
RabbitMQ提供transaction和confirm模式来确保生产者不丢消息(两者只能选其一,不能同时使用)
RabbitMQ事务指的是基于客户端实现的事务管理,当在消息发送过程中添加了事务,处理效率降低几十倍甚至上百倍(用的比较少),confirm模式使用较多
transaction机制就是说,发送消息前,开启事物(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()),如果发送成功则提交事物(channel.txCommit())
下面进行事务验证:
- 注册RabbitMQ事务管理器
@Configuration
public class RabbitMQConfig {
/**
* 注册rabbitmq事务管理器
*
* @param connectionFactory 连接工厂
* @param rabbitTemplate rabbit模板
* @return rabbitmq事务管理器
*/
@Bean
public RabbitTransactionManager rabbitTransactionManager(CachingConnectionFactory connectionFactory, RabbitTemplate rabbitTemplate) {
// channel开启事务支持
rabbitTemplate.setChannelTransacted(true);
return new RabbitTransactionManager(connectionFactory);
}
}
- 发送者发送消息:需使用@Transactional(rollbackFor = Exception.class)事务注解
接下来postman测试:
消费者未收到任何消息,事务开启成功!
注释掉int i = 1 / 0;
代码
继续测试。
消费者收到两条消息,完美!
1.2、RabbitMQ消息确认和return机制
消息确认机制:确认消息提供者是否成功发送消息到交换机
return机制:确认消息是否成功的从交换机分发到队列
生产者
配置文件新增后面两项:
server:
port: 9001
spring:
application:
name: provider
rabbitmq:
host: 47.96.156.51
port: 5672
virtual-host: host1
username: likelong
password: 247907lkl
publisher-confirm-type: simple # 开启消息确认
publisher-returns: true # 开启return机制
confirm监听:
@Component
public class MyConfirmListener implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//init方法会在创建当前MyConfirmListener对象之后执行
@PostConstruct //加上该注解的方法,会在实例化对象之后执行
public void init() {
rabbitTemplate.setConfirmCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
//参数b表示消息确认结果 参数s表示发送的消息
if (b) {
System.out.println("消息成功发送到交换机!");
} else {
System.out.println("消息发送到交换机失败!");
//消息发送失败,重发消息
rabbitTemplate.convertAndSend(RabbitMQConstant.EXCHANGE_NAME, RabbitMQConstant.ROUTER_KEY_EXCHANGE, s);
}
}
}
return监听:
@Component
public class MyReturnListener implements RabbitTemplate.ReturnsCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setReturnsCallback(this);
}
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
//只要此方法执行说明交换机分发消息到队列失败
System.out.println("消息从交换机分发到队列失败");
//获取交换机
String exchange = returnedMessage.getExchange();
//获取路由key
String routingKey = returnedMessage.getRoutingKey();
//获取发送消息
String msg = returnedMessage.getMessage().toString();
//失败就再次发送
rabbitTemplate.convertAndSend(exchange,routingKey,msg);
}
}
新增方法:
测试:
2、消息队列弄丢数据
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发
那么如何持久化呢,其实也很容易,就下面两步
1、将queue的持久化标识durable设置为true,则代表是一个持久的队列
2、发送消息的时候将deliveryMode=2
这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据
3、消费者弄丢数据
消费者丢数据一般是因为采用了默认自动确认消息模式。这种模式下,消费者会自动确认收到信息。这时RabbitMQ会立即将消息删除,这种情况下如果消费者出现异常而没能处理该消息,就会丢失该消息
至于解决方案,采用手动确认消息即可。
手动ack实现如下:
消费者
配置文件新增如下红框内容:
注意点:发送者发送消息最好用json格式字符串,以便消费者解析消费。
4、消息重复消费
消息重复的原因
消息重复的原因有两个:1.生产时消息重复,2.消费时消息重复。
-
生产时消息重复
由于生产者发送消息给MQ,在MQ确认的时候出现了网络波动,生产者没有收到确认,实际上MQ已经接收到了消息。这时候生产者就会重新发送一遍这条消息。 -
消费时消息重复
消费者消费成功后,再给MQ确认的时候出现了网络波动,MQ没有接收到确认,为了保证消息被消费,MQ就会继续给消费者投递之前的消息。这时候消费者就接收到了两条一样的消息。
消息消费的幂等性——多次消费的执行结果时相同的 (避免重复消费)
如何保证消息幂等性
让每个消息携带一个全局的唯一ID,即可保证消息的幂等性,具体消费过程为:
消费者获取到消息后先根据id去查询redis/db是否存在该消息
如果不存在,则正常消费,消费完毕后写入redis/db
如果存在,则证明消息被消费过,直接丢弃
四、消息队列作用/使用场景总结
1、系统解耦
场景说明:用户下单之后,订单系统要通知库存系统
传统方式:订单系统直接调用库存系统提供的接口,如果库存系统出现故障会导致订单系统失败 |
---|
使用消息队列: |
---|
2、异步处理
场景说明:用户注册成功之后,需要发送短信和邮箱提醒
传统方式:用户注册,数据库新增成功后,发送邮件,发送邮件成功后,发送短信,最后等发送短信成功之后才会给用户响应 |
---|
使用消息队列: |
---|
3、消息通信
场景说明:应用系统之间的通信,例如聊天室
聊天室 |
---|
4、流量削峰
场景说明:秒杀业务
秒杀业务:大量的请求不会主动请求秒杀业务,而是存放在消息队列(缓存) |
---|
5、日志处理
场景说明:系统中大量的日志处理
日志搜集处理 |
---|