【消息队列】RabbitMQ如何保障消息可靠性投递

【消息队列】如何保障消息可靠性投递

在现代分布式系统中,消息队列扮演着至关重要的角色,它不仅能够解耦系统组件,还能提高系统的扩展性和可维护性。

加上面试消息队列的八股文最喜欢问这个了。消息可靠性投递,这个对于消息队列非常重要,很多时候,我们不是人为的去干预,但是仍然出现其他意外的事情

导致消息在传递过程中可能会面临丢失、重复、损坏等问题,这就要求我们必须重视消息的可靠性投递。

什么是消息可靠性投递?

消息可靠性投递的目标是确保消息能够从生产者(Producer)可靠地传递到消费者(Consumer),并且在传递过程中不丢失、不重复、不损坏。实现这一目标需要采取多种技术手段和策略。

下面列举三个栗子:

故障情况1:消息没有发送到消息队列上。导致消费者拿不到消息,业务功能缺失,数据错误

  • 解决思路A:在生产者端进行确认,具体操作中我们会分别针对交换机和队列来确认,如果没有成功发送到消息队列服务器上,那就可以尝试重新发送

  • 解决思路B:为目标交换机指定备份交换机,当目标交换机投递失败时,把消息投递至备份交换机

故障情况2:消息成功存入消息队列,但是消息队列服务器宕机了。原本保存在内存中的消息也丢失了。即使服务器重新启动,消息也找不回来了

  • 导致消费者拿不到消息,业务功能缺失,数据错误

  • 解决思路:消息持久化到硬盘上,哪怕服务器重启也不会导致消息丢失

故障情况3:消息成功存入消息队列,但是消费端出现问题,例如:宕机、抛异常等等

  • 导致业务功能缺失,数据错误

解决思路

  1. 消费端消费消息成功,给服务器返回ACK信息,然后消息队列删除该消息

  2. 消费端消费消息失败,给服务器端返回NACK信息。

  3. 同时把消息恢复为待消费的状态,这样就可以再次取回消息,重试一次(当然,这就需要消费端接口支持幂等性)

故障情况1:消息没有发送到消息队列上

1. 生产者代码demo演示

1.1 配置POM

xml 复制代码
 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.nateshao</groupId>
    <artifactId>code7_confirm_consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>code7_confirm_consumer</name>
    <description>code7_confirm_consumer</description>
    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

1.2 主启动类

没有特殊设定:

java 复制代码
package com.nateshao.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
@SpringBootApplication
public class RabbitMQConsumerMainType {

    public static void main(String[] args) {
        SpringApplication.run(RabbitMQConsumerMainType.class, args);
    }
}

1.3 YAML

注意:publisher-confirm-typepublisher-returns是两个必须要增加的配置,如果没有则本节功能不生效

yaml 复制代码
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: 123456
    virtual-host: /
    publisher-confirm-type: CORRELATED # 交换机的确认
    publisher-returns: true # 队列的确认
logging:
  level:
    com.nateshao.producer.config.MQProducerAckConfig: info

2. 创建配置类

在这里我们为什么要创建这个配置类呢?首先,我们需要声明回调函数来接收RabbitMQ服务器返回的确认信息:

方法名 方法功能 所属接口 接口所属类
confirm() 确认消息是否发送到交换机 ConfirmCallback RabbitTemplate
returnedMessage() 确认消息是否发送到队列 ReturnsCallback RabbitTemplate

然后,就是对RabbitTemplate的功能进行增强,因为回调函数所在对象必须设置到RabbitTemplate对象中才能生效。

原本RabbitTemplate对象并没有生产者端消息确认的功能,要给它设置对应的组件才可以。

而设置对应的组件,需要调用RabbitTemplate对象下面两个方法:

设置组件调用的方法 所需对象类型
setConfirmCallback() ConfirmCallback接口类型
setReturnCallback() ReturnCallback接口类型

2.1 API说明

①ConfirmCallback接口

这是RabbitTemplate内部的一个接口,源代码如下:

java 复制代码
	/**
	 * A callback for publisher confirmations.
	 *
	 */
	@FunctionalInterface
	public interface ConfirmCallback {

		/**
		 * Confirmation callback.
		 * @param correlationData correlation data for the callback.
		 * @param ack true for ack, false for nack
		 * @param cause An optional cause, for nack, when available, otherwise null.
		 */
		void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause);

	}

生产者端发送消息之后,回调confirm()方法

  • ack参数值为true:表示消息成功发送到了交换机
  • ack参数值为false:表示消息没有发送到交换机

②ReturnCallback接口

同样也RabbitTemplate内部的一个接口,源代码如下:

java 复制代码
	/**
	 * A callback for returned messages.
	 *
	 * @since 2.3
	 */
	@FunctionalInterface
	public interface ReturnsCallback {

		/**
		 * Returned message callback.
		 * @param returned the returned message and metadata.
		 */
		void returnedMessage(ReturnedMessage returned);

	}

注意:接口中的returnedMessage()方法仅在消息没有发送到队列时调用

ReturnedMessage类中主要属性含义如下:

属性名 类型 含义
message org.springframework.amqp.core.Message 消息以及消息相关数据
replyCode int 应答码,类似于HTTP响应状态码
replyText String 应答码说明
exchange String 交换机名称
routingKey String 路由键名称

3. 配置类代码

3.1 要点1

加@Component注解,加入IOC容器

3.2 要点2

配置类自身实现ConfirmCallback、ReturnCallback这两个接口,然后通过this指针把配置类的对象设置到RabbitTemplate对象中。

操作封装到了一个专门的void init()方法中。

为了保证这个void init()方法在应用启动时被调用,我们使用@PostConstruct注解来修饰这个方法。

关于@PostConstruct注解大家可以参照以下说明:

@PostConstruct注解是Java中的一个标准注解,它用于指定在对象创建之后立即执行的方法。当使用依赖注入(如Spring框架)或者其他方式创建对象时,@PostConstruct注解可以确保在对象完全初始化之后,执行相应的方法。

使用@PostConstruct注解的方法必须满足以下条件:

  1. 方法不能有任何参数。
  2. 方法必须是非静态的。
  3. 方法不能返回任何值。

当容器实例化一个带有@PostConstruct注解的Bean时,它会在调用构造函数之后,并在依赖注入完成之前调用被@PostConstruct注解标记的方法。这样,我们可以在该方法中进行一些初始化操作,比如读取配置文件、建立数据库连接等。

3.3 生产者中的代码

有了以上说明,下面我们就可以展示配置类的整体代码:

java 复制代码
package com.nateshao.producer.mq.config;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/5/29 16:00
 * @Version 1.0
 */
@Configuration
@Slf4j
public class RabbitConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void initRabbitTemplate() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        // 消息发送到交换机成功或失败时调用这个方法
        log.info("confirm() 回调函数打印 CorrelationData:" + correlationData);
        log.info("confirm() 回调函数打印 ack:" + ack);
        log.info("confirm() 回调函数打印 cause:" + cause);
    }

    @Override
    public void returnedMessage(ReturnedMessage returned) {
        // 发送到队列失败时才调用这个方法
        log.info("returnedMessage() 回调函数 消息主体: " + new String(returned.getMessage().getBody()));
        log.info("returnedMessage() 回调函数 应答码: " + returned.getReplyCode());
        log.info("returnedMessage() 回调函数 描述:" + returned.getReplyText());
        log.info("returnedMessage() 回调函数 消息使用的交换器 exchange : " + returned.getExchange());
        log.info("returnedMessage() 回调函数 消息使用的路由键 routing : " + returned.getRoutingKey());
    }
}

4.消费者代码演示

application.yml

yaml 复制代码
spring:
  rabbitmq:
    host: localhost
    port: 15672
    username: guest
    password: 123456
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual # 把消息确认模式改为手动确认
        prefetch: 1 # 每次从队列中取回消息的数量

监听类:MyMessageListener.class

java 复制代码
package com.nateshao.producer.mq.config;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
 * @Author 千羽
 * @公众号 程序员千羽
 * @Date 2024/6/1 14:18
 * @Version 1.0
 */
@Component
@Slf4j
public class MQProducerAckConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.info("消息发送到交换机成功!数据:" + correlationData);
        } else {
            log.info("消息发送到交换机失败!数据:" + correlationData + " 原因:" + cause);
        }
    }

    @Override
    public void returnedMessage(ReturnedMessage returned) {
        log.info("消息主体: " + new String(returned.getMessage().getBody()));
        log.info("应答码: " + returned.getReplyCode());
        log.info("描述:" + returned.getReplyText());
        log.info("消息使用的交换器 exchange : " + returned.getExchange());
        log.info("消息使用的路由键 routing : " + returned.getRoutingKey());
    }
}

5.验证效果

5.1 验证交换机错误

java 复制代码
@Test
public void testExchangeDirectErrorSendMessage() {
    rabbitTemplate.convertAndSend(EXCHANGE_DIRECT + "000", ROUTING_KEY, "Message Test Confirm~~~ ~~~");
}

控制台输出,提示:没有找到该交换机no exchange 'exchange.direct.order~'

java 复制代码
Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'exchange.direct.order000' in vhost '/', class-id=60, method-id=40)
confirm() 回调函数打印 CorrelationData:null
confirm() 回调函数打印 ack:false
confirm() 回调函数打印 cause:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'exchange.direct.order000' in vhost '/', class-id=60, method-id=40)
  1. 2验证路由错误
java 复制代码
/**
 * 验证路由地址写错
 */
@Test
public void testRoutingErrorMessage() {
    rabbitTemplate.convertAndSend(EXCHANGE_DIRECT, ROUTING_KEY + "routing ~", "Message Test Confirm~~~ ~~~");
}

控制台打印:发送交换机成功ack:true,但是路由失败NO_ROUTE

java 复制代码
returnedMessage() 回调函数 消息主体: Message Test Confirm~~~ ~~~
returnedMessage() 回调函数 应答码: 312
confirm() 回调函数打印 CorrelationData:null
confirm() 回调函数打印 ack:true
confirm() 回调函数打印 cause:null
returnedMessage() 回调函数 描述:NO_ROUTE
returnedMessage() 回调函数 消息使用的交换器 exchange : exchange.direct.order
returnedMessage() 回调函数 消息使用的路由键 routing : queue.orderrouting ~

6. 完整代码

java 复制代码
@SpringBootTest(classes = RabbitMQProducerMainType.class)
public class RabbitMQTest {  
  
    public static final String EXCHANGE_DIRECT = "exchange.direct.order";
    public static final String ROUTING_KEY = "order";
  
    @Autowired  
    private RabbitTemplate rabbitTemplate;
  
    @Test
    public void testSendMessage() {
        rabbitTemplate.convertAndSend(EXCHANGE_DIRECT + "~", ROUTING_KEY , "Hello 千羽");
    }
  
}

通过调整代码,测试如下三种情况:

  • 交换机正确、路由键正确
  • 交换机正确、路由键不正确,无法发送到队列
  • 交换机不正确,无法发送到交换机

故障情况2:备份交换机

故障情况2:消息成功存入消息队列,但是消息队列服务器宕机了。原本保存在内存中的消息也丢失了。即使服务器重新启动,消息也找不回来了

  • 导致消费者拿不到消息,业务功能缺失,数据错误

  • 解决思路:消息持久化到硬盘上,哪怕服务器重启也不会导致消息丢失

1. 创建备份交换机

1.1 创建备份交换机

注意:备份交换机一定要选择fanout类型,因为原交换机转入备份交换机时并不会指定路由键

1.2 创建备份交换机要绑定的队列

①创建队列

②绑定交换机

注意:这里是要和备份交换机绑定

1.3 针对备份队列创建消费端监听器

java 复制代码
    public static final String EXCHANGE_DIRECT_BACKUP = "exchange.direct.order.backup";
    public static final String QUEUE_NAME_BACKUP  = "queue.order.backup";

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = QUEUE_NAME_BACKUP, durable = "true"),
            exchange = @Exchange(value = EXCHANGE_DIRECT_BACKUP),
            key = {""}
    ))
    public void processMessageBackup(String dateString,
                                     Message message,
                                     Channel channel) {
        log.info("BackUp: " + dateString);
    }

2. 设定备份关系

2.1 原交换机删除

·

2.2 重新创建原交换机

2.3 原交换机重新绑定原队列

3.测试

  • 启动消费者端
  • 发送消息,但是路由键不对,于是转入备份交换机

实际上,这种备份交换机还是比较少的,一般常用的是采取ack的确认机制

故障情况3:交换机和队列持久化

rabbitmq默认是持久化的,这一点可以看一下底层的源码

通过点击监听器@RabbitListener,找到Queue[] queuesToDeclare() default {};

看到String durable() default "";看注释可以看到,默认情况下,如果你提供队列名称的话,默认就是持久化的。

1. 测试非持久化交换机和队列

1.1 创建非持久化交换机

创建之后,可以在列表中看到:

1.2 创建非持久化队列

创建之后,可以在列表中看到:

1.3 绑定

1.4 发送消息

java 复制代码
    public static final String EXCHANGE_TRANSIENT = "exchange.transient.user";
    public static final String ROUTING_KEY_TRANSIENT = "user";

    @Test
    public void testSendMessageTransient() {
        rabbitTemplate.convertAndSend(EXCHANGE_TRANSIENT,ROUTING_KEY_TRANSIENT,"Hello 千羽 user~~~");
    }

1.5 查看已发送消息

结论:临时性的交换机和队列也能够接收消息,但如果RabbitMQ服务器重启之后会怎么样呢?

1.6 重启RabbitMQ服务器

shell 复制代码
docker restart rabbitmq

重启之后,刚才临时性的交换机和队列都没了。在交换机和队列这二者中,队列是消息存储的容器,队列没了,消息就也跟着没了。

2. 持久化的交换机和队列

我们其实不必专门创建持久化的交换机和队列,因为它们默认就是持久化的。接下来我们只需要确认一下:存放到队列中,尚未被消费端取走的消息,是否会随着RabbitMQ服务器重启而丢失?

2.1 发送消息

运行以前的发送消息方法即可,不过要关掉消费端程序

2.2 在管理界面查看消息

2.3 重启RabbitMQ服务器

shell 复制代码
docker restart rabbitmq

2.4 再次查看消息

仍然还在

相关推荐
程序员小凯1 小时前
Spring Boot测试框架详解
java·spring boot·后端
你的人类朋友2 小时前
什么是断言?
前端·后端·安全
程序员小凯3 小时前
Spring Boot缓存机制详解
spring boot·后端·缓存
i学长的猫3 小时前
Ruby on Rails 从0 开始入门到进阶到高级 - 10分钟速通版
后端·ruby on rails·ruby
用户21411832636023 小时前
别再为 Claude 付费!Codex + 免费模型 + cc-switch,多场景 AI 编程全搞定
后端
茯苓gao4 小时前
Django网站开发记录(一)配置Mniconda,Python虚拟环境,配置Django
后端·python·django
Cherry Zack4 小时前
Django视图进阶:快捷函数、装饰器与请求响应
后端·python·django
一个尚在学习的计算机小白4 小时前
RabbitMQ
分布式·rabbitmq
爱读源码的大都督4 小时前
为什么有了HTTP,还需要gPRC?
java·后端·架构
码事漫谈4 小时前
致软件新手的第一个项目指南:阶段、文档与破局之道
后端