RabbitMQ_6_高级特性(3)

发送方确认

在使用RabbitMQ的时候,可以通过信息持久化来解决因为服务器的异常崩溃导致的消息丢失,但是还有一个问题,当消息的生产者将消息发送出去之后,消息到底有没有正确地到达服务器呢?如果在消息到达服务器之前已经丢失(比如RabbitMQ重启,那么RabbitMQ重启期间生产者消息投递失败),持久化操作也解决不了这个问题,因为消息根本没有到达服务器,何谈持久化?

RabbitMQ为我们提供了两种解决方案:

  1. 通过事务机制实现
  2. 通过发送方确认机制实现

事务机制比较消耗性能,在实际工作中使用也不多,咱们主要介绍confirm机制来实现发送方的确认。RabbitMQ为我们提供了两个方式来控制消息的可靠性投递:

  1. confirm确认模式
  2. return退回模式

这两种模式之间并不冲突,它们是可以单独使用,也可以一起使用,confirm确认模式用于Producer和Exchange之间,在Exchange收到消息后进行确认;而return退回模式用于Exchange和Queue之间,当Exchange路由的Queue不存在时,此时,消息就会被退回了......

confirm确认模式

Producer在发送消息的时候,对发送端设置一个ConfirmCallback,无论消息是否到达Exchange,这个监听都会被执行,如果Exchange成功收到,ACK为true;如果没收到消息,ACK就为false。

下面我们通过代码来演示,步骤如下:

  1. 配置RabbitMQ
  2. 设置确认回调逻辑并发送消息
  3. 测试
配置RabbitMQ
bash 复制代码
spring:
  application:
    name: rabbit-extensions-demo
    #配置RabbitMQ的基本信息
    #amqp://username:password@Ip:port/virtual-host
  rabbitmq:
        addresses: amqp://admin:admin@106.52.188.165:5672/extension
        listener:
          simple:
#            acknowledge-mode: none  #消息接收确认
#             acknowledge-mode: auto
             acknowledge-mode: manual
        publisher-confirm-type: correlated #消息发送确认
声明队列、交换机和绑定关系

常量类:

java 复制代码
    //发送方确认
    public static  final  String CONFIRM_QUEUE = "confirm.queue";
    public static  final  String CONFIRM_EXCHANGE = "confirm.exchange";

声明队列、交换机和绑定关系:

java 复制代码
     //发送方确认
    @Bean("confirmQueue")
    public Queue confirmQueue(){
        return QueueBuilder.durable(Contants.CONFIRM_QUEUE).build();
    }

    @Bean("confirmExchange")
    public DirectExchange confirmExchange(){
        return ExchangeBuilder.directExchange(Contants.CONFIRM_EXCHANGE).build();
    }

    @Bean("confirmBinding")
    public Binding confirmBinding(@Qualifier("confirmQueue")Queue queue,
    @Qualifier("confirmExchange")DirectExchange confirmExchange){
        return BindingBuilder.bind(queue).to(confirmExchange).with("confirm");
    }
Controller:
java 复制代码
 @RequestMapping("/confirm")
    public String confirm() {
        CorrelationData correlationData = new CorrelationData("1");
        confirmRabbitTemplate.convertAndSend(Contants.CONFIRM_EXCHANGE, "confirm", "confirm test", correlationData);
        return "消息发送成功";
    }
设置确认回调逻辑

无论消息确认成功还是失败,都会调用ComfirmCallback的confirm方法。如果消息成功发送到broker,ack为true。

如果消息发送失败,ack为false,并且cause提前失败的原因。

java 复制代码
    @Bean("confirmRabbitTemplate")
    public  RabbitTemplate confirmRabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        //设置消息确认的回调方法       发送方-》交换机
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                //1、这种方式设置confirmcallback影响所有使用RabbitTemplate的方法
                //2、重复调用接口时会显示错误
                System.out.println("执行了confirm方法");
                if (ack) {
                    System.out.printf("接收到消息,消息Id:%s\n", correlationData == null ? null : correlationData.getId());
                } else {
                    System.out.printf("未接收到消息,消息Id:%s,cause:%s\n", correlationData == null ? null : correlationData.getId(), cause);
                    //相应业务处理
                }
            }
        });
        return rabbitTemplate;
    }

当然这个配置也可以配置在Controller的RabbitTemplate中,但是会存在两个问题:

  1. 这种方式设置confirmcallback影响所有使用RabbitTemplate的方法
  2. 重复调用接口时会显示错误

RabbitTemplate.ConfirmCallback和ConfirmListener区别

在RabbitMQ中,ConfirmListener和ConfirmCallback都是用来处理消息确认机制,但它们属于不同的客户端库,并且使用的场景和方式有所不同。

  1. ConfirmListener是RabbitMQ Java Client库中的接口。这个库是RabbitMQ官方提供的一个直接与RabbitMQ服务器交互的客户端库。ConfirmListener接口提供了两个方法:handleAck和handleNack,用于处理消息确认和否定确认的事件
  2. ConfirmCallback是Spring AMQP框架中的一个接口。专门为Spring环境设计。用于简化和RabbitMQ交互的过程。他只包含一个confirm方法,用于处理消息确认的回调。

在Spring Boot应用中,通常会使用ConfirmCallback,因为它与Spring框架的其他部分更加整合,可以利用Spring配置和依赖注入功能。而在使用RabbitMQ Java Client库时,则可能会直接实现ConfirmListener接口,更直接地与RabbitMQ的Channel交互

测试

运行程序,调用接口:127.0.0.1:8080/producer/confirm

观察控制台,消息ack成功:

修改交换机名称,重新调用接口:

java 复制代码
    @RequestMapping("/confirm")
    public String confirm() {
        CorrelationData correlationData = new CorrelationData("1");
        confirmRabbitTemplate.convertAndSend(Contants.CONFIRM_EXCHANGE+1, "confirm", "confirm test", correlationData);
        return "消息发送成功";
    }

运行结果:

return退回模式

消息到达Exchange之后,会根据路由规则匹配,把消息放入Queue中,Exchange到Queue的过程,如果一条消息无法被任何队列消费(即没有队列能和消息的路由键匹配或队列不存在等),可以选择把消息退回给发送者。消息退回给发送者,我们可以设置一个返回回调方法,对消息进行处理。

步骤如下:

  1. 配置RabbitMQ
  2. 设置返回回调逻辑并发送消息
  3. 测试
配置RabbitMQ

这里的配置和Confirm消息确认模式相同:

bash 复制代码
spring:
  application:
    name: rabbit-extensions-demo
    #配置RabbitMQ的基本信息
    #amqp://username:password@Ip:port/virtual-host
  rabbitmq:
        addresses: amqp://admin:admin@106.52.188.165:5672/extension
        listener:
          simple:
#            acknowledge-mode: none  #消息接收确认
#             acknowledge-mode: auto
             acknowledge-mode: manual
        publisher-confirm-type: correlated #消息发送确认
队列、交换机和绑定关系

也是和Confirm消息确认模式相同。

常量:

java 复制代码
   //发送方确认
    public static  final  String CONFIRM_QUEUE = "confirm.queue";
    public static  final  String CONFIRM_EXCHANGE = "confirm.exchange";

队列、交换机和绑定关系:

java 复制代码
    //发送方确认
    @Bean("confirmQueue")
    public Queue confirmQueue(){
        return QueueBuilder.durable(Contants.CONFIRM_QUEUE).build();
    }

    @Bean("confirmExchange")
    public DirectExchange confirmExchange(){
        return ExchangeBuilder.directExchange(Contants.CONFIRM_EXCHANGE).build();
    }

    @Bean("confirmBinding")
    public Binding confirmBinding(@Qualifier("confirmQueue")Queue queue,
    @Qualifier("confirmExchange")DirectExchange confirmExchange){
        return BindingBuilder.bind(queue).to(confirmExchange).with("confirm");
    }
Controller:
java 复制代码
 @RequestMapping("/returns")
    public String returns() {
        CorrelationData correlationData = new CorrelationData("5");
        confirmRabbitTemplate.convertAndSend(Contants.CONFIRM_EXCHANGE, "confirm111", "returns test", correlationData);
        return "消息发送成功";
    }
设置返回回调逻辑

当消息无法被路由到任何队列,它将返回给发送者,这时setReturnCallback设置的回调将被触发。

java 复制代码
 @Bean("confirmRabbitTemplate")
    public  RabbitTemplate confirmRabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        //设置消息确认的回调方法       发送方-》交换机
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                //1、这种方式设置confirmcallback影响所有使用RabbitTemplate的方法
                //2、重复调用接口时会显示错误
                System.out.println("执行了confirm方法");
                if (ack) {
                    System.out.printf("接收到消息,消息Id:%s\n", correlationData == null ? null : correlationData.getId());
                } else {
                    System.out.printf("未接收到消息,消息Id:%s,cause:%s\n", correlationData == null ? null : correlationData.getId(), cause);
                    //相应业务处理
                }
            }
        });
        //消息退回的回调方法
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            //交换机 -》队列
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                System.out.println("消息退回:"+returnedMessage);
            }
        });
        return rabbitTemplate;
    }

使用RabbitTemplate的setMandatory方法设置mandatory属性为true(默认为false)。这个属性的作用是告诉RabbitMQ,如果一条消息无法被任何队列消费,RabbitMQ应该将消息返回给发送者,此时ReturnCallback就会被触发。

回调函数中有一个参数:ReturnedMessage,包含以下属性:

java 复制代码
public class ReturnedMessage {
 //返回的消息对象,包含了消息体和消息属性
 private final Message message;
 //由Broker提供的回复码, 表⽰消息⽆法路由的原因. 通常是⼀个数字代码,每个数字代表不同
的含义. 
 private final int replyCode;
 //⼀个⽂本字符串, 提供了⽆法路由消息的额外信息或错误描述.
 private final String replyText;
 //消息被发送到的交换机名称
 private final String exchange;
 //消息的路由键,即发送消息时指定的键
 private final String routingKey;
}
测试

运行程序,调用接口:127.0.0.1:8080/producer/returns

观察控制台,消息被退回:

常见面试题(如何保证RabbitMQ消息的可靠传输?)

从这个图中,可以看出,消息可能丢失的场景以及解决方案:

  • 1、生产者将消息发送到Exchange失败
  • 可能原因:网络问题等
  • 解决办法:发送方确认中的comfirm确认模式

2、消息在交换机中无法路由到指定队列

  • 可能原因:代码或者配置层面错误,导致消息路由失败
  • 解决办法:发送方确认中的return模式

3、消息队列自身数据丢失

  • 可能原因:消息到达RabbitMQ之后,RabbitMQServer宕机导致消息丢失。
  • 解决办法:持久性机制。开启RabbitMQ持久化(交换机持久化+队列持久化+消息持久化),就是消息写入之后会持久化到磁盘,如果RabbitMQ挂了,恢复之后会自动读取之前存储的数据。(极端情况下,RabbitMQ还未持久化就挂了,可能导致少量数据丢失,这个概率极低,当发送这种情况时,也可以通过集群的方式提高可靠性)

4、消费者异常,导致消息丢失

  • 可能原因:消息到达消费者,还没来得及消费,消费者宕机。消费者逻辑有问题

  • 解决办法:消息确认机制,RabbitMQ提供了消费者应答机制来使RabbitMQ能够感知到消费者是否成功消费消息。默认情况下消费者应答机制是自动应答的,可以开启手动确认,当消费者确认消费成功后才会删除消息,从而避免消息丢失。除此之外,也可以配置重试机制(参考下文),当消息消费异常时,通过消息重试机制确保消息的可靠性

重试机制

之前,我们讲消费者确认机制时,我们讲到RabbitMQ提供的方式:

1、自动确认:消息到达消费者,队列中的消息就被删除了

2、手动确认:消息处理成功后,需要进行ACK

Spring AMQP则是封装了三种方式:

1、NONE:相当于自动确认

2、MANUAL:相当于手动确认

3、AUTO:当消费端没有抛出异常时,就删除队列中的消息;当消费端抛出了异常,就会自动进行重试(如果是网络问题,重试可以解决;代码问题,重试多次也没有效果)

代码演示

AUTO:

配置:

java 复制代码
spring:
  application:
    name: rabbit-extensions-demo
    #配置RabbitMQ的基本信息
    #amqp://username:password@Ip:port/virtual-host
  rabbitmq:
        addresses: amqp://admin:admin@106.52.188.165:5672/extension
        listener:
          simple:
#            acknowledge-mode: none  #消息接收确认
             acknowledge-mode: auto
#             acknowledge-mode: manual
             retry:
              enabled: true # 开启消费者失败重试
              initial-interval: 5000ms # 初始失败等待时长为5秒
              max-attempts: 5 # 最大重试次数
        publisher-confirm-type: correlated #消息发送确认

交换机和队列:

java 复制代码
//重试机制
    public static final String RETRY_QUEUE = "retry.queue";
    public static final String RETRY_EXCHANGE = "retry.exchange";

发送消息:

java 复制代码
@RequestMapping("/retry")
    public String retry(){
        rabbitTemplate.convertAndSend(Contants.RETRY_EXCHANGE,"retry","retry test...");
        return "消息发送成功";

    }

消费消息:

java 复制代码
@Component
public class RetryListener {
    @RabbitListener(queues = Contants.RETRY_QUEUE)
    public void handleMessage(Message message) throws UnsupportedEncodingException {
        long deliverTag = message.getMessageProperties().getDeliveryTag();
        System.out.printf("["+Contants.RETRY_QUEUE+"],接收到消息:%s,deliverTag:%s",new String(message.getBody(),
                "UTF-8"),deliverTag);
        int a = 10 / 0;
        System.out.println("业务处理完成!");
    }
}

调用接口,发送消息:127.0.0.1:8080/producer/retry

可以看到消息包含第一次发送在内,一共被重新发了4次,每隔5秒重发1次:

当我们将配置注掉时,消费者和队列(不一定是消费者获取的,也可能是队列发布的,推拉模式)会不断进行重发:

注意:这里是重发消息,而不是刚才的重试,重试的deliverflag是一样的,而这里的deliverflag是递增的!!

如果我们对异常进行捕获,那么就不会进行重试:

java 复制代码
@RabbitListener(queues = Contants.RETRY_QUEUE)
    public void handleMessage(Message message) throws UnsupportedEncodingException {
        long deliverTag = message.getMessageProperties().getDeliveryTag();
        System.out.printf("["+Contants.RETRY_QUEUE+"],接收到消息:%s,deliverTag:%s",new String(message.getBody(),
                "UTF-8"),deliverTag);
        try{
            int a = 10 / 0;
            System.out.println("业务处理完成!");
        }catch(Exception e){
            System.out.println("业务处理失败!");
        }

    }

重新运行程序,结果如下:

手动确认

配置:

java 复制代码
spring:
  application:
    name: rabbit-extensions-demo
    #配置RabbitMQ的基本信息
    #amqp://username:password@Ip:port/virtual-host
  rabbitmq:
        addresses: amqp://admin:admin@106.52.188.165:5672/extension
        listener:
          simple:
#            acknowledge-mode: none  #消息接收确认
#             acknowledge-mode: auto
             acknowledge-mode: manual
             retry:
              enabled: true # 开启消费者失败重试
              initial-interval: 5000ms # 初始失败等待时长为5秒
              max-attempts: 5 # 最大重试次数
        publisher-confirm-type: correlated #消息发送确认

消费信息:

java 复制代码
@Component
public class RetryListener {
    @RabbitListener(queues = Contants.RETRY_QUEUE)
    public void handleMessage(Message message, Channel channel) throws IOException {
        long deliverTag = message.getMessageProperties().getDeliveryTag();
        System.out.printf("["+Contants.RETRY_QUEUE+"],接收到消息:%s,deliverTag:%s",new String(message.getBody(),
                "UTF-8"),deliverTag);
        try {
            int a = 10 / 0;
            System.out.println("业务处理完成!");
            channel.basicAck(deliverTag,false);
        }catch (Exception e){
            channel.basicNack(deliverTag,false,true);
        }
    }
}

因为刚才的消息没有被消费,所以这里直接用刚才的消息测试即可。

运行结果:

可以看到,手动确认模式时,重发次数限制不会像在AUTO模式下直接生效,因为是否重试以及何时重试更多地取决于应用程序的逻辑和消费者的实现。

自动确认模式下,RabbitMQ会在消息投递给消费者后自动确认消息。如果消费者处理消息时抛出异常,RabbitMQ根据配置的重试参数自动将消息重新入队,从而实现重试。重试次数和重试间隔等参数可以直接在RabbitMQ的配置中设定,并且RabbitMQ会负责执行这些重试策略。

手动模式下,消费者需要显式地对消息进行确认。如果消费者在处理信息时遇到异常,可以选择不确认消息使消息可以重新入队。重试的控制权在于应用程序本身,而不是RabbitMQ的内部机制。应用程序可以通过自己的逻辑和利用RabbitMQ的高级特性来实现有效的重试策略。

使用重试机制注意:

1、自动确认模式下:程序逻辑异常,多次重试还是失败,消息就会被自动确认,那么消息就丢失了

2、手动确认模式下:程序逻辑异常,多次重试消息依然处理失败,无法被确认,就一直时unacked的状态,导致消息积压。

相关推荐
初次攀爬者2 天前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
初次攀爬者4 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
让我上个超影吧5 天前
消息队列——RabbitMQ(高级)
java·rabbitmq
塔中妖5 天前
Windows 安装 RabbitMQ 详细教程(含 Erlang 环境配置)
windows·rabbitmq·erlang
断手当码农5 天前
Redis 实现分布式锁的三种方式
数据库·redis·分布式
初次攀爬者5 天前
Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson
redis·分布式·后端
业精于勤_荒于稀5 天前
物流订单系统99.99%可用性全链路容灾体系落地操作手册
分布式
Ronin3055 天前
信道管理模块和异步线程模块
开发语言·c++·rabbitmq·异步线程·信道管理
Asher05095 天前
Hadoop核心技术与实战指南
大数据·hadoop·分布式
凉凉的知识库5 天前
Go中的零值与空值,你搞懂了么?
分布式·面试·go