RabbitMQ是如何保证消息可靠性的?——Java全栈知识(16)

RabbitMQ 的消息不可靠也就是 RabbitMQ 消息丢失只会发生在以下几个方面:

  1. 生产者发送消息到 MQ 或者 Exchange 过程中丢失。
  2. Exchange 中的消息发送到 MQ 中丢失。
  3. 消息在 MQ 或者 Exchange 中服务器宕机导致消息丢失。
  4. 消息被消费者消费的过程中丢失。
    image.png
    大致就分为生产者 -> MQ -> 消费者这三步的时候消息丢失。

1、生产者到 MQ

1.1. 生产者重试机制

首先第一种情况,就是生产者发送消息时,出现了网络故障,导致与 MQ 的连接中断。
为了解决这个问题,SpringAMQP 提供的消息发送时的重试机制。即:当 RabbitTemplate 与 MQ 连接超时后,多次重试。
修改 publisher 模块的 application.yaml 文件,添加下面的内容:

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

1.2、生产者确认机制

1、Publisher Confirm 机制

也就是生产者确认机制,用于确保消息已经被 Exchange 成功接收和处理。一旦消息成功到达 Exchange 并被处理,RabbitMQ 会向消息生产者发送确认信号 (ACK)。如果由于某种原因(例如,Exchange 不存在或路由键不匹配)消息无法被处理,RabbitMQ 会向消息生产者发送否认信号 (NACK)

	//启用Publisher Confirms  channel.confirmSelect();  //设置Publisher Confirms回调  channel.addConfirmListener(new ConfirmListener() {  //在这里处理消息确认  @Override  public void handleAck(long deliveryTag, boolean multiple) throws IOException {  System.out.println("Message confirmed with deliveryTag:"deliveryTag);  }  //在这里处理消息未确认  @Override  public void handleNack(long deliveryTag, boolean multiple) throws IOException {  System.out.println("Message not confirmed with deliveryTag:"deliveryTag);  }  
});
2、Publisher Return 机制

Publisher Confirm 机制用于消息无法正常到达交换机中的情况,Publisher Return 机制用于消息无法正常路由到队列的情况。
image.png|800
当消息正常路由到队列中的时候,MQ 不会返回任何消息,当无法正常路由的时候会返回错误信息。

1.3、开启生产者确认

在 publisher 模块的 application.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,回调机制。
定义 ReturnCallback

每个 RabbitTemplate 只能配置一个 ReturnCallback,因此我们可以在配置类中统一设置。我们在 publisher 模块定义一个配置类:
image.png
内容如下:

package com.itheima.publisher.config;import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Configuration;import javax.annotation.PostConstruct;@Slf4j
@AllArgsConstructor
@Configuration
public class MqConfig {private final RabbitTemplate rabbitTemplate;@PostConstructpublic void init(){rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {@Overridepublic void returnedMessage(ReturnedMessage returned) {log.error("触发return callback,");log.debug("exchange: {}", returned.getExchange());log.debug("routingKey: {}", returned.getRoutingKey());log.debug("message: {}", returned.getMessage());log.debug("replyCode: {}", returned.getReplyCode());log.debug("replyText: {}", returned.getReplyText());}});}
}
定义 ConfirmCallback

由于每个消息发送时的处理逻辑不一定相同,因此 ConfirmCallback 需要在每次发消息时定义。具体来说,是在调用 RabbitTemplate 中的 convertAndSend 方法时,多传递一个参数:
image.png
这里的 CorrelationData 中包含两个核心的东西:

  • id:消息的唯一标示,MQ 对不同的消息的回执以此做判断,避免混淆
  • SettableListenableFuture:回执结果的 Future 对象
    将来 MQ 的回执就会通过这个 Future 来返回,我们可以提前给 CorrelationData 中的 Future 添加回调函数来处理消息回执:
    image.png

我们新建一个测试,向系统自带的交换机发送消息,并且添加 ConfirmCallback

@Test
void testPublisherConfirm() {// 1.创建CorrelationDataCorrelationData cd = new CorrelationData();// 2.给Future添加ConfirmCallbackcd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {@Overridepublic void onFailure(Throwable ex) {// 2.1.Future发生异常时的处理逻辑,基本不会触发log.error("send message fail", ex);}@Overridepublic void onSuccess(CorrelationData.Confirm result) {// 2.2.Future接收到回执的处理逻辑,参数中的result就是回执内容if(result.isAck()){ // result.isAck(),boolean类型,true代表ack回执,false 代表 nack回执log.debug("发送消息成功,收到 ack!");}else{ // result.getReason(),String类型,返回nack时的异常描述log.error("发送消息失败,收到 nack, reason : {}", result.getReason());}}});// 3.发送消息rabbitTemplate.convertAndSend("hmall.direct", "q", "hello", cd);
}

执行结果如下:
image.png
可以看到,由于传递的 RoutingKey 是错误的,路由失败后,触发了 return callback,同时也收到了 ack。
当我们修改为正确的 RoutingKey 以后,就不会触发 return callback 了,只收到 ack。
而如果连交换机都是错误的,则只会收到 nack。
注意
开启生产者确认比较消耗 MQ 性能,一般不建议开启。而且大家思考一下触发确认的几种情况:

  • 路由失败:一般是因为 RoutingKey 错误导致,往往是编程导致
  • 交换机名称错误:同样是编程错误导致
  • MQ 内部故障:这种需要处理,但概率往往较低。因此只有对消息可靠性要求非常高的业务才需要开启,而且仅仅需要开启 ConfirmCallback 处理 nack 就可以了。

2、队列

2.1. 数据持久化

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

  • 交换机持久化
  • 队列持久化
  • 消息持久化
    我们以控制台界面为例来说明。
2.1.1. 交换机持久化

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

2.1.2. 队列持久化

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

2.1.3. 消息持久化

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

说明:在开启持久化机制以后,如果同时还开启了生产者确认,那么 MQ 会在消息持久化以后才发送 ACK 回执,进一步确保消息的可靠性。
不过出于性能考虑,为了减少 IO 次数,发送到 MQ 的消息并不是逐条持久化到数据库的,而是每隔一段时间批量持久化。一般间隔在 100 毫秒左右,这就会导致 ACK 有一定的延迟,因此建议生产者确认全部采用异步方式。

2.2. LazyQueue

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

  • 消费者宕机或出现网络故障
  • 消息发送量激增,超过了消费者处理速度
  • 消费者处理业务发生阻塞
    一旦出现消息堆积问题,RabbitMQ 的内存占用就会越来越高,直到触发内存预警上限。此时 RabbitMQ 会将内存消息刷到磁盘上,这个行为成为 PageOut. PageOut 会耗费一段时间,并且会阻塞队列进程。因此在这个过程中 RabbitMQ 不会再处理新的消息,生产者的所有请求都会被阻塞。

为了解决这个问题,从 RabbitMQ 的 3.6.0 版本开始,就增加了 Lazy Queues 的模式,也就是惰性队列。惰性队列的特征如下:

  • 接收到消息后直接存入磁盘而非内存
  • 消费者要消费消息时才会从磁盘中读取并加载到内存(也就是懒加载)
  • 支持数百万条的消息存储
    而在 3.12 版本之后,LazyQueue 已经成为所有队列的默认格式。因此官方推荐升级 MQ 为 3.12 版本或者所有队列都设置为 LazyQueue 模式。
2.2.1. 控制台配置 Lazy 模式

在添加队列的时候,添加 x-queue-mod=lazy 参数即可设置队列为 Lazy 模式:
image.png

2.2.2. 代码配置 Lazy 模式

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

@Bean
public Queue lazyQueue(){return QueueBuilder.durable("lazy.queue").lazy() // 开启Lazy模式.build();
}

这里是通过 QueueBuilderlazy() 函数配置 Lazy 模式,底层源码如下:
image.png

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

@RabbitListener(queuesToDeclare = @Queue(name = "lazy.queue",durable = "true",arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void listenLazyQueue(String msg){log.info("接收到 lazy.queue的消息:{}", msg);
}
2.2.3. 更新已有队列为 lazy 模式

对于已经存在的队列,也可以配置为 lazy 模式,但是要通过设置 policy 实现。
可以基于命令行设置 policy:

rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues  

命令解读:

  • rabbitmqctl :RabbitMQ 的命令行工具
  • set_policy :添加一个策略
  • Lazy :策略名称,可以自定义
  • "^lazy-queue$" :用正则表达式匹配队列的名字
  • '{"queue-mode":"lazy"}' :设置队列模式为 lazy 模式
  • --apply-to queues:策略的作用对象,是所有的队列

当然,也可以在控制台配置 policy,进入在控制台的 Admin 页面,点击 Policies,即可添加配置:
image.png

3、消费者

有了持久化机制后,那么怎么保证消息在持久化下来之后一定能被消费者消费呢?这里就涉及到消息的消费确认机制。
在 RabbitMQ 中,消费者处理消息成功后可以向 MQ 发送 ack 回执,MQ 收到 ack 回执后才会删除该消息,这样才能确保消息不会丢失。如果消费者在处理消息中出现了异常,那么就会返回 ack 回执,MQ 收到回执之后就会重新投递一次消息,如果消费者一直都没有返回 ACK/NACK 的话,那么他也会在尝试重新投递。

3.1、消费者确认机制

为了确认消费者是否成功处理消息,RabbitMQ 提供了消费者确认机制(Consumer Acknowledgement)。即:当消费者处理消息结束后,应该向 RabbitMQ 发送一个回执,告知 RabbitMQ 自己消息处理状态。回执有三种可选值:

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

一般 reject 方式用的较少,除非是消息格式有问题,那就是开发问题了。因此大多数情况下我们需要将消息处理的代码通过 try catch 机制捕获,消息处理成功时返回 ack,处理失败时返回 nack.
由于消息回执的处理代码比较统一,因此 SpringAMQP 帮我们实现了消息确认。并允许我们通过配置文件设置 ACK 处理方式,有三种模式:

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

修改 consumer 服务的 SpringRabbitListener 类中的方法,模拟一个消息处理的异常:

@RabbitListener(queues = "simple.queue")  
public void listenSimpleQueueMessage(String msg) throws InterruptedException {  log.info("spring 消费者接收到消息:【" + msg + "】");  if (true) {  throw new MessageConversionException("故意的");  }  log.info("消息处理完成");  
}

测试可以发现:当消息处理发生异常时,消息依然被 RabbitMQ 删除了。
我们再次把确认机制修改为 auto:

spring:  rabbitmq:  listener:  simple:  acknowledge-mode: auto # 自动ack

在异常位置打断点,再次发送消息,程序卡在断点时,可以发现此时消息状态为 unacked(未确定状态): image.png 放行以后,由于抛出的是消息转换异常,因此 Spring 会自动返回 reject,所以消息依然会被删除: image.png
我们将异常改为 RuntimeException 类型:

@RabbitListener(queues = "simple.queue")  
public void listenSimpleQueueMessage(String msg) throws InterruptedException {  log.info("spring 消费者接收到消息:【" + msg + "】");  if (true) {  throw new RuntimeException("故意的");  }  log.info("消息处理完成");  
}

在异常位置打断点,然后再次发送消息测试,程序卡在断点时,可以发现此时消息状态为 unacked(未确定状态): image.png 放行以后,由于抛出的是业务异常,所以 Spring 返回 ack,最终消息恢复至 Ready 状态,并且没有被 RabbitMQ 删除: image.png 当我们把配置改为 auto 时,消息处理失败后,会回到 RabbitMQ,并重新投递到消费者。

3.2、失败重试机制

当消费者出现异常后,消息会不断 requeue(重入队)到队列,再重新发送给消费者。如果消费者再次执行依然出错,消息会再次 requeue 到队列,再次投递,直到消息处理成功为止。极端情况就是消费者一直无法执行成功,那么消息 requeue 就会无限循环,导致 mq 的消息处理飙升,带来不必要的压力: image.png

当然,上述极端情况发生的概率还是非常低的,不过不怕一万就怕万一。为了应对上述情况 Spring 又提供了消费者失败重试机制:在消费者出现异常时利用本地重试,而不是无限制的 requeue 到 mq 队列。

修改 consumer 服务的 application. yml 文件,添加内容:

spring:  rabbitmq:  listener:  simple:  retry:  enabled: true # 开启消费者失败重试  initial-interval: 1000ms # 初识的失败等待时长为1秒  multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval  max-attempts: 3 # 最大重试次数  stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

重启 consumer 服务,重复之前的测试。可以发现:

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

结论:

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

3.3、失败处理策略

在之前的测试中,本地测试达到最大重试次数后,消息会被丢弃。这在某些对于消息可靠性要求较高的业务场景下,显然不太合适了。因此 Spring 允许我们自定义重试次数耗尽后的消息处理策略,这个策略是由 MessageRecovery 接口来定义的,它有 3 个不同实现:

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

比较优雅的一种处理方案是 RepublishMessageRecoverer,失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。

1)在 consumer 服务中定义处理失败消息的交换机和队列


@Bean  
public DirectExchange errorMessageExchange(){  return new DirectExchange("error.direct");  
}  
@Bean  
public Queue errorQueue(){  return new Queue("error.queue", true);  
}  
@Bean  
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){  return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");  
}

2)定义一个 RepublishMessageRecoverer,关联队列和交换机

@Bean  
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){  return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");  
}

完整代码如下:


package com.itheima.consumer.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.amqp.rabbit.core.RabbitTemplate;  
import org.springframework.amqp.rabbit.retry.MessageRecoverer;  
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;  
import org.springframework.context.annotation.Bean;  
​  
@Configuration  
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled", havingValue = "true")  
public class ErrorMessageConfig {  @Bean  public DirectExchange errorMessageExchange(){  return new DirectExchange("error.direct");  }  @Bean  public Queue errorQueue(){  return new Queue("error.queue", true);  }  @Bean  public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){  return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");  }  
​  @Bean  public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){  return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");  }  
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/675537.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

鉴源实验室丨汽车入侵检测系统介绍及测试

作者 | 张诏景 上海控安可信软件创新研究院工控网络安全组 来源 | 鉴源实验室 社群 | 添加微信号“TICPShanghai”加入“上海控安51fusa安全社区” 01 入侵检测系统背景 智能网联汽车不再是一个孤立的嵌入式系统了&#xff0c;信息安全问题越来越被重视。国内外发布了多项标…

【深度学习基础】batch、layer normalization区别

文章目录 1 normalization1.1 batch normalization1.1 motivation 2 layer normalizaiton2.1 motivation 3、面试题 2 pooling2.1 pooling的作用2.2 Pooling 层如何进行反向传播和梯度更新&#xff1f;Max Pooling和 Average Pooling的区别&#xff0c;使用场景分别是什么&…

使用quicker进行局域网文件互传

使用了动作&#xff1a;文件服务器 https://getquicker.net/Sharedaction?code7a49ca6b-d243-4478-1e87-08d9f1ba2358 在文件夹中打开打开这个动作就能使用。 配置 右键动作可以设置&#xff1a; 选择了最后一个之后&#xff0c;打开服务之后能在右下角有一个弹窗&#xff…

[一本Java+一本Java]5月7日简历指导直播

一、直播预告 每周二、四、六16:30简历指导直播&#xff0c;前美团项目负责人、校招VIP CEO-大拿老师在线讲简历优化 二、简历内容 三、简历核心问题 【简历1】一本- Java 1 虽然项目名称有一定的包装&#xff0c;两个项目的内容都是一眼能看出来的烂大街的项目&#xff0c…

【智能优化算法】野狗智能优化算法(Dingo Optimization Algorithm DOA)

野狗智能优化算法(Dingo Optimization Algorithm DOA)是期刊“MATHEMATICAL PROBLEMS IN ENGINEERING”的2021年智能优化算法 01.引言 野狗智能优化算法(Dingo Optimization Algorithm DOA)该算法的灵感来自野狗的狩猎策略&#xff0c;即迫害攻击&#xff0c;分组策略和清除行…

【论文阅读笔记】MAS-SAM: Segment Any Marine Animal with Aggregated Features

1.论文介绍 MAS-SAM: Segment Any Marine Animal with Aggregated Features MAS-SAM&#xff1a;利用聚合特征分割任何海洋动物 Paper Code(空的) 2.摘要 最近&#xff0c;分割任何模型&#xff08;SAM&#xff09;在生成高质量的对象掩模和实现零拍摄图像分割方面表现出卓越…

端侧AI从“芯”开发机会到来,MediaTek举办天玑开发者大会MDDC2024

MDDC2024速览&#xff1a; 发布芯片新品MediaTek天玑9300旗舰5G生成式AI移动芯片、生态发布天玑AI先锋计划、for开发者的生成式AI端侧“天玑AI开发套件”、发布《生成式AI手机产业白皮书》、for游戏的MediaTek星速引擎技术…… MediaTek 5月27日举办天玑开发者大会2024&#xf…

shell脚本编写-测试同一网段内主机是否在线

除了可以使用ansible自动化运维工具判断主机是否在线以外&#xff0c;还可以通过编写Shell脚本来实现。 1、编写脚本 #! /bin/bash #测试192.168.81.0/24网段中哪些主机处于开机状态&#xff0c;哪些主机处于关机状态# #方法一&#xff1a;使用for循环判断 # for i in {1..25…

PyRun_SimpleString(“import cv2“); 报错解决

#include <Python.h> #include <iostream>using namespace std;int main() {Py_Initialize();if (!Py_IsInitialized()){printf("初始化失败&#xff01;");return 0;}PyRun_SimpleString("import sys");PyRun_SimpleString("sys.path.ap…

苍穹外卖项目---------收获以及改进(3-4天)

①公共字段填充----mybatis 第一步&#xff1a;自定义注解 /*** 自定义注解用于标识某个方法需要进行功能字段的填充*/ Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface AutoFill {//枚举&#xff1a;数据库操作类型&#xff1a; update ins…

Redis学习4——Redis应用之限流

引言 Redis作为一个内存数据库其读写速度非常快&#xff0c;并且支持原子操作&#xff0c;这使得它非常适合处理频繁的请求&#xff0c;一般情况下&#xff0c;我们会使用Redis作为缓存数据库&#xff0c;但处理做缓存数据库之外&#xff0c;Redis的应用还十分广泛&#xff0c…

STM32接入CH340芯片的初始化进入升级模式(死机)问题处理

目录 1. 问题描述2. 问题分析2.1 CH340G/K 的初始化波形2.2 第1种USB升级电路2.3 第2种USB升级电路2.4 第3种USB升级电路2.5 第4种USB升级电路 3. 总结 1. 问题描述 我所用的CH340G&#xff08;CH340K也用过&#xff09;接在MCU的电路中&#xff0c;在插入CH340G/K 的接插件&a…