RabbitMQ保证消息可靠性

项目开发中经常会使用消息队列来完成异步处理、应用解耦、流量控制等功能。虽然消息队列的出现解决了一些场景下的问题,但是同时也引出了一些问题,其中使用消息队列时如何保证消息的可靠性就是一个常见的问题。如果在项目中遇到需要保证消息一定被消费的场景时,如何保证消息不丢失,如何保证消息的可靠性?

一、如何保证 RabbitMQ 的消息可靠性

RabbitMQ 是如何消息传递如下图:

生产者Producer 将消息发送到指定的 交换机Exchange,交换机根据路由规则路由到绑定的 队列Queue 中,最后和消费者建立连接后,将消息推送给 消费者Consumer

那么消息会在哪些环节丢失呢,列出可能出现消息丢失的场景有:

  • 生产者将消息发送到 RabbitMQ Server 异常:可能因为网络问题造成 RabbitMQ 服务端无法收到消息,造成生产者发送消息丢失场景。
  • RabbitMQ Server 中消息在交换机中无法路由到指定队列:可能由于代码层面或配置层面错误导致消息路由到指定队列失败,造成生产者发送消息丢失场景。
  • RabbitMQ Server 中存储的消息丢失:可能因为 RabbitMQ Server 宕机导致消息未完全持久化或队列丢失导致消息丢失等持久化问题,造成 RabbitMQ Server 存储的消息丢失场景。
  • 消费者消费消息异常:可能在消费者接收到消息后,还没来得及消费消息,消费者宕机或故障等问题,造成消费者无法消费消息导致消息丢失的场景。

以上就是 RabbitMQ 可能出现消息丢失的场景,接下来将依次讲解如何避免这些消息丢失的场景问题。

二、接口实现(利用之前的helloworld案例)

2.1. 保证生产者发送消息到 RabbitMQ Server(Exchange)

为了避免因为网络故障或闪断问题导致消息无法正常发送到 RabbitMQ Server 的情况,RabbitMQ 提供了两种方案让生产者可以感知到消息是否正确无误的发送到 RabbitMQ Server中,这两种方案分别是 事务机制发送方确认机制。下面分别介绍一下这两种机制如何实现。

而发送方确认机制可以通过Confirm效果保证消息一定送达到Exchange来实现

2.1.1.事务机制(不推荐,性能不好)

配置类中配置事务管理器

@Configuration
public class RabbitMQConfig {
    /**
     * 配置事务管理器
     */
    @Bean
    public RabbitTransactionManager transactionManager(ConnectionFactory connectionFactory) {
        return new RabbitTransactionManager(connectionFactory);
    }
}

通过添加事务注解 + 开启事务实现事务机制:

/**
 * 消息业务实现类
 */
@Service
public class RabbitMQServiceImpl {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Transactional // 事务注解
    public void sendMessage() {
        // 开启事务
        rabbitTemplate.setChannelTransacted(true);
        // 发送消息
        rabbitTemplate.convertAndSend(RabbitMQConfig.Direct_Exchange, routingKey, message);
    }
}

通过上面的配置即可实现事务机制,执行流程为:在生产者发送消息之前,开启事务,而后发送消息,如果消息发送至 RabbitMQ Server 失败后,进行事务回滚,重新发送。如果 RabbitMQ Server 接收到消息,则提交事务。

可以发现事务机制其实是同步操作,存在阻塞生产者的情况直到 RabbitMQ Server 应答,这样其实会很大程度上降低发送消息的性能,所以一般不会使用事务机制来保证生产者的消息可靠性,而是使用发送方确认机制。

2.1.2.发送方确认机制

在helloworld案例生产者代码添加如下内容:

        //4.开启confirms
        channel.confirmSelect();

        //5. 设置confirms的异步回调
        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("消息成功的发送到了Exchange!");
            }

            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("消息未能成功的发送到了Exchange,需重试,或者保存到数据库中或者做其他的操作");
            }
        });

2.2. 保证消息可以路由到Queue

采用Return机制,保证Exchange上的消息一定可以送达到Queue

import com.augus.util.RabbitMQConnectionUtil;
import com.rabbitmq.client.*;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Publisher {
    //声明了一个名为 QUEUE_NAME 的常量,用于指定要发送消息的队列名称。
    public static final String QUEUE_NAME = "hello";

    @Test
    public void publish() throws IOException, TimeoutException {
        //1.获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2.构建channel
        Channel channel = connection.createChannel();

        //3.构建队列
        /**
         * QUEUE_NAME:要声明的队列的名称。在这段代码中,使用了常量 QUEUE_NAME,它指定了队列的名称为 "hello"。
         * durable:指定队列是否是持久化的。如果设置为 true,则表示队列将在服务器重启后保留下来。如果设置为 false,则表示队列是非持久化的,服务器重启后队列将被删除。在这段代码中,设置为 false,表示队列是非持久化的。
         * exclusive:指定队列是否是排他的。如果设置为 true,则表示只有声明该队列的连接可以使用该队列。其他连接将无法访问该队列。在这段代码中,设置为 false,表示队列不是排他的。
         * autoDelete:指定队列是否是自动删除的。如果设置为 true,则表示当最后一个消费者断开连接后,队列将被自动删除。在这段代码中,设置为 false,表示队列不会自动删除。
         * arguments:指定队列的其他属性。在这段代码中,设置为 null,表示没有指定其他属性。
         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        //4.开启confirms
        channel.confirmSelect();

        //5. 设置confirms的异步回调
        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("消息成功的发送到了Exchange!");
            }

            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("消息未能成功的发送到了Exchange,需重试,或者保存到数据库中或者做其他的操作");
            }
        });

        //6. 设置Return回调,确认消息是否路由到了Queue
        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消息没有路由到指定的队列");
            }
        });

        //7. 在发送消息时,将basicPublish方法参数中的mandatory设置为true,即可开启Return机制,当消息没有路由到队列中时,就会执行return回调

        //4.发布消息
        String message = "你好,明天去哪里";

        /**
         * "":指定消息要发送到的交换机名称。在这里,空字符串表示使用默认的交换机。默认的交换机会将消息直接发送到指定的队列中。
         * QUEUE_NAME:指定消息要发送到的队列名称。在这段代码中,使用了常量 QUEUE_NAME,它指定了要发送消息的队列名称为 "hello"。
         * null:指定消息的额外属性。在这里,设置为 null,表示没有指定额外的属性。
         * message.getBytes():指定要发送的消息内容。message 是一个字符串,通过调用 getBytes() 方法将其转换为字节数组进行发送。
         */
        channel.basicPublish("",QUEUE_NAME,true,null,message.getBytes());
        System.out.println("消息发送成功!");
    }
}

2.3. 保证Queue可以持久化消息

队列消息在RabbitMQ服务重启后,就会丢失,所以采用可以DeliveryMode设置消息持久化,而DeliveryMode设置为2代表持久化,如果设置为1,就代表不会持久化。下面代码第八步如下:

public class Publisher {
    //声明了一个名为 QUEUE_NAME 的常量,用于指定要发送消息的队列名称。
    public static final String QUEUE_NAME = "hello";

    @Test
    public void publish() throws IOException, TimeoutException {
        //1.获取连接对象
        Connection connection = RabbitMQConnectionUtil.getConnection();

        //2.构建channel
        Channel channel = connection.createChannel();

        //3.构建队列
        /**
         * QUEUE_NAME:要声明的队列的名称。在这段代码中,使用了常量 QUEUE_NAME,它指定了队列的名称为 "hello"。
         * durable:指定队列是否是持久化的。如果设置为 true,则表示队列将在服务器重启后保留下来。如果设置为 false,则表示队列是非持久化的,服务器重启后队列将被删除。在这段代码中,设置为 false,表示队列是非持久化的。
         * exclusive:指定队列是否是排他的。如果设置为 true,则表示只有声明该队列的连接可以使用该队列。其他连接将无法访问该队列。在这段代码中,设置为 false,表示队列不是排他的。
         * autoDelete:指定队列是否是自动删除的。如果设置为 true,则表示当最后一个消费者断开连接后,队列将被自动删除。在这段代码中,设置为 false,表示队列不会自动删除。
         * arguments:指定队列的其他属性。在这段代码中,设置为 null,表示没有指定其他属性。
         */
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);//参数2设置为true,服务重启才会保存队列

        //4.开启confirms
        channel.confirmSelect();

        //5. 设置confirms的异步回调
        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("消息成功的发送到了Exchange!");
            }

            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("消息未能成功的发送到了Exchange,需重试,或者保存到数据库中或者做其他的操作");
            }
        });

        //6. 设置Return回调,确认消息是否路由到了Queue
        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消息没有路由到指定的队列");
            }
        });

        //7. 在发送消息时,将basicPublish方法参数中的mandatory设置为true,即可开启Return机制,当消息没有路由到队列中时,就会执行return回调

        //8.设置消息持久化,然后消息发送的时候传递过去
        AMQP.BasicProperties props = new AMQP.BasicProperties().builder().deliveryMode(2).build();

        //4.发布消息
        /**
         * 第一个参数 "" 是用于指定消息的交换机名称。在这里,空字符串表示使用默认的交换机。
         * 第二个参数 QUEUE_NAME 是指定消息的队列名称。这个参数表示将消息发送到名为 QUEUE_NAME 的队列中。
         * 第三个参数 true 是用于指定消息是否需要持久化。如果设置为 true,则消息将被持久化到磁盘,即使 RabbitMQ 服务器重启也不会丢失。
         * 第四个参数 props 是用于设置消息的属性。这里没有提供具体的代码,但通常可以使用 BasicProperties 类来设置消息的属性,例如设置消息的优先级、过期时间等。
         * 第五个参数 "你好,明天去哪里".getBytes() 是消息的内容。在这里,将字符串 "你好,明天去哪里" 转换为字节数组作为消息的内容。
         */
        channel.basicPublish("",QUEUE_NAME,true,props,"你好,明天去哪里".getBytes());
        System.out.println("消息发送成功!");
    }
}

2.4.测试

先启动生产者,发送消息到队列。发现执行了 handleAck

查看rabbitMQ,如下:

 重启RabbitMQ,再次查看,数据依然在:

2.5.保证消费者可以正常消费消息

保证消费者可以正常消费,让消费者关闭自动ack,并且设置消息的流控,最终实现消费者可以尽可能去多消费消息,详情看WorkQueue模式

三、SpringBoot整合RabbitMQ实现

3.1. 保证生产者发送消息到 RabbitMQ Server(Exchange)

实现方式:通过发送方确认机制保证生产者消息发送到交换机

3.1.1.配置文件

使用Confirm机制,在application.yaml中添加配置:

spring:
  rabbitmq:
    publisher-confirm-type: correlated  # 开启发送方确认机制

配置属性有三种分别为:

  • none:表示禁用发送方确认机制
  • correlated:表示开启发送方确认机制
  • simple:表示开启发送方确认机制,并支持 waitForConfirms() 和 waitForConfirmsOrDie() 的调用。

这里一般使用 correlated 开启发送方确认机制即可,至于 simple 的 waitForConfirms() 方法调用是指串行确认方法,即生产者发送消息后,调用该方法等待 RabbitMQ Server 确认,如果返回 false 或超时未返回则进行消息重传。由于串行性能较差,这里一般都是用异步 confirm 模式。

3.1.2.通过调用 setConfirmCallback() 实现异步 confirm 模式感知消息发送结果

生产者代码如下:
import com.augus.config.RabbitMQConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;

@SpringBootTest
public class PublisherTest {
    @Autowired
    public RabbitTemplate rabbitTemplate;
@Test public void publishWithConfirm() throws IOException { /** * rabbitTemplate.setConfirmCallback() 方法用于设置 RabbitTemplate 的确认回调函数,该函数在消息发送到交换机后被调用。 * 参数说明: * ConfirmCallback:确认回调接口,需要实现 RabbitTemplate.ConfirmCallback 接口,并重写其中的 confirm() 方法。 * correlationData:关联数据,用于关联消息和回调结果。 * ack:消息是否成功发送到交换机,true 表示成功,false 表示失败。 * cause:失败原因,如果消息发送失败,该参数会包含失败的原因。 * 作用说明: * 当消息成功发送到交换机时,ack 参数为 true,表示消息成功发送到了交换机,可以进行相应的处理。 * 当消息未能送达到交换机时,ack 参数为 false,表示消息发送失败,需要进行相应的处理,可以根据 cause 参数获取失败的原因进行后续处理 */ rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { if(ack){ System.out.println("消息成功发送到了交换机"); }else { System.out.println("消息未能送达到Exchange,需要做后续处理!!!"); } } }); //将消息 "你好,国庆节去哪里玩呢?" 发送到名为 EXCHANGE 的交换机,并使用路由键 "topic.black.dog" 进行消息路由。根据交换机和队列的绑定关系,消息将被路由到与该路由键匹配的队列中,供消费者进行消费。 rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE,"topic.black.dog","你好,张飒,明天有空吗?"); System.out.println("消息发送成功"); System.in.read(); } }

生产者发送消息后通过调用 setConfirmCallback() 可以将信道设置为 confirm 模式,所有消息会被指派一个消息唯一标识,当消息被发送到 RabbitMQ Server 后,Server 确认消息后生产者会回调设置的方法,从而实现生产者可以感知到消息是否正确无误的投递,从而实现发送方确认机制。并且该模式是异步的,发送消息的吞吐量会得到很大提升。

上面就是发送放确认机制的配置和使用,使用这种机制可以保证生产者的消息可靠性投递,并且性能较好。

3.2. 保证消息能从交换机路由到指定队列

在确保生产者能将消息投递到交换机的前提下,RabbitMQ 同样提供了消息投递失败的策略配置来确保消息的可靠性,接下来通过配置来介绍一下消息投递失败的策略。

3.2.1.在application.yaml中添加如下内容:

在之前的内容上添加即可

spring:
  rabbitmq:
    publisher-confirm-type: correlated  # 开启发送方确认机制
    publisher-returns: true   # 该配置项设置是否要求消息发布者返回结果。当设置为true时,表示消息发布者必须返回结果,以确认消息是否成功发布。当设置为false时,表示消息发布者不需要返回结果,消息发布后不会进行确认。
    template:
      mandatory: true     # 设置模板是否为必选项。当设置为true时,表示模板是必选的,即消息中必须包含该模板。当设置为false时,表示模板是可选的,消息中可以不包含该模板。

mandatory 分为 true 失败后返回客户端false 失败后自动删除两种策略。显然设置为 false 无法保证消息的可靠性。到这里的配置是可以保证生产者发送消息的可靠性投递。

3.2.2.设置路由失败后的回调方法

通过调用 setReturnCallback() 方法设置路由失败后的回调方法:

import com.augus.config.RabbitMQConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;

@SpringBootTest
public class PublisherTest {
    @Autowired
    public RabbitTemplate rabbitTemplate;

    @Test
    public void publishWithConfirm() throws IOException {
        /**
         * rabbitTemplate.setConfirmCallback() 方法用于设置 RabbitTemplate 的确认回调函数,该函数在消息发送到交换机后被调用。
         * 参数说明:
         *      ConfirmCallback:确认回调接口,需要实现 RabbitTemplate.ConfirmCallback 接口,并重写其中的 confirm() 方法。
         *      correlationData:关联数据,用于关联消息和回调结果。
         *      ack:消息是否成功发送到交换机,true 表示成功,false 表示失败。
         *      cause:失败原因,如果消息发送失败,该参数会包含失败的原因。
         * 作用说明:
         *      当消息成功发送到交换机时,ack 参数为 true,表示消息成功发送到了交换机,可以进行相应的处理。
         *      当消息未能送达到交换机时,ack 参数为 false,表示消息发送失败,需要进行相应的处理,可以根据 cause 参数获取失败的原因进行后续处理
         */
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if(ack){
                    System.out.println("消息成功发送到了交换机");
                }else {
                    System.out.println("消息未能送达到Exchange,需要做后续处理!!!");
                }
            }
        });

        //设置路由失败回调方法: 新版本用 setReturnsCallback ,老版本用setReturnCallback
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             * MQ没有将消息投递给指定的队列回调方法
             * @param message 投递失败的消息详细信息
             * @param replyCode 回复的状态码
             * @param replyText 回复的文本内容
             * @param exchange 消息发给哪个交换机
             * @param routingKey 消息用哪个路邮键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("消息:"+message+"路由队列失败,后续继续重试");
            }
        });

        //将消息 "你好,国庆节去哪里玩呢?" 发送到名为 EXCHANGE 的交换机,并使用路由键 "topic.black.dog" 进行消息路由。根据交换机和队列的绑定关系,消息将被路由到与该路由键匹配的队列中,供消费者进行消费。
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE,"topic.black.dog","你好,张飒,明天有空吗?");
        System.out.println("消息发送成功");

        System.in.read();
    }

}

通过调用 setReturnCallback() 方法即可实现当交换机路由到指定队列失败后回调方法,拿到被退回的消息信息,进行相应的处理如记录日志或重传等等。

3.3. 保证消息在 RabbitMQ Server 中的持久化

对于交换机的持久化,可以使用durable(true)方法来进行设置

    /**
     * 创建交换机
     * @return
     */
    @Bean
    public Exchange bootExchange(){
        //创建交换机 Topic, 需要指定交换机名称
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }

通过QueueBuilder.durable(QUEUE_NAME).build()创建的队列是持久化的

    /**
     * 创建队列
     * @return
     */
    @Bean
    public Queue bootQueue(){
        //指定队列名称
        return QueueBuilder.durable(QUEUE_NAME).build();
    }

对于消息的持久化,只需要在发送消息时将消息持久化,配置如下:

package com.augus;

import com.augus.config.RabbitMQConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;

@SpringBootTest
public class PublisherTest {
    @Autowired
    public RabbitTemplate rabbitTemplate;

    @Test
    public void publishWithConfirm() throws IOException {
        /**
         * rabbitTemplate.setConfirmCallback() 方法用于设置 RabbitTemplate 的确认回调函数,该函数在消息发送到交换机后被调用。
         * 参数说明:
         *      ConfirmCallback:确认回调接口,需要实现 RabbitTemplate.ConfirmCallback 接口,并重写其中的 confirm() 方法。
         *      correlationData:关联数据,用于关联消息和回调结果。
         *      ack:消息是否成功发送到交换机,true 表示成功,false 表示失败。
         *      cause:失败原因,如果消息发送失败,该参数会包含失败的原因。
         * 作用说明:
         *      当消息成功发送到交换机时,ack 参数为 true,表示消息成功发送到了交换机,可以进行相应的处理。
         *      当消息未能送达到交换机时,ack 参数为 false,表示消息发送失败,需要进行相应的处理,可以根据 cause 参数获取失败的原因进行后续处理
         */
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if(ack){
                    System.out.println("消息成功发送到了交换机");
                }else {
                    System.out.println("消息未能送达到Exchange,需要做后续处理!!!");
                }
            }
        });

        /**设置路由失败回调方法: 新版本用 setReturnsCallback ,老版本用setReturnCallback
         *
         *
         */
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             * MQ没有将消息投递给指定的队列回调方法
             * @param message 投递失败的消息详细信息
             * @param replyCode 回复的状态码
             * @param replyText 回复的文本内容
             * @param exchange 消息发给哪个交换机
             * @param routingKey 消息用哪个路邮键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("消息:"+message+"路由队列失败,后续继续重试");
            }
        });

        /**
         * 使用 RabbitMQ 的 RabbitTemplate 对象将消息发送到指定的交换机和路由键。具体作用和含义如下:
         * RabbitMQConfig.EXCHANGE:表示要发送消息的交换机名称。
         * "topic.black.dog":表示消息的路由键,用于确定消息被发送到哪个队列。
         * "你好,王五,明天有空吗?":表示要发送的消息内容。
         * new MessagePostProcessor() {...}:创建了一个匿名的 MessagePostProcessor 对象,用于对消息进行后处理操作。
         *      postProcessMessage(Message message) 方法:在该方法中对消息进行后处理操作,这里的操作是设置消息的持久化,即将消息标记为持久化消息,确保消息在 RabbitMQ 服务器重启后不会丢失。
         *      message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT):设置消息的持久化模式为 PERSISTENT,表示消息需要被持久化存储。
         *      return message:返回经过后处理的消息。
         */
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE, "topic.black.dog", "你好,王五,明天有空吗?", new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                /**
                 * 设置消息持久化
                 * MessageDeliveryMode 是 RabbitMQ 中用于设置消息的投递模式的枚举类型,它有两个枚举值:
                 *      MessageDeliveryMode.NON_PERSISTENT:表示非持久化消息。当消息被发送到 RabbitMQ 服务器后,如果服务器发生重启或崩溃,该消息可能会丢失。
                 *      MessageDeliveryMode.PERSISTENT:表示持久化消息。当消息被发送到 RabbitMQ 服务器后,会被持久化存储,即使服务器发生重启或崩溃,该消息也不会丢失。
                 */
                message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                return message;
            }
        });
        System.out.println("消息发送成功");

        System.in.read();
    }
}
通过确保消息、交换机、队列的持久化操作可以保证消息的在 RabbitMQ Server 中不丢失,从而保证可靠性,其实除了持久化之外还需要保证 RabbitMQ 的高可用性,否则 MQ 都宕机或磁盘受损都无法确保消息的可靠性,关于高可用性这里就不作过多说明,有兴趣的可以去了解一下。

3.4. 保证消费者消费的消息不丢失

在保证发送方和 RabbitMQ Server 的消息可靠性的前提下,只需要保证消费者在消费消息时异常消息不丢失即可保证消息的可靠性。

RabbitMQ 提供了 消费者应答机制 来使 RabbitMQ 能够感知到消费者是否消费成功消息,默认情况下,消费者应答机制是自动应答的,也就是RabbitMQ 将消息推送给消费者,便会从队列删除该消息,如果消费者在消费过程失败时,消息就存在丢失的情况。所以需要将消费者应答机制设置为手动应答,只有消费者确认消费成功后才会删除消息,从而避免消息的丢失。

下面来看看如何配置消费者手动应答:
spring:
  rabbitmq:
    host: 192.168.42.146
    port: 5672
    username: guest
    password: guest
    virtual-host: / #虚拟主机用于在RabbitMQ服务器上创建逻辑上的隔离环境。在这里,虚拟主机的名称是/,表示使用默认虚拟主机
    #生产端
    publisher-returns: true
    template:
      mandatory: true
    #消费端
    listener:
      simple:
        acknowledge-mode: manual # 手动ack
        #初始连接数量
        concurrency: 5 #设置消费者的初始连接数量。这表示同时处理的消息数量
        #最大连接数量
        max-concurrency: 10 # 设置消费者的最大连接数量。这表示允许的最大同时处理的消息数量。
        #限流
        prefetch: 1 # 设置消费者的预取计数。这表示在消费者处理完当前消息之前,不会从RabbitMQ服务器获取更多的消息。
    publisher-confirm-type: correlated # 表示开启发送确认机制

通过 listener.simple.acknowledge-mode = manual 即可将消费者应答机制设置为手动应答。

之后只需要在消费消息时,通过调用 channel.basicAck()channel.basicNack() 来根据业务的执行成功选择是手动确认消费还是手动丢弃消息。

@Component
@Slf4j
public class ConsumeListener {

    //该注解表示该方法监听名为 RabbitMQConfig.QUEUE 的队列,当队列中有消息到达时,该方法将被触发执行。
    @RabbitListener(queues = RabbitMQConfig.QUEUE)
    public void consume(String msg, Channel channel, Message message) throws IOException {
        /**
         * String msg:表示接收到的消息内容,即队列中的消息。
         * Channel channel:表示 RabbitMQ 的通道(Channel),可以用于进行消息确认、拒绝等操作。
         * Message message:表示接收到的消息对象,包含了消息的内容和属性等信息。
         */
        System.out.println("队列的消息为:"+msg);

        //通过 message.getMessageProperties().getClusterId() 获取消息的唯一标识(cluster ID)
        String id = message.getMessageProperties().getCorrelationId();

        // 获取消息索引
        long index = message.getMessageProperties().getDeliveryTag();
        
        System.out.println("唯一标识为:"+id);

        try {
            /**
             * 进行消息确认,告知 RabbitMQ 已成功处理该消息。
             * index = message.getMessageProperties().getDeliveryTag():获取消息的交付标签(delivery tag),表示消息在通道中的唯一标识。
             * false:表示是否批量确认模式。在这里设置为 false,表示只确认当前的消息。
             */
            channel.basicAck(index,false);
        }catch (Exception e){
            // 记录日志
            log.info("出现异常:{}", e.getMessage());
            try {
                // 手动丢弃信息
                channel.basicNack(index, false, false);
            } catch (IOException ex) {
                log.info("丢弃消息异常");
            }
        }

    }
}

这里说明一下 basicAck()basicNack() 的参数说明:

  • void basicAck(long deliveryTag, boolean multiple) 方法(会抛异常):
    • deliveryTag:该消息的index
    • multiple:是否批量处理(true 表示将一次性ack所有小于deliveryTag的消息)
  • void basicNack(long deliveryTag, boolean multiple, boolean requeue) 方法(会抛异常):
    • deliveryTag:该消息的index
    • multiple:是否批量处理(true 表示将一次性ack所有小于deliveryTag的消息)
    • requeue:被拒绝的是否重新入队列(true 表示添加在队列的末端;false 表示丢弃)

通过设置手动确认消费者应答机制即可保证消费者在消费信息时的消息可靠性

3.5.Spring Boot 提供的消息重试机制

除了消费者应答机制外,Spring Boot也提供了一种重试机制,只需要通过配置即可实现消息重试从而确保消息的可靠性,如下:
spring:
  rabbitmq:
    host: 192.168.42.146
    port: 5672
    username: guest
    password: guest
    virtual-host: / #虚拟主机用于在RabbitMQ服务器上创建逻辑上的隔离环境。在这里,虚拟主机的名称是/,表示使用默认虚拟主机
    #生产端
    publisher-returns: true # 启用生产者消息返回机制。当消息无法路由到队列时,生产者将收到返回的消息
    template:
      mandatory: true # 设置生产者发送消息时是否要求消息必须成功路由到队列。如果设置为true,当消息无法路由到队列时,将抛出异常。
    #消费端
    listener:
      simple:
        acknowledge-mode: manual # 手动ack
        #初始连接数量
        concurrency: 5 #设置消费者的初始连接数量。这表示同时处理的消息数量
        #最大连接数量
        max-concurrency: 10 # 设置消费者的最大连接数量。这表示允许的最大同时处理的消息数量。
        #限流
        prefetch: 1 # 设置消费者的预取计数。这表示在消费者处理完当前消息之前,不会从RabbitMQ服务器获取更多的消息。
      direct:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 5000ms # 初始失败等待时长为5秒
          multiplier: 1  # 失败的等待时长倍数(下次等待时长 = multiplier * 上次等待时间)
          max-attempts: 3 # 最大重试次数
          stateless: true # true无状态;false有状态(如果业务中包含事务,这里改为false)

    publisher-confirm-type: correlated # 表示开启发送确认机制

通过配置在消费者的方法上如果执行失败或执行异常只需要抛出异常(一定要出现异常才会触发重试,注意:不要捕获异常) 即可实现消息重试,这样也可以保证消息的可靠性。

posted @ 2023-09-18 17:46  酒剑仙*  阅读(63)  评论(0编辑  收藏  举报