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(生产者确认)。

相关推荐
serve the people3 天前
python环境搭建 (十三) httpx和aiohttp
开发语言·python·httpx
深蓝电商API5 天前
httpx 异步客户端处理 WebSocket 数据
websocket·网络协议·httpx
SunnyRivers6 天前
HTTPX vs Requests vs AIOHTTP特征和性能对比指南
httpx·requests·aiohttp
SunnyRivers6 天前
Python 的下一代 HTTP 客户端 HTTPX 特性详解
python·httpx
深蓝电商API8 天前
httpx库异步爬虫实战对比aiohttp
爬虫·httpx
qq_3814549911 天前
Python httpx:现代HTTP客户端全解析
httpx
曲幽12 天前
FastAPI异步多线程:从踩坑到精通,解锁高性能API的正确姿势
python·flask·fastapi·web·thread·async·httpx·asyncio
SunnyRivers13 天前
Python 中的 HTTP 客户端:Requests、HTTPX 与 AIOHTTP 对比
python·httpx·requests·aiohttp·区别
数据知道22 天前
如何使用 httpx + SQLAlchemy 异步高效写入上亿级图片链接与MD5到 PostgreSQL
数据库·postgresql·httpx
曲幽1 个月前
重构FastAPI生产部署:用异步网关与无服务器计算应对高并发
python·serverless·fastapi·web·async·httpx·await·asyncio