1.前言MQ的应用场景
一、什么是mq?
MQ全称 Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信,解耦。
二、常见的mq产品
RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq
RabbitMQ: One broker to queue them all | RabbitMQ
三、作用
3.1、异步处理
场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种 1.串行的方式 2.并行的方式
串行方式: 将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是,邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西.
并行方式:将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间。
消息队列:假设三个业务节点分别使用50ms,串行方式使用时间150ms,并行使用时间100ms。虽然并行已经提高的处理时间,但是,前面说过,邮件和短信对正常的使用网站没有任何影响,客户端没有必要等着其发送完成才显示注册成功,应该是写入数据库后就返回.
引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理
由此可以看出,引入消息队列后,用户的响应时间就等于写入数据库的时间+写入消息队列的时间(可以忽略不计),引入消息队列后处理后,响应时间是串行的3倍,是并行的2倍。
3.2、应用解耦
场景:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口.
这种做法有一个缺点:
当库存系统出现故障时,订单就会失败。 订单系统和库存系统高耦合. 引入消息队列
订单系统
:
用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
库存系统
:
订阅下单的消息,获取下单消息,进行拆订单操作。 就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失.
3.3、流量削峰
场景: 秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。
作用:
1.可以控制活动人数,超过此一定阀值的订单直接丢弃.
2.可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)
2. Docker 安装
1.官方镜像
该镜像包含用户操作界面
2.Docker运行,并设置开机自启动
docker run -d --restart=always --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.10-management
默认登录账户和密码 guest
3. 交换机与队列的介绍
1.流程
首先先介绍一个简单的一个消息推送到接收的流程,提供一个简单的图
黄色的圈圈就是我们的消息推送服务,将消息推送到 中间方框里面也就是 rabbitMq的服务器,然后经过服务器里面的交换机、队列等各种关系(后面会详细讲)将数据处理入列后,最终右边的蓝色圈圈消费者获取对应监听的消息。
2. 交换机类型(面试题)
常用的交换机有以下三种,因为消费者是从队列获取信息的,队列是绑定交换机的,所以对应的消息推送/接收模式也会有以下几种:
1、Direct Exchange
直连型交换机,根据消息携带的路由键将消息投递给对应队列。
大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键 routing key 。
然后当一个消息携带着路由值为abc,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值abc去寻找绑定值的队列。
2、Fanout Exchange
扇型(广播)交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
3、Topic Exchange
主题交换机,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。
简单地介绍下规则:
* (星号) 用来表示一个单词 (必须出现的)
# (井号) 用来表示任意数量(零个或多个)单词
// * 代表两点之间一个占位单词 // # 代表后面所有,匹配所有
Plain Text
通配的绑定键是跟队列进行绑定的,举个小例子
队列Q1 绑定键为 *.TT.* 队列Q2绑定键为 TT.#
如果一条消息携带的路由键为 A.TT.B,那么队列Q1将会收到;
如果一条消息携带的路由键为TT.AA.BB,那么队列Q2将会收到;
当一个队列的绑定键为 "#"(井号) 的时候,这个队列将会无视消息的路由键,接收所有的消息。
当 * (星号) 和 # (井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为。
如果只有 # ,它就实现了扇形交换机的功能。
所以主题交换机也就实现了扇形交换机的功能,和直连交换机的功能
3.交换机与队列特性设置(了解)
Exchange、Queue属性 durability
durability默认是durable(持久化),durability 属性用于定义队列或者交换机是否是持久化的。
当一个队列或者交换机被声明为持久化的时候,它们的元数据会被存储到磁盘上,这样即使在 RabbitMQ 服务器重启后,这些队列或者交换机的定义仍然存在。
持久化的队列和交换机可以确保消息的持久化,即使 RabbitMQ 服务器重启或者崩溃,消息也不会丢失。当消息被发送到持久化的队列或者交换机时,它们会被写入磁盘,以确保消息的持久性。
需要注意的是,仅仅将队列或者交换机声明为持久化是不够的,消息本身也必须被标记为持久化,才能确保消息的持久性。这可以通过在消息的属性中设置 delivery_mode 为 2 来实现。
总之, durability 属性在 RabbitMQ 中用于确保队列和交换机的持久化,以及消息的持久性,从而提高消息传递的可靠性。
Exchange、Queue类型 delete
在 RabbitMQ 中, auto delete 属性用于定义队列或交换机是否在没有消费者或绑定时自动删除。
当一个队列或交换机被声明为 auto delete 属性为 true 时,当没有消费者与该队列关联或者没有绑定到该交换机的队列时,它们会自动被删除。
这个属性通常用于临时队列或者临时交换机的场景。临时队列或交换机是在没有消费者或者绑定时自动创建的,用于处理一些临时性的任务或者临时的消息传递。
需要注意的是,如果一个持久化的队列或交换机被声明为 auto delete 属性为 true,那么它们在没有消费者或绑定时也会被自动删除,但它们的定义会在 RabbitMQ 服务器重启后重新创建。
总之, auto delete 属性在 RabbitMQ 中用于定义队列或交换机是否在没有消费者或绑定时自动删除,适用于临时队列或交换机的场景。
Exchange属性Internal
Internal的意思是内部的意思,在交换机这里设置为“Yes”之后,表示当前Exchange是RabbitMQ内部使用,用户所创建的Queue不会消费该类型交换机下的消息,既然是为了RabbitMQ系统所用,作为用户,我们就没有必要创建该类型的Exchange,当然默认也是选择No.
4.SpringBoot整合RabbitMQ
流程图概括
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2. 配置RabbitMQ连接
在application.properties
或application.yml
中配置RabbitMQ服务器的连接参数:
#配置RabbitMQ
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
3.DirectExchange
3.1消费者
@Configuration
public class DirectConsumer {
//注册一个队列
@Bean //启动多次为什么不报错?启动的时候,它会根据这个名称Direct_Q01先去查找有没有这个队列,如果有什么都不做,如果没有创建一个新的
public Queue queue(){
return QueueBuilder.durable("Direct_Q01").maxLength(100).build();
}
//注册交换机
@Bean
public DirectExchange exchange(){
//1.启动的时候,它会根据这个名称Direct_E01先去查找有没有这个交换机,如果有什么都不做,如果没有创建一个新的
return ExchangeBuilder.directExchange("Direct_E01").build();
}
//绑定交换机与队列关系
@Bean
public Binding binding(Queue queue,DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("RK01");
}
//启动一个消费者
@RabbitListener(queues = "Direct_Q01")
public void receiveMessage(String msg){
System.out.println("收到消息:"+msg);
}
}
3.2生产者
@Service
public class DirectProvider {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(Object message) {
rabbitTemplate.convertAndSend("Direct_E01", "RK01", message);
}
}
3.3 一个交换机对多个队列的特点:
3.4 一个队列对多个消费者特点:
5.FanoutExchange
4.1 消费者
4.2生产者
5.TopicExchange
5.1 消费者
5.2 生产者
6.死信队列
面试题:你们是如何保证消息不丢失的?
6.1、什么是死信
在 RabbitMQ 中充当主角的就是消息,在不同场景下,消息会有不同地表现。
死信就是消息在特定场景下的一种表现形式,这些场景包括:
1. 消息被拒绝访问,即 RabbitMQ返回 basicNack 的信号时 或者拒绝basicReject
2. 消费者发生异常,超过重试次数 。 其实spring框架调用的就是 basicNack
3. 消息的Expiration 过期时长或队列TTL过期时间。
4. 消息队列达到最大容量
上述场景经常产生死信,即消息在这些场景中时,被称为死信。
6.2、什么是死信队列
死信队列就是用于储存死信的消息队列,在死信队列中,有且只有死信构成,不会存在其余类型的消息。
死信队列在 RabbitMQ 中并不会单独存在,往往死信队列都会绑定这一个普通的业务消息队列,当所绑定的消息队列中,有消息变成死信了,那么这个消息就会重新被交换机路由到指定的死信队列中去,我们可以通过对这个死信队列进行监听,从而手动的去对这一消息进行补偿。 人工干预
6.3、那么,我们到底如何来使用死信队列呢?
死信队列基本使用,只需要在声明业务队列的时候,绑定指定的死信交换机和RoutingKey即可。
public class DeadConsumer {
//死信交换机
@Bean
public DirectExchange deadExchange(){
return ExchangeBuilder.directExchange("Dead_E01").build();
}
//死信队列
@Bean
public Queue deadQueue1(){
return QueueBuilder.durable("Dead_Q01").build();
}
//死信交换机与死信队列的绑定
@Bean
public Binding deadBinding1(Queue deadQueue1,DirectExchange deadExchange){
return BindingBuilder.bind(deadQueue1).to(deadExchange).with("RK_DEAD");
}
//业务队列
@Bean
public Queue queue1(){
return QueueBuilder
.durable("Direct_Q01")
.deadLetterExchange("Dead_E01")
.deadLetterRoutingKey("RK_DEAD")
//.ttl(10*1000) //该属性是队列的属性,设置消息的过期时间,消息在队列里面停留时间n毫秒后,就会把这个消息投递到死信交换机,针对的是所有的消息
//.maxLength(20) //设置队列存放消息的最大个数,x-max-length属性值,当队列里面消息超过20,会把队列之前的消息依次放进死信队列
.build();
}
//业务交换机
@Bean
public DirectExchange exchange(){
return ExchangeBuilder.directExchange("Direct_E01").build();
}
//业务交换机与队列的绑定
@Bean
public Binding binding1(Queue queue1,DirectExchange exchange){
return BindingBuilder.bind(queue1).to(exchange).with("RK01");
}
//@RabbitListener(queues = "Direct_Q01")
public void receiveMessage(OrderingOk msg) {
log.info("消费者1 收到消息:"+ msg );
int i= 5/0;
}
// @RabbitListener(queues = "Direct_Q01")
// public void receiveMessage(OrderingOk msg,Message message, Channel channel) throws IOException {
//
// long deliveryTag = message.getMessageProperties().getDeliveryTag();
//
// System.out.println("消费者1 收到消息:"+ msg +" tag:"+deliveryTag);
//
// channel.basicReject(deliveryTag, false);
// try {
// // 处理消息...
// int i= 5/0;
// // 如果处理成功,手动发送ack确认 ,Yes
// channel.basicAck(deliveryTag, false);
// } catch (Exception e) {
// // 处理失败,可以选择重试或拒绝消息(basicNack或basicReject) NO
// channel.basicNack(deliveryTag, false, false); // 并重新入队
//
// }
}
//}
6.4. 自动应答死信配置
#-------------MQ 高级配置---------
#预抓取数量
spring.rabbitmq.listener.simple.prefetch=50
#设置消费者手动应答模式
spring.rabbitmq.listener.simple.acknowledge-mode = auto
#开启自动应答重试机制
spring.rabbitmq.listener.simple.retry.enabled=true
#默认重试3次
spring.rabbitmq.listener.simple.retry.max-attempts=3
#重试间隔时间 单位ms
spring.rabbitmq.listener.simple.retry.initial-interval=1000ms
#时间间隔倍数,默认是1倍
spring.rabbitmq.listener.simple.retry.multiplier=2
#最大间隔时间
spring.rabbitmq.listener.simple.retry.max-interval=5000ms
7.延迟队列
7.1场景:
1.定时发布文章
2.秒杀之后,给30分钟时间进行支付,如果30分钟后,没有支付,订单取消。
3.预约餐厅,提前半个小时发短信通知用户。
A -> 13:00 17:00 16:30 延迟时间: 7*30 * 60 * 1000
B -> 11:00 18:00 17:30 延迟时间: 13*30 * 60 * 1000
C -> 8:00 14:00 13:30 延迟时间: 11*30 * 60 * 1000
7.2第一种方式:创建具有超时功能且绑定死信交换机的消息队列
@Bean
public Queue directQueueLong(){
return QueueBuilder.durable("业务队列名称")
.deadLetterExchange("死信交换机名称")
.deadLetterRoutingKey("死信队列 RoutingKey")
.ttl(20000) // 消息停留时间
//.maxLength(500)
.build();
}
监听死信队列,即可处理超时的消息队列
缺点:
上述实现方式中,ttl延时队列中所有的消息超时时间都是一样的,如果不同消息想设置不一样的超时时间,就需要建立多个不同超时时间的消息队列,比较麻烦,且不利于维护。
7.3第二种方式:创建通用延时消息
rabbitTemplate.convertAndSend("交换机名称", "RoutingKey","对象",
message => {
message.getMessageProperties().setExpiration(String.valueOf(5000))
return message;
}
);
缺点:
该种方式可以创建一个承载不同超时时间消息的消息队列,但是这种方式有一个问题,如果消息队列中排在前面的消息没有到超时时间,即使后面的消息到了超时时间,先到超时时间的消息也不会进入死信队列,而是先检查排在最前面的消息队列是否到了超时时间,如果到了超时时间才会继续检查后面的消息。
7.4 第三种方式:使用rabbitmq的延时队列插件,实现同一个队列中有多个不同超时时间的消息,并按时间超时顺序出队
7.4.1下载延迟插件
在 RabbitMQ 的 3.5.7 版本之后,提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列 ,同时需保证 Erlang/OPT 版本为 18.0 之后。
我这里 MQ 的版本是 3.10.0 现在去 GitHub 上根据版本号下载插件
Releases · rabbitmq/rabbitmq-delayed-message-exchange · GitHub
7.4.2安装插件并启用
我用的是 Docker 客户端,下载完成后直接把插件放在 /root 目录,然后拷贝到容器内plugins目录下(rabbitmq是容器的name,也可以使用容器id)
docker cp /opt/rabbitmmq/rabbitmq_delayed_message_exchange-3.10.0.ez rabbitmq:/plugins
进入 Docker 容器
docker exec -it rabbitmq /bin/bash
在plugins内启用插件
#先执行,解除防火墙限制,增加文件权限
umask 0022
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
退出容器
exit
重启 RabbitMQ
docker restart rabbitmq
通过UI查看
7.4.3原理
7.4.4如何使用?
1、消费者
@Configuration
@Slf4j
public class DelayConsumer {
@Bean //创建延迟队列
public Queue delayQueue(){
return QueueBuilder.durable("Delay_Q01").build();
}
@Bean //创建延迟交换机 CustomExchange为自定义交换机类型
public CustomExchange delayExchange(){
//x-delayed-message定义交换机的类型,map为参数,键为x-delayed-type,值为direct即为直连交换机
Map<String,Object> map= MapUtil.of("x-delayed-type","direct");
return new CustomExchange("Delay_E01","x-delayed-message",true,false,map);
}
@Bean
public Binding deadBinding(Queue delayQueue,CustomExchange delayExchange){
return BindingBuilder.bind(delayQueue).to(delayExchange).with("RK_DELAY").noargs();
}
@RabbitListener(queues = "Delay_Q01")
public void rabbitMessage(OrdingOK msg){
log.info("消费者1接收到消息"+msg);
}
2.生产者
@Service
public class DelayProvider {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMessage(OrdingOK ordingOK){
rabbitTemplate.convertAndSend("Delay_E01", "RK_DELAY", ordingOK, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
Integer id = ordingOK.getId();
int ttl= 0;
if (id==1){
ttl=50*1000;
}else if (id ==2 ){
ttl=40*1000;
}
else if (id ==3 ){
ttl=5*1000;
}
else if (id ==4 ){
ttl=20*1000;
}
else if (id ==5 ){
ttl=10*1000;
}
//设置expiration为消息的过期时间,如果第一个消息的过期时间比较长,可能会出现消息堵塞状态
//message.getMessageProperties().setExpiration(String.valueOf(expiration));
//延迟交换机使用的delay参数,设置延迟时间,单位为微秒
message.getMessageProperties().setDelay(ttl);
return message;
}
});
}
}
8.消息的可靠性
面试题:Rabbitmq怎么保证消息的可靠性?
1.消费端消息可靠性保证:
-
消息确认(Acknowledgements):
消费者在接收到消息后,默认情况下RabbitMQ会自动确认消息(autoAck=true)。为保证消息可靠性,可以设置autoAck=false,使得消费者在处理完消息后手动发送确认(basicAck)。如果消费者在处理过程中发生异常或者未完成处理就终止运行,那么消息在超时时间内将不会被删除,会再次被RabbitMQ投递给其他消费者。
2.死信队列(Dead Letter Queue):
当消息不能被正常消费时(比如达到最大重试次数),可以通过设置TTL(Time To Live)或者死信交换器(Dead Letter Exchange)将消息路由至死信队列,从而有机会后续分析和处理这些无法正常消费的消息。
2.生产端消息可靠性保证:
-
消息持久化:
当生产者发布消息时,可以选择将其标记为持久化(persistent).这意味着即使 RabbitMQ 服务器重启,消息也不会丢失,因为它们会被存储在磁盘上。
-
确认(Confirm)机制:
开启confirm回调模式后,RabbitMQ会在消息成功写入到磁盘并至少被一个交换器接受后,向生产者发送一个确认(acknowledgement)。若消息丢失或无法投递给任何队列,RabbitMQ将会发送一个否定确认(nack). 生产者可以根据这些确认信号判断消息是否成功送达并采取相应的重试策略。
RabbitMQ作为消息中间件并启用publisher confirms(发布者确认)与publisher returns(发布者退回)机制时,可以确保消息从生产者到交换机的投递过程得到更准确的状态反馈。
1.@PostConstruct注解
@PostConstruct注解是Java EE规范中的一部分,主要用于标记在一个Bean初始化完成后需要执行的方法。这个注解由JSR-250定义,并且在Spring框架以及其他遵循Java EE标准的应用服务器中广泛支持。
功能与用途:初始化方法,当容器完成对Bean的实例化并且所有依赖注入完成后,将会自动调用标有@PostConstruct
注解的方法。这为开发者提供了一个机会,在对象正式投入使用之前进行一些必要的初始化工作,比如初始化资源、预计算某些值、启动后台任务等增强。
2. Publisher Confirms(发布者确认)
作用: Publisher Confirm机制允许RabbitMQ服务器通知生产者一个消息是否已经被交换机正确接收。当publisher-confirm-type设置为CORRELATED时,RabbitMQ会向生产者发送确认或否定响应,确认消息已到达交换机,但不保证消息已被路由到至少一个队列中。
生产者到交换机的确认(消息到达交换机)
2.1.配置:
spring.rabbitmq.publisher-confirm-type = CORRELATED
2.2. 代码实现
只要到达交换机就会触发
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
// 消息成功投递成功并被确认
} else {
// 消息未能正确投递
}
}
});
3.Publisher Returns(发布者退回)
作用: Publisher Return机制用于当消息无法按照路由键规则路由到任何队列时,或者由于其他原因(例如队列满、消息过大等)而被交换机拒绝时,RabbitMQ将消息返回给生产者。
交换机到队列的确认(消息是否正常发送到了队列)
通过实现 ReturnCallback 接口,发送消息失败返回,比如交换机路由不到队列时触发回调:
1.只有消息没有路由到队列的时候,才触发该回调 .
2.只要有一个队列接受到消息了,它就认为成功.
3.1 配置
spring.rabbitmq.publisher-returns = true
3.2 代码实现
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
// 处理未被路由或因某种原因被退回的消息
}
});
4.完整代码
@Service
public class ReliabilityProvider implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
//1.生产者向队列确认机制
//2.无论什么情况下都会执行
rabbitTemplate.setConfirmCallback(this);
//只有消息路由不到时才不触发,正常不会触发
//若有一个队列成功,则默认全部成功,不会触发
rabbitTemplate.setReturnCallback(this);
}
public void sendMessage(ReliabilityOK reliabilityOK){
CorrelationData correlationData = new CorrelationData();
correlationData.setId(String.valueOf(reliabilityOK.getId()));
rabbitTemplate.convertAndSend("Direct_E02","RK01",reliabilityOK,correlationData);
}
//1.生产者向队列确认机制
//2.无论什么情况下都会执行
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
System.out.println("消息投递成功");
//当ack表现为trues时,拿到订单Id,修改订单状态
System.out.println(correlationData.getId());
}else {
System.out.println("消息未投递成功"+cause);
}
}
//只有消息路由不到时才不触发,正常不会触发
//若有一个队列成功,则默认全部成功,不会触发
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
System.out.println("消息未发送成功");
}
}
9. 如何保证消息的幂等性
在RabbitMQ中,保证消费者的幂等性主要依赖于业务设计和实现,而非RabbitMQ本身提供的一种直接功能。
在基于Spring Boot整合RabbitMQ的场景下,要保证消费者的幂等性,通常需要结合业务逻辑设计以及额外的技术手段来实现。以下是一个简化的实现思路:
幂等性保证的基本原则
生产端:
状态检查:在消息发送前,先查询数据库,确认此消息是否已被处理过。如果是,则直接忽略;否则,继续处理,并在处理完成后更新消息状态为已处理。
消费端:
唯一标识:每个消息都携带一个全局唯一的ID或业务ID(BizId),如订单号、交易流水号等,以便在消费端能够识别重复的消息。
@RabbitListener(queues = "q1")
//如何保证消费者的幂等性?
public void process(OrderingOk orderingOk) throws IOException {
// 1. 判断数据库是否已经处理过?
// 2. 如果存在直接丢弃
// 3. 如果不存在直接执行下面的业务代码
}
}