SpringBoot基于RabbitMQ实现消息可靠性

文章目录

  • [1. ☃️概述](#1. ☃️概述)
  • [2. ☃️生产者消息确认](#2. ☃️生产者消息确认)
    • [2.1 ❄️❄️概述](#2.1 ❄️❄️概述)
    • [2.2 ❄️❄️实战](#2.2 ❄️❄️实战)
      • [⛷️⛷️⛷️2.2.1 修改配置](#⛷️⛷️⛷️2.2.1 修改配置)
      • [⛷️⛷️⛷️2.2.2 定义 Return 回调](#⛷️⛷️⛷️2.2.2 定义 Return 回调)
      • [⛷️⛷️⛷️2.2.3 定义ConfirmCallback](#⛷️⛷️⛷️2.2.3 定义ConfirmCallback)
  • [3. ☃️消息持久化](#3. ☃️消息持久化)
    • [3.1 ❄️❄️交换机持久化](#3.1 ❄️❄️交换机持久化)
    • [3.2 ❄️❄️队列持久化](#3.2 ❄️❄️队列持久化)
    • [3.3 ❄️❄️消息持久化](#3.3 ❄️❄️消息持久化)
  • [4. ☃️消费者消息确认](#4. ☃️消费者消息确认)
    • [4.1 ❄️❄️三种确认模式](#4.1 ❄️❄️三种确认模式)
    • [4.2 ❄️❄️消息失败重试机制](#4.2 ❄️❄️消息失败重试机制)
      • [⛷️⛷️⛷️4.2.1 本地重试机制](#⛷️⛷️⛷️4.2.1 本地重试机制)
      • [4.2.2 ⛷️⛷️⛷️失败策略](#4.2.2 ⛷️⛷️⛷️失败策略)
  • [5. ☃️总结](#5. ☃️总结)

1. ☃️概述

消息从发送到消费者接收 会经历的过程如下:

丢失消息的可能性

  • 发送时丢失:
    • 生产者发送的消息未送达exchange
    • 消息到达exchange后未到达queue
  • MQ宕机,queue将消息丢失
  • consumer接收到消息后未消费就宕机

针对这些问题,RabbitMQ分别给出了解决方案

  • 生产者确认机制
  • mq持久化
  • 消费者确认机制
  • 失败重试机制

2. ☃️生产者消息确认

2.1 ❄️❄️概述

RabbitMQ 提供了 publisher confirm 机制来避免消息发送到 MQ 过程中丢失。这种机制必须给每个消息指定一个唯一ID。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。

返回结果有两种方式:

  • publisher-confirm,发送者确认
    • 消息成功投递到交换机,返回ack
    • 消息未投递到交换机,返回nack
  • publisher-return,发送者回执
    • 消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。


2.2 ❄️❄️实战

⛷️⛷️⛷️2.2.1 修改配置

java 复制代码
spring:
  rabbitmq:
    publisher-confirm-type: correlated
    publisher-returns: true
    template:
      mandatory: true

配置说明:

  • publish-confirm-type:开启publisher-confirm,这里支持两种类型:
    • simple:同步等待confirm结果,直到超时
    • correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
  • publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback
  • template.mandatory:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息

⛷️⛷️⛷️2.2.2 定义 Return 回调

每个RabbitTemplate只能配置一个ReturnCallback ,因此需要在项目加载时配置:

修改publisher服务,添加一个:

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 获取RabbitTemplate
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 设置ReturnCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 投递失败,记录日志
            log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
                     replyCode, replyText, exchange, routingKey, message.toString());
            // 如果有业务需要,可以重发消息
        });
    }
}

⛷️⛷️⛷️2.2.3 定义ConfirmCallback

ConfirmCallback 可以在发送消息时指定,因为每个业务处理 confirm 成功或失败的逻辑不一定相同。

java 复制代码
public void testSendMessage2SimpleQueue() throws InterruptedException {
    // 1.消息体
    String message = "hello, spring amqp!";
    // 2.全局唯一的消息ID,需要封装到 CorrelationData 中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 3.添加callback
    correlationData.getFuture().addCallback(
        result -> {
            if(result.isAck()){
                // 3.1.ack,消息成功
                log.debug("消息发送成功, ID:{}", correlationData.getId());
            }else{
                // 3.2.nack,消息失败
                log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
            }
        },
        ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
    );
    // 4.发送消息
    rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData);

    // 休眠一会儿,等待ack回执
    //Thread.sleep(20);
}

3. ☃️消息持久化

生产者确认可以确保消息投递到 RabbitMQ 的队列中,但是消息发送到 RabbitMQ 以后,如果突然宕机,也可能导致消息丢失。

要想确保消息在RabbitMQ中安全保存,必须开启消息持久化机制。

  • 交换机持久化
  • 队列持久化
  • 消息持久化

3.1 ❄️❄️交换机持久化

java 复制代码
@Bean
public DirectExchange simpleExchange(){
    // 三个参数:①交换机名称、②是否持久化、③当没有queue与其绑定时是否自动删除
    return new DirectExchange("simple.direct", true, false);
}

事实上,默认情况下,由SpringAMQP声明的交换机都是持久化的。

3.2 ❄️❄️队列持久化

java 复制代码
@Bean
public Queue simpleQueue(){
    // 使用QueueBuilder构建队列,durable就是持久化的
    return QueueBuilder.durable("simple.queue").build();
}

事实上,默认情况下,由SpringAMQP声明的队列都是持久化的。

3.3 ❄️❄️消息持久化

默认情况下,SpringAMQP 交换机 队列 以及发出的任何消息都是持久化的,不用特意指定。

4. ☃️消费者消息确认

RabbitMQ 是 阅后即焚 机制,RabbitMQ 确认消息被消费者消费后会立刻删除。

而 RabbitMQ 是通过 消费者回执 来确认消费者是否成功处理消息的:消费者获取消息后,应该向 RabbitMQ 发送 ACK 回执,表明自己已经处理消息。

设想这样的场景:

  • 1)RabbitMQ投递消息给消费者
  • 2)消费者获取消息后,返回ACK给RabbitMQ
  • 3)RabbitMQ删除消息
  • 4)消费者宕机,消息尚未处理

这样,消息就丢失了。因此消费者返回ACK的时机非常重要。

4.1 ❄️❄️三种确认模式

而 SpringAMQP 则允许配置三种确认模式:

•manual:手动ack,需要在业务代码结束后,调用api发送ack。

•auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack。

•none:关闭ack,MQ假定 消费者获取消息后会成功处理,因此消息投递后立即被删除(存在丢失消息的风险)。

由此可知:

  • none 模式下,消息投递是不可靠的,可能丢失
  • auto 模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack
  • manual:自己根据业务情况,判断什么时候该ack

一般,我们都是使用默认的 auto 即可。

相关配置

java 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        #acknowledge-mode: none # 关闭ack
        #acknowledge-mode: manual  # 手动ack
        acknowledge-mode: auto # 自动ack

4.2 ❄️❄️消息失败重试机制

当消费者出现异常后,消息会不断 requeue(重入队)到队列,再重新发送给消费者,然后再次异常,再次 requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力:怎么办呢?

⛷️⛷️⛷️4.2.1 本地重试机制

我们可以利用 Spring 的 retry 机制,在消费者出现异常时利用 本地重试 ,而不是无限制的 requeue 到 mq 队列。

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

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

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

  • 在重试3次后,SpringAMQP 会抛出异常 AmqpRejectAndDontRequeueException,说明本地重试触发了。
  • 查看 RabbitMQ 控制台,发现消息被删除了,说明最后 SpringAMQP 返回的是ack,mq删除消息了。

结论

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

4.2.2 ⛷️⛷️⛷️失败策略

在之前的测试中,达到最大重试次数后,消息会被丢弃,这是由 Spring 内部机制决定的。

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有 MessageRecovery 接口来处理,它包含三种不同的实现:

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

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

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

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

java 复制代码
@Configuration
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");
    }
}

其实 我们在生产中会指定死信交换机来处理失败的消息


5. ☃️总结

如何确保RabbitMQ消息的可靠性?

  • 开启生产者确认机制,确保生产者的消息能到达队列
  • 开启持久化功能,确保消息未消费前在队列中不会丢失
  • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
  • 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理
相关推荐
dkbnull3 小时前
深入理解Spring两大特性:IoC和AOP
spring boot
初次攀爬者3 小时前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
洋洋技术笔记7 小时前
Spring Boot条件注解详解
java·spring boot
洋洋技术笔记1 天前
Spring Boot配置管理最佳实践
spring boot
用户8307196840822 天前
Spring Boot 项目中日期处理的最佳实践
java·spring boot
大道至简Edward2 天前
Spring Boot 2.7 + JDK 8 升级到 Spring Boot 3.x + JDK 17 完整指南
spring boot·后端
洋洋技术笔记2 天前
Spring Boot启动流程解析
spring boot·后端
怒放吧德德3 天前
Spring Boot 实战:RSA+AES 接口全链路加解密(防篡改 / 防重放)
java·spring boot·后端
李慕婉学姐3 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
QQ5110082853 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php