RabbitMQ 高级特性

目录

1. 消息确认

1.1 消息确认机制

1.2 手动确认

1.3 代码示例

1.3.1 AcknowledgeMode.NONE

1.3.2 AcknowledgeMode.AUTO(默认)

1.3.3 AcknowledgeMode.MANUAL

2. 持久性

2.1 交换机持久化

2.2 队列持久化

2.3 消息持久化

3. 发送方确认

3.1 confirm 确认模式

3.2 return 退回模式


1. 消息确认

1.1 消息确认机制

当生产者发送消息到达消费端之后,可能会出现以下情况:

1)消息处理成功

2)消息处理异常

RabbitMQ 向消费者发送消息之后,就会把这条消息删除,那么第二种情况就会造成消息丢失,因此为了保证消息从队列可靠的达到消费者,RabbitMQ 提供了消息确认机制

消费者在订阅队列时,可以指定 autoAck 参数,根据这个参数设置,消息确认机制分为以下两种情况:

1)自动确认

当 autoAck 为 true 时,RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者从磁盘)中删除,而不管消费者是否真正地消费了这些消息,自动确认模式适合对于消息可靠性要求不高的场景

2)手动确认

当 autoAck 为 false 时,RabbitMQ 会等待消费者显式地调用 Basic.Ack 命令,回复确认信号后从内存(或者磁盘)中删去这些消息,这种模式适合对消息可靠性要求比较高地场景

代码示例:

DefaultConsumer consumer = new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.out.println("接收到的消息:" + new String(body));
    }
};
channel.basicConsume(Constant.FANOUT_QUEUE2,true,consumer);

当 autoAck 为 false 时,对于 RabbitMQ 服务端而言,队列中地消息分为两个部分:

1)等待投递给消费者地消息

2)已经投递给消费者,但是还没有收到消费者确认信号地消息

如果 RabbitMQ 一直没有收到消费者地确认信号,并且消费这个消息地消费者已经断开连接,则 RabbitMQ 会安排该消息重新进入队列,等待投递给下一个消费者,也有可能是原来地哪个消费者

从 RabbitMQ 的 web 管理平台上,也可以看到当前队列中 Ready 和 Unacked 状态的消息数

Ready:等待投递给消费者的消息数

Unacked:已经投递给消费者,但是未收到消费者确认信号的消息数

1.2 手动确认

消费者在收到消息之后,可以选择确认,也可以选择直接拒绝或者跳过,RabbitMQ 提供了不同的确认应答的方式,消费者客户端可以调用与其对应的 channel 的相关方法

1)肯定确认:Channel.basicAck(long deliveryTag,boolean multiple)

RabbitMQ 已经知道了该消息并且成功的处理消息,可以将其丢弃了

deliveryTag:消息的唯一标识,deliveryTag 是每个 Channel(通道) 独立维护的,所以在每个通道上都是唯一的,当消费者确认(ack)一条消息时,必须使用对应的通道上进行确认

multiple:是否批量确认,可以对一系列连续的 deliveryTag 进行批量确认,值为 true 则会一次性 ack所有小于或者等于 deliveryTag 的消息,值为 false,则只确认当前指定 deliveryTag 的消息

deliveryTag 是 RabbitMQ 中消息确认机制的一个重要组成部分,它确保了消息传递的可靠性和顺序性

2)否定确认:Channel.basicReject(long deliveryTag,boolean requeue)

deliveryTag:同上

requeue:表示拒绝后,这条消息如何处理,如果 requeue 参数设置为 true,则 RabbitMQ 会重新将这条消息存入队列,以便可以额发送给下一个订阅的消费者,如果设置为 false,则 RabbitMQ 会把消息东队列中移除,而不把它发送给新的消费者

3)否定确认:Channel.basicNack(long deliveryTag,boolean multiple,boolean requeue)

Basic.Reject 命令一次只能拒绝一条消息,想要批量拒绝消息,使用 Basic.Nack

multiple:设置为 true,表示拒绝 deliveryTag 编号之前所有未被当前消费者确认的消息

1.3 代码示例

基于 SpringBoot 来演示消息的确认机制,Spring—AMQP 对消息确认机制提供了三种策略

1)AcknowledgeMode.NONE

这种模式下,消息⼀旦投递给消费者,不管消费者是否成功处理了消息,RabbitMQ 就会自动确认
消息,从 RabbitMQ 队列中移除消息,如果消费者处理消息失败,消息可能会丢失

消费者正确处理:MQ 删除相应的消息

消费者异常处理:MQ 删除相应的消息

2)AcknowledgeMode.AUTO(默认)

这种模式下,消费者在消息处理成功时会⾃动确认消息,但如果处理过程中抛出了异常,则不会确
认消息

消费者正确处理:消息自动确认

消费者异常处理:消息会不停的重试

3)AcknowledgeMode.MANUAL

手动确认模式下,消费者必须在成功处理消息后显式调用 basicAck 方法来确认消息,如果消息未被确认,RabbitMQ 会认为消息尚未被成功处理,并且会在消费者可用时重新投递该消息,这种模式提高了消息处理的可靠性,因为即使消费者处理消息后失败,消息也不会丢失,而是可以被重新处理

1.3.1 AcknowledgeMode.NONE

1)配置确认机制

spring:
  rabbitmq:
    addresses: amqp://lk:lk@44.34.51.65:5672/order
    listener:
      simple:
        acknowledge-mode: none

2)发送消息

队列,交换机配置

public class Constants {
    public static final String ACK_EXCHANGE = "ack.exchange";
    public static final String ACK_QUEUE = "ack.queue";
}
@Configuration
public class RabbitMQConfig {
    //消息确认
    @Bean("ackQueue")
    public Queue ackQueue() {
        return QueueBuilder.durable(Constants.ACK_QUEUE).build();
    }
    @Bean("directExchange")
    public DirectExchange directExchange() {
        return ExchangeBuilder.directExchange(Constants.ACK_EXCHANGE).build();
    }
    @Bean("ackBinding")
    public Binding ackBinding(@Qualifier("directExchange") DirectExchange directExchange, @Qualifier("ackQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(directExchange).with("ack");
    }
}

通过接口发消息

@RequestMapping("/producer")
@RestController
public class ProducerController {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @RequestMapping("/ack")
    public String ack() {
        rabbitTemplate.convertAndSend(Constants.ACK_EXCHANGE,"ack","consumer ack test");
        return "发送成功";
    }
}

3)消费端逻辑

@Component
public class AckListener {
    @RabbitListener(queues = Constants.ACK_QUEUE)
    public void ListenerQueue(Message message, Channel channel) throws Exception {
        System.out.printf("接收到消息: %s,deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
                message.getMessageProperties().getDeliveryTag());
        //模拟处理失败
        int mum = 1 / 0;
        System.out.println("处理完成");
    }
}

先把消费者注掉,观察结果

开启消费者

 

可以看到,消费者处理失败了,但是消息已经从 RabbitMQ 中移除了 

1.3.2 AcknowledgeMode.AUTO(默认)

1)配置确认机制

spring:
  rabbitmq:
    addresses: amqp://lk:lk@44.34.51.65:5672/order
    listener:
      simple:
        acknowledge-mode: auto

2)重新运行程序

先注掉消费者

开启消费者

从日志上可以看出,当消费者出现异常,RabbitMQ 会不断的重发,由于异常,多次重试还是失败,消息没被确认,也无法 ACK,就一致处于 Unacked 状态,导致消息积压

1.3.3 AcknowledgeMode.MANUAL

1)配置确认机制

spring:
  rabbitmq:
    addresses: amqp://lk:lk@44.34.51.65:5672/order
    listener:
      simple:
        acknowledge-mode: manual

2)消费端手动确认逻辑

@Component
public class AckListener {
    @RabbitListener(queues = Constants.ACK_QUEUE)
    public void ListenerQueue(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.printf("接收到消息: %s,deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
                    message.getMessageProperties().getDeliveryTag());
            System.out.println("处理完成");
            //手动确认
            channel.basicAck(deliveryTag,false);
        }catch (Exception e) {
            //否定确认
            //第三个参数 requeue,是否重新发送,为 true,则会重新发送,为 false,直接丢弃
            channel.basicNack(deliveryTag,false,true);
        }

    }
}

上述代码运行结果是正常的,运行后消息会被签收

3)异常时拒绝签收

@Component
public class AckListener {
    @RabbitListener(queues = Constants.ACK_QUEUE)
    public void ListenerQueue(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.printf("接收到消息: %s,deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
                    message.getMessageProperties().getDeliveryTag());
            System.out.println("处理完成");
            //设置异常
            int sum = 1 / 0;
            //手动确认
            channel.basicAck(deliveryTag,false);
        }catch (Exception e) {
            //否定确认
            //第三个参数 requeue,是否重新发送,为 true,则会重新发送,为 false,直接丢弃
            channel.basicNack(deliveryTag,false,true);
        }

    }
}

可以看到,消息异常时不断重试,deliveryTag 从 1 递增

可以看到即使消息未被确认,消息也不会丢失 

总结:

1)AcknowledgeMode.NONE

不管消息是正常还是异常处理,MQ 都会删除相应的消息

2)AcknowledgeMode.AUTO(默认)

消息被正常处理,消息会自动确认,被异常处理时,消息会不停重试

3)AcknowledgeMode.MANUAL

消息被正常处理时,消息会被确认,被异常处理时,消息会重新入队列,消息不会丢失

2. 持久性

前面在消费端处理消息的时候,保证了消息的不丢失,但是当 RabbitMQ 服务器停掉之后,生产者发送的消息会不会丢失呢

RabbitMQ 的持久化分为 3 个部分:交换机持久化、队列持久化、消息持久化

2.1 交换机持久化

交换机持久化时通过声明交换机时将 durable 参数设置为 true 来实现,当 MQ 的服务器发生意外或者关闭之后,重启 RabbitMQ 时不需要重新建立交换机,交换机会自动创建,相当于一直存在

如果交换器不设置持久化,那么在 RabbitMQ 服务重启之后,相关的交换机元数据会丢失,对⼀个长期使用的交换器来说,建议将其置为持久化的

ExchangeBuilder. topicExchange (Constant. ACK_EXCHANGE_NAME ).durable(true).build() ;

2.2 队列持久化

队列的持久化是在声明队列时将 durable 参数设置为 true 来实现,如果队列不设置持久化,那么在 RabbitMQ 服务重启之后,该队列就会被删掉,此时数据也会丢失(队列没有了,消息也就没有了)

队列的持久化能保证队列本身的元数据不会因为异常情况而丢失,但是并不能保证内部所存储的消息不会丢失,因此要确保消息不会丢失,需要将消息设置为持久化

QueueBuilder. durable (Constant. ACK_QUEUE ).build();

从源码中可以看到队列默认是持久化的

通过下面代码,可以创建非持久化队列

QueueBuilder. nonDurable (Constant. ACK_QUEUE ).build();

2.3 消息持久化

 要实现消息持久化,需要把消息的投递模式( MessageProperties 中的 deliveryMode)设置为 2,也就是 MessageDeliveryMode. PERSISTENT

public enum MessageDeliveryMode {
    NON_PERSISTENT , //⾮持久化
    PERSISTENT ; //持久化
}

设置了队列和消息持久化,当 RabbitMQ 服务重启之后,消息依然存在,如果只设置队列持久化,重启之后消息还会丢失,如果只设置消息持久化,重启之后队列消息,从而消息也丢失,因此消息和队列都要设置持久化

//⾮持久化信息
channel.basicPublish("", QUEUE_NAME ,null,msg.getBytes());
//持久化信息
channel.basicPublish("", QUEUE_NAME ,MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());

MessageProperties.PERSISTENT_TEXT_PLAIN 实际就是封装了这个属性

public static final BasicProperties PERSISTENT_TEXT_PLAIN =
    new BasicProperties("text/plain",
            null,
            null,
            2, //deliveryMode
            0, null, null, null,
            null, null, null, null,
            null, null);

如果想使用 RabbitTemplate 发送持久化消息,代码如下

// 要发送的消息内容
String message = "This is a persistent message";
// 创建⼀个Message对象,设置为持久化
Message messageObject = new Message(message.getBytes(),new MessageProperties());
messageObject.getMessageProperties().setDeliveryMode(MessageDeliveryMode. PERSIS
TENT );
// 使⽤RabbitTemplate发送消息
rabbitTemplate.convertAndSend(Constant. ACK_EXCHANGE_NAME , "ack",
messageObject);

RabbitMQ 默认情况下都会将消息视为持久化的

3. 发送方确认

在使用 RabbitMQ 的时候,可以通过消息持久化来解决因为服务器的异常崩溃而导致的消息丢失,当消息的生产者将消息发送出去之后,如果在消息到达服务器之前已经丢失(比如 RabbitMQ 重启,在重启期间生产者消息投递失败),持久化操作不能解决这个问题,因为消息根本到达服务器

RabbitMq 提供了发送方确认机制,RabbitMQ 提供了两个方式来控制消息的可靠性传输

1)confirm 确认模式

2)return 退回模式

3.1 confirm 确认模式

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

1)配置 RabbitMQ

spring:
  rabbitmq:
    addresses: amqp://lk:lk@44.34.51.65:5672/order
    listener:
      simple:
        acknowledge-mode: manual
    publisher-confirm-type: correlated #消息发送确认

2)设置确认回调逻辑并发送消息

无论消息确认成功还是失败,都会调用 ConfirmCallback 的 confirm 方法,如果消息成功发送到 Broker,ack 为 true,失败为 false,并且提供原因

//发送方确认
    public static final String CONFIRM_QUEUE = "confirm.queue";
    public static final String CONFIRM_EXCHANGE = "confirm.exchange";
//发送方确认
    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable(Constants.CONFIRM_QUEUE).build();
    }
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        return ExchangeBuilder.directExchange(Constants.CONFIRM_EXCHANGE).build();
    }
    @Bean("confirmBinding")
    public Binding confirmBinding(@Qualifier("confirmExchange") Exchange exchange,@Qualifier("confirmQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("confirm").noargs();
    }
    @RequestMapping("/confirm")
    public String confirm() {
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                //correlationData:发送消息时附加信息,通常用于在确认回调中识别特定消息
                //ack:交换机收到消息为 true
                //cause:消息确认失败时的原因
                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);
                }
            }
        });
        CorrelationData correlationData = new CorrelationData("1");
        rabbitTemplate.convertAndSend(Constants.CONFIRM_EXCHANGE,"confirm","confirm test",correlationData);
        return "发送成功";
    }

测试程序,观察结果

修改交换机名称,重新运行程序

消息未到达交换机,ack 为 false 

再次通过接口发送消息

只能有一个 confirmCallback 来支持每一个 RabbitTemplate

这种方式设置 confirmCallback 影响所有使用 RabbitTemplat 的方法,重复调用接口时,会提示错误

当我们代码内部里面创建了  RabbitTemplat,Spring 在启动时会使用我们创建的 RabbitTemplat,如果没有,就会使用 Spring 框架创建

解决方法:可以单独写一个类,自己创建 RabbitTemplat,在注入的时候选择名称注入,就可以使用到自己创建的 RabbitTemplat

@Configuration
public class RabbitTemplateConfig {
    @Bean
    public RabbitTemplate configRabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                //correlationData:发送消息时附加信息,通常用于在确认回调中识别特定消息
                //ack:交换机收到消息为 true
                //cause:消息确认失败时的原因
                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;
    }
}
    @Autowired
    private RabbitTemplate configRabbitTemplate;
   
    @RequestMapping("/confirm")
    public String confirm() {
        CorrelationData correlationData = new CorrelationData("1");
        rabbitTemplate.convertAndSend(Constants.CONFIRM_EXCHANGE,"confirm","confirm test",correlationData);
        return "发送成功";
    }

此时上述的问题就可以解决了

3.2 return 退回模式

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

1)配置 RabbitMQ

spring:
  rabbitmq:
    addresses: amqp://lk:lk@44.34.51.65:5672/order
    listener:
      simple:
        acknowledge-mode: manual
    publisher-confirm-type: correlated #消息发送确认

2)设置返回回调逻辑并发送消息

    @RequestMapping("/confirm")
    public String confirm() {
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returned) {
                System.out.printf("消息被退回: %s",returned);
            }
        });
        rabbitTemplate.convertAndSend(Constants.CONFIRM_EXCHANGE,"confirm111","return test");
        return "发送成功";
    }

此时设置为 routingKey 是错误的,观察结果

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

  • 14
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值