RabbitMQ高级篇

RabbitMQ高级篇

任何技术都会有失误的地点,所以我们应当考虑周全

在RabbitMQ高级部分,我们可以通过各种方式确保MQ消息的可靠性,如果真的发送失败,有没有其它的兜底方案。

生产者的可靠性

对于消息丢失的可能性,我们先从流程分析,消息从发送者发送消息,到消费者处理消息。

消息从生产者到消费者的每一步都可能导致消息丢失。

  • 发送消息时丢失
    • 生产者发送消息时连接MQ失败
    • 生产者发送消息到MQ后未找到Exhange
    • 生产者发送消息到MQ的exchange后,未找到合适的Queue
    • 消息到达MQ后,处理消息的进程发生异常
  • MQ导致消息丢失:
    • 消息到达MQ,保存到队列后,尚未消费或突然宕机
  • 消费者处理消息时:
    • 消息接收后尚未处理突然宕机
    • 消息接收后处理过程中抛出异常

因此我们要解决消息丢失问题,保证MQ的可靠性,就必须从3个方面入手:

  1. 确保生产者一定把消息发送到MQ
  2. 确保MQ不会将消息弄丢
  3. 确保消费者一定要处理消息

生产者重试机制

当生产者发送消息时,发生了网络故障,导致于MQ的连接中断。

为了解决这个问题,SpringAMQP提供的消息发送时的重试机制。也就是当与MQ连接超时,多次重试。

在yml文件中进行配置

spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = 失败后的初始等待时间(initial-interval) * multiplier
max-attempts: 3 # 最大重试次数

就会发现当连接超时,每隔一秒重试一次,总共能重试3次

注意的是,当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率,不过SpringAMQP提供的重试机制是 阻塞式的重试,也就是说在多次重试等待的过程中,当前的线程是堵塞的。

所以对于业务性能有要求时,建议禁用重试机制,如果一定要使用,要合理配置等待时长和重试机制,也可以使用异步线程来执行发送消息的代码。

生产者确认机制

一般情况下,只要生产者与MQ之间的网络连接顺畅,基本不会出现消息丢失的情况。

不过在有的情况下,会出现消息发送到MQ之后丢失的现象

  1. MQ内部处理消息的进程发生了异常
  2. 生产者发送消息到MQ后未找到Exchange
  3. 生产者发送消息到达MQ的Exchange后,未找到合适的Queue,因此无法路由

针对上述情况,RabbitMQ提供了生产者确认机制

  • Publisher Confirm
  • Publisher Return

在开启机制的情况下,当生产者发送消息给MQ后,MQ会根据消息处理的情况返回不同的回执。

  • 当消息投递到MQ,但是路由失败时,通过Publisher Return返回异常消息,同时返回ack的确认信息,代表投递成功
  • 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
  • 持久消息投递了MQ,并且入队完成持久化,返回ACK,告知投递成功
  • 其他情况都会返回NACK,告知投递失败

其中ack和nack属于Publisher Confirm机制,ack是投递成功;nack是投递失败,而return则属于Publisher Return机制

默认两种机制都是关闭状态,都需要通过配置文件来开启

实现生产者确认

在模块下的yaml中添加配置:

spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制

publisher-confirm-type有三种模式可选:

  • none:关闭confirm
  • simple:同步堵塞等待MQ的回执
  • correlated:MQ异步同调返回回调

一般推荐correlated,回调机制

由于每个消息发送时的处理逻辑不一定相同,因此ConfirmCallback需要在每次发消息时定义,具体来说就是在调用RabbitTemplate中的convertAndSend方法时,多传递一个参数:

CorrelationData中包含两个核心的东西:

  • id:消息的唯一标识,MQ对不同的消息的回执以此做判断,避免混淆
  • SettableListenableFuture:回执结果的Future对象

将来MQ的回执就会通过这个Future来返回,我们可以提前给CorrelationData中的Future添加回调函数来处理消息回执

MQ的可靠性

消息的MQ以后,如果MQ不能及时保存,也会导致消息丢失,所以MQ的可靠性也非常重要

数据持久化

为了提高性能,默认情况下MQ的数据都是在内存存储的临时数据,重启后就会消失,为了保证数据的可靠性,必须配置数据持久化:

  • 交换机持久化
  • 队列持久化
  • 消息持久化

交换机持久化

在控制台的Exchanges页面,添加交换机时可以配置交换机的Durability参数,设置Durable就是持久化模式,Transient就是临时模式。

队列持久化

在控制台的Queues页面,添加队列时,同样可以配置队列的Durability参数

消息持久化

在控制台发送消息的时候,可以添加很多参数,而消息的持久化是要配置一个Delivery mode;

注意

1.持久化的消息:是在没有消费的情况下存在MQ,消费了也被删除

2.在开启持久化机制之后,如果同时还开启了生产者确认,那么MQ会在消息持久化以后才发送ACK回执,进一步确保消息的可靠性。

出于性能考虑,为了减少IO次数,发送到MQ的消息并不是逐条持久化到数据库的,而是每隔一段时间批量持久化,一般间隔在100毫秒左右,这就会导致ACK有一定的延迟,一次建议生产者确认全部采用异步方式

LazyQueue

在默认情况下,RabbitMQ会将接收到的消息保存在内存中以降低消息收发的延迟,但在某些情况下,会导致消息挤压,比如:

  • 消息者宕机或者出现网络故障
  • 消息发送量激增,超过了消费者处理速度
  • 消费者处理业务发生堵塞

一旦出现消息堆积问题,RabbitMQ的内存占用就会越来越高,直到触发内存预警上限,此时RabbitMQ会将内存消息刷到磁盘上,这个行为称为PageOut,PageOut会耗费一段时间,并且会堵塞队列进程,因此这个过程RabbitMQ不会再处理新的消息,生产者的所有请求都会被堵塞。

所以为了解决这个问题,从RabbitMQ的3.6版本开始,就增加了Lazy Queue的模式,也就是惰性队列。

惰性队列的特性:

  • 接收到消息后直接存入磁盘而非内存
  • 消费者要消费消息时才会从磁盘中读取并加载到内存(也就是懒加载)
  • 支持数百万的消息存储

在3.12后,LazyQueue已经成为所有队列的默认格式。因此官方推荐升级MQ为3.12版本或者所有队列都设置为LazyQueue模式

代码配置Lazy模式

在利用SpringAMQP声明队列的时候,添加x-queue-mod=lazy参数也可设置队列为Lazy模式

@Bean
public Queue lazyQueue(){
return QueueBuilder.durable("lazy.queue")
.lazy() // 消息队列是否是lazy模式;里面设置了一个参数x-queue-mode,表示是否是lazy模式
.build();
}

当然也可以基于注解来声明队列并设置为Lazy模式

@RabbitListener(
queuesToDeclare = @Queue(
name = "lazy.queue",
durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy")
)
)
public void listenLazyQueue(String message) {
System.out.println("lazyQueue: " + message);
}

消费者的可靠性

当RabbitMQ向消费者投递消息以后,需要直到消费者的处理状态,因为消息投递给消费者并不代表就一定被正确消费了,也可能出现的故障有很多

列如:

  • 消息投递的过程中出现了网络故障
  • 消费者接收到消息后突然宕机
  • 消费者接收到消息后,因处理不当导致异常

如果出现情况,消息会丢失,因此RabbitMQ必须知道消费者的处理状态,一旦消息处理失败才能重新投递消息

消费者确认机制

为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制,当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态

回执有三种可选值:

  1. ack:成功处理消息,RabbitMQ从队列中删除该消息
  2. nack:消息处理失败,RabbitMQ需要再次投递消息
  3. reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

一般reject方式用的较少,除非是消息格式有问题,那就是开发问题,因此大多数情况下我们需要将消息处理的代码通过trycatch机制捕获,消息处理成功时返回ack,处理失败时返回nack

SpringAMQP帮我们实现了消息确认,并允许我们通过配置文件设置ACK处理方式,有三种模式:

  • none:不处理,即消息投递给消费者后立刻ack,消息会立刻从MQ删除,非常不安全,不建议使用
  • manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活
  • auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack,当业务出现异常时,根据异常判断返回不同结果;
    • 如果是业务异常,会自动返回nack
    • 如果是消息处理或校验异常,自动返回reject

失败重试机制

当消费者发现异常后,消息会不断重入队到队列,再重新发送给消费者,如果消费者再次执行依然出错,消息会再次requeue到队列,再次投递,直到消息处理成功为止。

极端情况就是消费者一直无法执行成功,那么消息requeue就会无限循环,导致mq的消息处理飙升,带来不必要的压力。

为了以上这种情况,Spring提供了消费者失败重试机制:在消费者出现异常时利用本地重试,而不是无限制的requue到mq队列。

修改服务的application.yml文件

spring: rabbitmq:   listener:     simple:       retry:         enabled: true         max-attempts: 3 # 最大重试次数         initial-interval: 1000 # 重试间隔时间         multiplier: 1 # 重试间隔时间倍数;下次等待时长 = 上次重试间隔时间(initial-interval) * multiplier         stateless: true # 是否为无状态的,即重试时会重新获取一次连接;如果包含事务则为 false

启动之后,消费者在失败后没有重新回到MQ无限重新投递,而是在本地重试了3次。抛出AmqpRejectAndDontRequeueException异常。查看RabbitMQ控制台,发现消息被删除了。说明最后SpringAMQP返回的是reject

在本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试,重试达到最大次数后,Spring会返回reject,消息会被丢弃。

失败处理策略

在之前的测试中,本地测试达到最大重试次数后,消息会被丢弃,这在某些对于消息可靠性要求较高的业务场景下,显然不太合适。

因此Spring允许我们自定义重试次数耗尽后的消息处理策略,这个策略是由MessageRecovery接口来定义的,他有三个不同实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

延迟消息

在电商的支付业务中,对于一些库存有限的商品,为了更好的用户体验,通常会在用户下单时立刻扣减商品库存,列如电影院购票,高铁购票,下单后就会锁定座位资源,其他人无法重复购买,但是这种就会存在一个问题,假如用户下单之后一直不付款,就会一直占有库存资源,导致其他客户无法正常交易,最终导致商户利益受损。

因此,电商中的通常做法就是

对于超过一定时间未支付的订单,应该立刻取消订单并释放占用的库存

比如30分钟算是订单超时,那么我们应该在用户下单后的第30分钟检查订单支付状态,如果发现未支付,应该立刻取消订单,释放库存。像是这种在一段时间后才执行的任务,我们称为延迟任务,我们要实现延迟任务,最简单的方案就是利用MQ的延迟消息。

延迟消息:生存者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间后才收到消息。

在RabbitMQ中实现延迟消息也有两种方案:

  1. 死信交换机+TTL
  2. 延迟消息插件

死信交换机

在一个队列的消息满足下列情况之一的时候,就可以成为死信:

  • 消费者适用basic.reject或者basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息。超时无人消费
  • 要投递的队列消息满了,无法投递

如果一个队列的消息成为死信,并且这个队列通过dead-letter-exchange属性指定了一个交换机,那么队列中的死信就会投递到这个交换机中, 而这个交换机就成为 死信交换机.而此时加入有队列与死信交换机绑定,则最终死信就会被投递到这个队列中。

死信交换机有什么作用

  1. 收集那些因处理失败而被拒绝的消息
  2. 收集那些因队列而被拒绝的消息
  3. 收集因TTL到期的消息

延迟消息插件

基于死信队列虽然可以实现延迟消息,但是很麻烦。因此RabbitMQ社区提供了一个延迟消息插件来实现相同的效果。

如果需要使用,则需要在服务器部署下载。

Tags · rabbitmq/rabbitmq-delayed-message-exchange · GitHub

声明延迟交换机

基于注解形式,并接收消息

/*
监听 delay.queue 队列的消息
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "delay.queue", durable = "true"),
exchange = @Exchange(value = "delay.direct",delayed = "true"),
key = "delay"
))
public void listenDelayQueueMessage(String message) throws Exception {
System.out.println("SpringRabbitListener listenDelayQueueMessage 消费者接收delay.queue的延迟消息: " + message + " " + LocalTime.now());
}

基于@Bean方式声明

package com.itheima.consumer.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DelayExchangeConfig {
//定义名字为 delay.direct的延时 direct类型的交换机
public DirectExchange delayDirectExchange() {
return ExchangeBuilder.directExchange("delay.direct")
.delayed()
.durable(true)
.build();
}
//定义名字为 delay.queue 的队列
public Queue delayQueue() {
return new Queue("delay.queue");
}
//绑定队列和交换机
public Binding delayBinding(Queue delayQueue, DirectExchange delayDirectExchange) {
return BindingBuilder.bind(delayQueue).to(delayDirectExchange).with("delay");
}
}

发送延迟消息

/**
* 发送到 delay.direct 交换机的延迟消息;延迟5秒
*/
@Test
public void testDelayMessage() {
// 发送 5s 过期消息
System.out.println("发送时间:" + LocalTime.now());
rabbitTemplate.convertAndSend("delay.direct", "delay", "hello, delay message ", new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setDelay(5000);
return message;
}
});
}

注意

延迟消息插件内部会维护一个本地数据库表,同时会使用Erlang Timers功能实现计时,如果消息的延迟时间设置较长,可能会导致堆积的延迟消息非常多,会带来较大CPU开销,同时延迟消息的时间会存在误差

所以,不建议设置延迟时间过长的延迟消息

本文作者:奕帆卷卷

本文链接:https://www.cnblogs.com/yifan0820/p/18055074

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   奕帆卷卷  阅读(31)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起