RabbitMQ消息零丢失实战:生产者确认+消费者重试+死信队列一站式方案

目录

前言

消息可靠性

发送者可靠性

发送者重连

发送者确认

MQ可靠性

数据持久化

Lazy Queue

消费者可靠性

消费者确认机制

失败重试机制

业务幂等性

唯一消息ID

业务判断 

延迟消息

死信交换机

代码实现 

延迟消息插件

下载

安装

声明延迟交换机

发送延迟消息

效果 

抽取MQ工具

共享配置

封装工具

自动装配


前言

RabbitMQ全栈实践手册:从零搭建消息中间件到SpringAMQP高阶玩法-CSDN博客

经过上次RabbitMQ的学习,基本具备了MQ收发消息的功能,可以满足大多数的业务场景,但是在

一些业务场景中我们要保证消息的可靠性,所以我们还需要学习一些RabbitMQ的一些高级功能,

例如:生产者确认、消费者重试、死信队列、延迟消息等。

消息可靠性

支付成功后,需要发送消息到broker,broker再发送消息给交易服务,但是这个过程消息可能丢

失,可能网络问题,有可以MQ挂了,这时候消息者那边已经支付了,但是订单状态还是未支付,

因为交易服务没有接收到消息,消息丢了。

消息丢失主要有三个地方:

  • 发送者发送消息到broker这个过程可能消息丢失。
  • 消息到达broker,broker可能挂了有可能消息丢失。
  • broker发送消息到消费者或者已经到达消费者,没有正确处理也会消息丢失。

发送者可靠性

发送者重连

有时候可能网络问题导致发送消息到broker失败的情况,这个时候我们可以通过配置开启失败后的

重连。

spring:
  rabbitmq:
    connection-timeout: 1s # 设置MQ的连接超时时间
    template:
      retry:
        enabled: true # 开启超时重试机制
        initial-interval: 1000ms # 失败后的初始等待时间
        multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
        max-attempts: 3 # 最大重试次数

现在我将mq停止掉了,配置了mq重连配置,再去发送mq可以看到重试了三次。

当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。如果对于业务性能有要求,建议禁用重试机制。如果一定要使用,请合理配置等待时长和重试次数,当然也可以考虑使用异步线程来执行发送消息的代码。 

发送者确认

Spring AMQP提供了Publisher Confirm和Publisher Returen机制,Publisher Confirm是确认消息是

否成功到达Exchange,Publisher Return处理从Exchange路由到Queue失败场景。

  • 消息投递到MQ,但是路由失败,此时通过PublisherReturn返回路由异常原因,然后返回ACK,告知投递成功。
  • 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功。
  • 持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知投递成功。
  • 其他情况都会返回NACK,告知投递失败。

 配置文件中添加配置开启Publisher Confirm机制和Publisher Return机制

spring:
  rabbitmq:
    publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
    publisher-returns: true # 开启publisher return机制

publisher-confirm-type配置说明:

none:关闭confirm机制

simple:同步阻塞等待MQ的回执消息

correlated:MQ异步回调方式返回回执消息

配置ReturnCallbacke,每个RabbitTemplate只能配置一个ReturnCallback

@Slf4j
@RequiredArgsConstructor
@Configuration
public class MqConfig {

    private final RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                log.error("触发return callback,");
                log.debug("exchange:{}",returnedMessage.getExchange());
                log.debug("routingKey:{}",returnedMessage.getMessage());
                log.debug("message:{}",returnedMessage.getMessage());
                log.debug("replyCode:{}",returnedMessage.getReplyCode());
                log.debug("replyText:{}",returnedMessage.getReplyText());
            }
        });
    }
}

发送消息,指定消息ID、消息ConfirmCallback

 @Test
    public void testConfirmCallback() throws InterruptedException {
        // 创建correlationData
        CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
        cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
            @Override
            public void onFailure(Throwable ex) {
                log.error("spring amqp处理确认结果异常",ex);
            }

            @Override
            public void onSuccess(CorrelationData.Confirm result) {
                // 判断是否成功
                if(result.isAck()){
                    log.debug("判断ConfirmCallback ack 消息发送成功!");
                }else{
                    log.error("收到ConfirmCallback nack 消息发送失败!reason:{}",result.getReason());
                }
            }
        });

        String exchangeName = "hmall.direct2";

        String message = "陶然同学学习RabbitMQ高级篇";

        rabbitTemplate.convertAndSend(exchangeName,"blue",message,cd);

        Thread.sleep(2000);
    }

 效果:

MQ可靠性

默认情况下,RabbitMQ会把接收到的消息放到内存,如果内存慢了再把消息放到磁盘。这样就会

导致两个问题:

  • MQ如果宕机,消息在内存里面就会丢失。
  • 内存空间有限,消费者故障或者处理慢,就会消息积压,造成MQ阻塞。

数据持久化

Rabbit实现数据持久化主要有三个方面:

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

我们使用Spring AMQP发送的交换机、队列、消息都是持久化的,即使MQ宕机重启,消息也还

在。

Lazy Queue

RabbitMQ从3.6.0版本开始,增加Lazy Queue概念,叫惰性队列,接收到消息直接存储在磁盘,不

再存储到内存,消费者消费消息的时候直接从磁盘加载到内存(有可以提前缓存部分消息到内存,

最多2048条)

消费者可靠性

消费者确认机制

消费者确认机制是为了消费者是否正确处理消息,当消费者处理消息结束后,应该向RabbitMQ发

送一个回执,告诉RabbitMQ自己处理消息状态:

  • ack:成功处理消息,RabbitMQ从队列删除该消息。
  • nack:消息处理失败,RabbitMQ需要再次投递消息
  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

Spring AMQP已经实现了消息确认机制,我们在配置文件配置即可,有三种方式:

none:不处理,接收到消息返回ack给broker,broker删除消息。

manual:收到模式,自己在业务代理里返回ack或者reject。

auto:自动模式,利用AOP对业务代码做环绕通知,正常处理完返回ack,出现异常返回nack

        业务异常会自动返回nack

        消息处理或校验异常返回reject

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: none # none,关闭ack;manual,手动ack;auto:自动ack

失败重试机制

消费者返回nack,mq就会重新投递消息,然后消费者又返回nack,mq有重新投递,无限循环,我

们希望消费者处理错误消息的时候,能够通知我们开发,由我们开发介入。

SpringAMQP提供了消费者失败重试机制,通过配置文件开启。

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

失败重试策略:

本地重试到达最大次数后,消息会被丢弃,对于某些可靠性要求较高的场景下,显然不太合适,因

此Spring允许自定义重试次数耗尽的消息处理策略,这个策略有MessageRecovery接口定义,它

有3个不同实现:

RejectAndDontRequestRecoverer:重试耗尽,直接reject,丢弃消息,默认实现。

ImmediateRequeueMessageRecoverer:重试耗尽,返回nack,消息重新入队。

RepublishMessageRecoverer:重试耗尽,将消息投递给指定交换机。

失败后将消息投递给一个指定的,专门存放异常消息队列,后续由人工集中处理。

@Configuration
public class ErrorMessageConfiguration {

    @Bean
    public DirectExchange errorExchange(){
        return new DirectExchange("error.direct");
    }

    @Bean
    public Queue errorQueue(){
        return new Queue("error.queue");
    }

    @Bean
    public Binding errorQueueBinding(Queue errorQueue,DirectExchange errorExchange){
        return BindingBuilder.bind(errorQueue).to(errorExchange).with("error");
    }

    @Bean
    public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
        return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
    }
}

业务幂等性

幂等性是在程序开发中,指同一个业务执行一次或多次对业务状态的影响是一致的。

唯一消息ID

方案一,是给每个消息都设置一个唯一id,利用id区分消息是否重复消费:

  1. 每一条消息都生成一个唯一的id,与消息一起投递给消费者。
  2. 消费者接收到消息后处理自己的业务,业务处理完成后将消息ID保存到数据库。
  3. 如果下次有收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。
    @Bean
    public MessageConverter messageConverter(){
        // 1.定义消息转换器
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }

业务判断 

方案二,是结合业务逻辑,基于业务本身做判断。以我们的余额支付业务为例:

方案二,是结合业务逻辑,基于业务本身做判断。以我们的余额支付业务为例:

延迟消息

延迟消息:发送者发送消息时指定一个时间,消费者不会立即收到消息,而是在指定时间之后才收

到消息。

延迟任务:设置一定时间之后才执行的任务。

死信交换机

当一个队列中的消息满足下列情况之一时,就会称为死信(dead letter)

  • 消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息(到达了队列或消息本身设置的过期时间),超时无人消费
  • 要投递的队列消息堆积满了,最早的消息可能成为死信

如果队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就会投递到这个

交换机中。这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)

代码实现 

声明normal.direct交换机、normal.queue队列并且指定死信交换机。

@Configuration
public class NormalConfiguration {

    @Bean
    public DirectExchange normalExchange(){
        return new DirectExchange("normal.direct");
    }

    @Bean
    public Queue normalQueue(){
        return QueueBuilder
                .durable("normal.queue")
                .deadLetterExchange("dlx.direct")
                .build();
    }

    @Bean
    public Binding normalExchangeBinding(Queue normalQueue,DirectExchange normalExchange){
        return BindingBuilder.bind(normalQueue).to(normalExchange).with("hi");
    }
}

监听dlx.direct交换机,死信交换机的routingkey要和normal.direct的toutingkey一样。

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "dlx.queue",durable = "true"),
            exchange = @Exchange(name = "dlx.direct",type = ExchangeTypes.DIRECT),
            key = "hi"
    ))
    public void listenDlxQueue(String message){
        log.info("消费者监听到dlx.queue的消息:【{}】",message);
    }

发送消息,并指定消息过期时间为10秒钟。

    @Test
    void testSendDelayMessage(){
        rabbitTemplate.convertAndSend("normal.direct","hi","hello 我叫陶然",message -> {
            message.getMessageProperties().setExpiration("10000");
            return message;
        });
    }

消息投递到normal.direct交换机,normal.direct交换机投递给normal.queue队列,normal.queue10

秒后没有人处理消息,将消息投递给dlx.direct交换机,dlx.direct交换机投递给dlx.queue队列。

延迟消息插件

基于死信队列虽然可以实现延迟消息,但是太麻烦了。因此RabbitMQ社区提供了一个延迟消息插

件来是实现相同效果。

下载

官方文档说明:

Scheduling Messages with RabbitMQ | RabbitMQ

安装

因为我们是基于Docker安装,所以需要先查看RabbitMQ的插件目录对应的数据卷。

docker volume inspect mq-plugins

 结果如下:

[
    {
        "CreatedAt": "2024-06-19T09:22:59+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/mq-plugins/_data",
        "Name": "mq-plugins",
        "Options": null,
        "Scope": "local"
    }
]

插件目录被挂载到了/var/lib/docker/volumes/mq-plugins/_data这个目录,我们上传插件到该目

录下。

接下来执行命令,安装插件:

docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange

运行结果如下:

声明延迟交换机

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "delay.queue",durable = "true"),
            exchange = @Exchange(name = "delay.direct",delayed = "true"),
            key = "delay"
    ))
    public void listenDelayMessage(String msg){
        log.info("接收到delay.queue的延迟消息:{}",msg);
    }

发送延迟消息

    @Test
    void testPublisherDelayMessage(){
        //1、创建消息
        String message = "hello,delayed message";
        //2、发送消息 利用消息后置处理器添加消息头
        rabbitTemplate.convertAndSend("delay.direct", "delay", message, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 添加延迟消息属性
                message.getMessageProperties().setDelay(5000);
                return message;
            }
        });
    }

效果 

抽取MQ工具

共享配置

首先,我们需要在nacos中抽取RabbitMQ的共享配置,命名为shared-mq.yaml

其中只包含mq的基础共享配置,内容如下:

spring:
  rabbitmq:
    host: ${hm.mq.host:192.168.150.101} # 主机名
    port: ${hm.mq.port:5672} # 端口
    virtual-host: ${hm.mq.vhost:/hmall} # 虚拟主机
    username: ${hm.mq.un:hmall} # 用户名
    password: ${hm.mq.pw:123} # 密码

封装工具

package com.hmall.common.utils;

import cn.hutool.core.lang.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.util.concurrent.ListenableFutureCallback;

/**
 * RabbitMQ消息发送工具类
 * 提供普通消息、延迟消息、带确认机制消息的发送能力
 */
@Slf4j
@RequiredArgsConstructor
public class RabbitMqHelper {

    // 通过构造器注入RabbitTemplate(由Spring自动装配)
    private final RabbitTemplate rabbitTemplate;

    /**
     * 发送普通消息(不带确认机制)
     * @param exchange    交换机名称
     * @param routingKey  路由键
     * @param msg         消息内容(会被自动序列化)
     */
    public void sendMessage(String exchange, String routingKey, Object msg) {
        log.debug("准备发送消息,exchange:{}, routingKey:{}, msg:{}", exchange, routingKey, msg);
        // 使用convertAndSend实现自动消息转换
        rabbitTemplate.convertAndSend(exchange, routingKey, msg);
    }

    /**
     * 发送延迟消息(需要安装rabbitmq_delayed_message_exchange插件)
     * @param exchange    交换机名称(必须是x-delayed-message类型)
     * @param routingKey  路由键
     * @param msg         消息内容
     * @param delay       延迟时间(单位:毫秒)
     */
    public void sendDelayMessage(String exchange, String routingKey, Object msg, int delay) {
        // 通过MessagePostProcessor设置延迟参数
        rabbitTemplate.convertAndSend(exchange, routingKey, msg, message -> {
            // 设置消息的延迟时间(插件识别的header)
            message.getMessageProperties().setDelay(delay);
            return message;
        });
    }

    /**
     * 带确认机制的消息发送(支持失败重试)
     * @param exchange     交换机名称
     * @param routingKey   路由键
     * @param msg          消息内容
     * @param maxRetries   最大重试次数(建议3-5次)
     * 
     * 实现特性:
     * 1. 使用CorrelationData跟踪消息状态
     * 2. 通过ListenableFuture实现异步确认
     * 3. 失败后自动重试直至最大次数
     * 
     * 注意:频繁重试可能影响性能,建议结合死信队列使用
     */
    public void sendMessageWithConfirm(String exchange, String routingKey, Object msg, int maxRetries) {
        log.debug("准备发送消息(带确认),exchange:{}, routingKey:{}, msg:{}", exchange, routingKey, msg);
        
        // 生成唯一消息ID用于追踪
        CorrelationData cd = new CorrelationData(UUID.randomUUID().toString(true));
        
        // 添加确认结果回调
        cd.getFuture().addCallback(new ListenableFutureCallback<>() {
            // 当前重试次数计数器
            int retryCount = 0;

            /**
             * 处理确认失败(网络异常等)
             */
            @Override
            public void onFailure(Throwable ex) {
                log.error("处理ACK回执时发生异常", ex);
            }

            /**
             * 处理Broker确认结果
             * @param result 确认结果对象(包含ack/nack状态)
             */
            @Override
            public void onSuccess(CorrelationData.Confirm result) {
                // 检查是否为NACK(消息未到达Broker)
                if (result != null && !result.isAck()) {
                    log.warn("收到Broker的NACK确认,消息未到达,当前重试次数:{}", retryCount);
                    
                    // 判断是否超过最大重试次数
                    if (retryCount >= maxRetries) {
                        log.error("消息重试次数耗尽(最大{}次),放弃发送", maxRetries);
                        return;
                    }
                    
                    // 生成新的消息ID(避免重复消息问题)
                    CorrelationData newCd = new CorrelationData(UUID.randomUUID().toString(true));
                    // 保持回调链
                    newCd.getFuture().addCallback(this);
                    
                    // 重新发送消息
                    rabbitTemplate.convertAndSend(exchange, routingKey, msg, newCd);
                    retryCount++;
                    
                    log.debug("正在进行第{}次重试...", retryCount);
                } else {
                    log.debug("消息已成功到达Broker");
                }
            }
        });
        
        // 执行首次消息发送
        rabbitTemplate.convertAndSend(exchange, routingKey, msg, cd);
    }
}

自动装配

该配置类主要用于 ​​整合RabbitMQ的消息序列化及工具类初始化​​,通过条件化加载机制确保在满足

依赖时自动配置生效。

package com.hmall.common.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hmall.common.utils.RabbitMqHelper;
import org.springframework.amqp.core.RabbitTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnClass(value = {MessageConverter.class, RabbitTemplate.class})
public class MqConfig {

    @Bean
    @ConditionalOnBean(ObjectMapper.class)
    public MessageConverter messageConverter(ObjectMapper mapper){
        // 1.定义消息转换器
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(mapper);
        // 2.配置自动创建消息id,用于识别不同消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }

    @Bean
    public RabbitMqHelper rabbitMqHelper(RabbitTemplate rabbitTemplate){
        return new RabbitMqHelper(rabbitTemplate);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陶然同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值