Rabbit MQ是如何保证消息不丢失的?

通过 Java 注解(Spring AMQP)的方式实现 RabbitMQ 消息不丢失,这是生产环境中必须要解决的核心问题。消息丢失可能发生在生产者发送阶段RabbitMQ 服务端存储阶段消费者接收处理阶段三个环节,针对每个环节给出注解 + 配置的完整解决方案,确保端到端的消息可靠性。

一、先明确:消息不丢失的核心原则

要保证消息不丢失,必须覆盖三个核心环节的防护:

  1. 生产者侧:确保消息成功发送到 RabbitMQ(失败则重试 / 兜底);
  2. MQ 服务端:确保消息持久化(宕机重启后不丢失);
  3. 消费者侧:确保消息处理完成后再确认(处理失败则重试 / 进入死信队列)。

二、完整实现方案(注解 + 配置)

1. 基础配置(application.yml):核心参数兜底

先配置全局的可靠性参数,这是注解实现的基础:

yaml

复制代码
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    # ========== 1. 生产者可靠性配置 ==========
    publisher-confirm-type: correlated # 开启生产者确认(异步回调)
    publisher-returns: true # 开启消息路由失败回调(比如交换机找不到队列)
    template:
      mandatory: true # 强制要求路由失败时触发return回调(否则消息直接丢弃)
    # ========== 2. 消费者可靠性配置 ==========
    listener:
      simple:
        acknowledge-mode: manual # 手动ACK(核心!禁止自动ACK,避免消费者未处理完就确认)
        concurrency: 1
        max-concurrency: 5
        retry:
          enabled: true # 开启消费重试
          max-attempts: 3 # 最大重试次数
          initial-interval: 1000ms # 首次重试间隔
          multiplier: 2 # 重试间隔倍数(第二次2s,第三次4s)
        default-requeue-rejected: false # 重试失败后不重新入队(避免死循环)
    # ========== 3. 连接池配置(可选,提升稳定性) ==========
    connection-timeout: 5000ms
    cache:
      channel:
        size: 10
        checkout-timeout: 1000ms
2. 生产者侧:确保消息发送成功(注解 + 回调)

通过「生产者确认(Confirm)+ 路由失败回调(Return)」确保消息到达 MQ,失败则重试 / 记录日志兜底。

(1)生产者配置类:开启回调(注解方式)
复制代码
package com.example.rabbitmq.config;

import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 生产者可靠性配置(注解式)
 */
@Configuration
public class RabbitProducerConfig {

    /**
     * 配置RabbitTemplate,开启确认和返回回调
     */
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        
        // 1. 生产者确认回调:确认消息是否到达交换机
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                System.out.println("[生产者] 消息成功到达交换机 | correlationId:" + correlationData.getId());
            } else {
                // 消息未到达交换机,这里做重试/入库/告警处理
                System.err.println("[生产者] 消息未到达交换机 | 原因:" + cause + " | correlationId:" + correlationData.getId());
                // 示例:调用重试方法(实际生产中建议用定时任务/重试框架)
                // retrySendMessage(correlationData.getId());
            }
        });
        
        // 2. 路由失败回调:消息到达交换机但未路由到队列
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            String msgContent = new String(message.getBody());
            // 路由失败,这里做重试/入库/告警处理
            System.err.println("[生产者] 消息路由失败 | 交换机:" + exchange + " | 路由键:" + routingKey + " | 消息:" + msgContent);
        });
        
        return rabbitTemplate;
    }
}
(2)生产者业务类:发送消息并携带唯一标识
复制代码
package com.example.rabbitmq.producer;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**
 * 可靠消息生产者(注解式)
 */
@Component
public class ReliableProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 发送可靠消息(携带唯一ID,用于确认和重试)
     */
    public void sendReliableMessage(String exchange, String routingKey, String message) {
        // 1. 生成唯一CorrelationId(用于追踪消息)
        String correlationId = UUID.randomUUID().toString();
        CorrelationData correlationData = new CorrelationData(correlationId);
        
        try {
            // 2. 发送消息(持久化消息)
            rabbitTemplate.convertAndSend(
                exchange, 
                routingKey, 
                message,
                // 3. 设置消息持久化(核心!避免MQ宕机丢失)
                msg -> {
                    msg.getMessageProperties().setDeliveryMode(org.springframework.amqp.core.MessageDeliveryMode.PERSISTENT);
                    return msg;
                },
                correlationData
            );
            System.out.println("[生产者] 消息发送请求已提交 | correlationId:" + correlationId + " | 消息:" + message);
        } catch (Exception e) {
            // 发送异常(比如网络问题),直接入库兜底
            System.err.println("[生产者] 消息发送异常 | correlationId:" + correlationId + " | 原因:" + e.getMessage());
            // 示例:将消息存入数据库,后续通过定时任务重试
            // saveMessageToDb(correlationId, exchange, routingKey, message);
        }
    }
}
3. MQ 服务端:确保消息持久化(注解配置)

通过 @Exchange@Queuedurable = "true" 声明持久化的交换机和队列,结合消息的持久化配置,确保 MQ 宕机重启后消息不丢失。

复制代码
package com.example.rabbitmq.consumer;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * 可靠消息消费者(注解式 + 手动ACK)
 */
@Component
public class ReliableConsumer {

    /**
     * 消费者:手动ACK + 持久化队列/交换机
     */
    @RabbitListener(
            bindings = @QueueBinding(
                    // 1. 队列持久化(durable = "true")
                    value = @Queue(name = "reliable.queue", durable = "true",
                            // 可选:配置死信队列(重试失败后进入)
                            arguments = {
                                    @Argument(name = "x-dead-letter-exchange", value = "deadletter.exchange"),
                                    @Argument(name = "x-dead-letter-routing-key", value = "deadletter.key")
                            }),
                    // 2. 交换机持久化(durable = "true")
                    exchange = @Exchange(name = "reliable.exchange", type = "direct", durable = "true"),
                    key = "reliable.key"
            )
    )
    public void consumeReliableMessage(String message, Channel channel, Message msg) throws IOException {
        long deliveryTag = msg.getMessageProperties().getDeliveryTag();
        try {
            // ========== 核心:业务处理逻辑 ==========
            System.out.println("[消费者] 开始处理消息 | deliveryTag:" + deliveryTag + " | 消息:" + message);
            // 模拟业务处理(比如入库、调用接口)
            // doBusinessLogic(message);
            
            // ========== 处理成功:手动确认(ACK) ==========
            channel.basicAck(deliveryTag, false); // false:只确认当前消息,不批量
            System.out.println("[消费者] 消息处理成功,已确认 | deliveryTag:" + deliveryTag);
        } catch (Exception e) {
            System.err.println("[消费者] 消息处理失败 | deliveryTag:" + deliveryTag + " | 原因:" + e.getMessage());
            // ========== 处理失败:拒绝并丢弃(重试后进入死信队列) ==========
            // requeue = false:不重新入队(避免死循环),消息会进入死信队列
            channel.basicReject(deliveryTag, false);
            // 可选:如果需要批量拒绝,用 basicNack
            // channel.basicNack(deliveryTag, false, false);
        }
    }

    /**
     * 死信队列消费者:处理重试失败的消息(人工排查)
     */
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(name = "deadletter.queue", durable = "true"),
                    exchange = @Exchange(name = "deadletter.exchange", type = "direct", durable = "true"),
                    key = "deadletter.key"
            )
    )
    public void consumeDeadLetterMessage(String message) {
        // 死信消息处理:记录日志、告警、人工介入
        System.err.println("[死信消费者] 收到重试失败的消息 | 消息:" + message);
        // 示例:发送告警邮件/短信
        // sendAlarm(message);
    }
}
4. 补充:死信队列(兜底重试失败的消息)

死信队列是「最后一道防线」,用于接收重试失败的消息,避免消息丢失且便于人工排查。上面的消费者代码中已经通过 @Argument 注解配置了死信队列的绑定关系,无需额外配置。

三、关键细节说明

1. 生产者侧核心保障
  • publisher-confirm-type: correlated:开启异步确认,确保消息到达交换机;
  • publisher-returns: true + mandatory: true:确保消息路由失败时触发回调,而非直接丢弃;
  • CorrelationData:携带唯一 ID,用于追踪消息的确认状态,方便重试 / 兜底;
  • 消息持久化:setDeliveryMode(PERSISTENT),确保消息在 MQ 中持久化存储。
2. MQ 服务端核心保障
  • 交换机 / 队列持久化:@Exchange(durable = "true") + @Queue(durable = "true"),确保 MQ 宕机重启后交换机和队列不丢失;
  • 消息持久化:结合生产者的 PERSISTENT 配置,消息会被写入磁盘,而非仅存于内存。
3. 消费者侧核心保障
  • 手动 ACK:acknowledge-mode: manual,只有业务处理完成后才调用 basicAck 确认,避免消费者宕机导致消息丢失;
  • 消费重试:开启重试机制,避免网络抖动等临时问题导致的处理失败;
  • 重试失败后拒绝入队:default-requeue-rejected: false + basicReject(deliveryTag, false),避免死循环,同时将消息送入死信队列;
  • 死信队列:兜底重试失败的消息,便于人工排查和处理。

四、测试验证

启动项目后,调用生产者发送消息,模拟不同场景验证可靠性:

  1. 生产者发送失败:关闭 RabbitMQ,发送消息 → 触发 Confirm 回调,记录失败日志;
  2. 路由失败:发送到不存在的路由键 → 触发 Return 回调,记录路由失败日志;
  3. 消费者处理失败:在业务逻辑中抛出异常 → 触发重试(3 次),最后进入死信队列;
  4. MQ 宕机重启:发送消息后关闭 MQ 再重启 → 消息仍存在,消费者重启后可正常消费。

总结

  1. 生产者侧 :开启 publisher-confirm + publisher-returns,携带 CorrelationData 追踪消息,失败则重试 / 入库兜底;
  2. MQ 服务端 :通过注解配置 durable = "true" 实现交换机 / 队列持久化,消息设置为 PERSISTENT 持久化存储;
  3. 消费者侧:开启手动 ACK,配置消费重试和死信队列,确保处理完成后才确认,失败则兜底到死信队列。

这一套组合拳能覆盖 99% 的生产场景,确保消息端到端不丢失。核心注解 / 配置是:durable = "true"(持久化)、acknowledge-mode: manual(手动 ACK)、publisher-confirm-type: correlated(生产者确认)。

相关推荐
泡泡以安5 天前
【爬虫教程】第4章:HTTP客户端库深度定制(httpx/aiohttp)
爬虫·http·httpx
清水白石0089 天前
构建高性能异步 HTTP 客户端:aiohttp 与 httpx 实战解析与性能优化
python·http·性能优化·httpx
时光803.14 天前
快速搭建青龙面板Docker教程
windows·ubuntu·bash·httpx
清水白石00818 天前
《requests vs httpx:Python 网络请求库的全面对比与实战指南》
网络·python·httpx
哆来A梦没有口袋1 个月前
Python入门学习三
httpx·requests·python的async·python的await·python的http·python的http请求·python的异步
介一安全3 个月前
资产信息收集与指纹识别:HTTPX联动工具实战指南
网络安全·安全性测试·httpx·安全工具
网硕互联的小客服4 个月前
SSL部署完成,https显示连接不安全如何处理?
ssl·httpx
万粉变现经纪人4 个月前
如何解决pip安装报错ModuleNotFoundError: No module named ‘python-dateutil’问题
开发语言·ide·python·pycharm·pandas·pip·httpx
万粉变现经纪人4 个月前
如何解决pip安装报错ModuleNotFoundError: No module named ‘websockets’问题
ide·pycharm·beautifulsoup·pandas·fastapi·pip·httpx