【RabbitMQ】如何保障消息一定能发到MQ?

消息从生产者-->交换机Exchange-->queue,如何能保证生产者到交换机一定是能够不丢失的?

1.开启Publisher Confirm机制

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    publisher-confirm-type: correlated  # 开启发布确认(correlated:回调返回确认结果)
    publisher-returns: true  # 可选,配合 return 机制处理路由失败消息

publisher-confirm-type 取值说明:

  • none:关闭确认(默认);
  • correlated发送消息后,通过回调返回确认结果(推荐)
  • simple:类似原生同步确认,可调用 waitForConfirms() 或 waitForConfirmsOrDie()

注意这里的ACK是RabbitMQ返回给生产者的消息

而我们之前说到的ACK是消费者确认给Queue的消息

RabbitMQ 提供Publisher Confirm(生产者确认) 机制,原理是:生产者开启确认模式后,RabbitMQ 在收到消息并处理完成后(如路由到队列、持久化等),会向生产者返回ack(成功)或nack(失败),生产者根据返回结果决定是否重试。

具体RabbitMQ服务器是怎么返回ACK/NACK来生产者的呢?

当生产者通过信道(Channel)发送消息后,消息会先到达 RabbitMQ 服务器,服务器会按流程处理:

  1. 验证消息是否发送到了存在的交换机(若交换机不存在且未设置mandatory=false,可能直接失败);
  2. 根据交换机类型和路由键(routingKey),将消息路由到匹配的队列;
  3. 若消息或队列设置了持久化,服务器会将消息写入磁盘(确保重启不丢失);
  4. 当以上步骤全部完成(或确定失败),服务器会生成ack(成功)或nack(失败)信号,通过原信道返回给生产者。

2.开启publisher-returns

publisher-returns:默认情况下,若消息发送到交换机后无法路由到任何队列(例如:路由键routingKey不匹配、队列未与交换机绑定等),RabbitMQ 会直接丢弃该消息,生产者无法感知这一过程,导致消息 “无声丢失”。

publisher-returns机制的作用就是让生产者能主动获取这些 “路由失败的消息”,从而进行补救(如重试发送、记录日志、存入死信队列等),避免消息因路由问题丢失。

1.配置文件中publisher-returns:true

2.RabbitTemplate要进行开启mandatory=true

import org.springframework.amqp.core.Message;
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;

@Configuration
public class RabbitConfig {

    // 定制RabbitTemplate,配置返回机制
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        
        // 1. 设置mandatory=true:路由失败时消息返回,而非丢弃
        rabbitTemplate.setMandatory(true);
        
        // 2. 注册ReturnCallback:处理返回的未路由消息
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 消息路由失败时触发此回调
            System.err.println("消息路由失败:");
            System.err.println("  消息内容:" + new String(message.getBody()));
            System.err.println("  返回码:" + replyCode);
            System.err.println("  描述:" + replyText);
            System.err.println("  交换机:" + exchange);
            System.err.println("  路由键:" + routingKey);
            
            // 此处可进行补救处理,例如:
            // - 修正路由键后重新发送
            // - 记录到数据库或日志,人工介入
            // - 转发到死信队列
        });
        
        return rabbitTemplate;
    }
}


两者结合,真实项目中的配置案例

1.xml

<dependencies>
    <!-- Spring Boot 核心 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!-- Spring AMQP(RabbitMQ 集成) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <!-- 日志(方便观察回调) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2.配置文件

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    # 开启 publisher confirm(确认机制)
    # 类型为 CORRELATED:通过回调返回确认结果(推荐)
    publisher-confirm-type: CORRELATED
    # 开启 publisher returns(返回机制)
    publisher-returns: true

3.核心配置类

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
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;

@Configuration
@Slf4j
public class RabbitConfig {

    // -------------------------- 交换机和队列配置(用于测试) --------------------------
    // 1. 正常交换机(已绑定队列,用于测试路由成功场景)
    public static final String NORMAL_EXCHANGE = "normal.exchange";
    public static final String NORMAL_QUEUE = "normal.queue";
    public static final String NORMAL_ROUTING_KEY = "normal.key";

    // 2. 无绑定交换机(未绑定任何队列,用于测试路由失败场景)
    public static final String NO_BIND_EXCHANGE = "no.bind.exchange";
    public static final String NO_BIND_ROUTING_KEY = "no.bind.key";

    // 3. 不存在的交换机(用于测试发送失败场景)
    public static final String NON_EXISTENT_EXCHANGE = "non.existent.exchange";


    // 声明正常交换机(持久化)
    @Bean
    public DirectExchange normalExchange() {
        return ExchangeBuilder.directExchange(NORMAL_EXCHANGE)
                .durable(true) // 持久化
                .build();
    }

    // 声明正常队列(持久化)
    @Bean
    public Queue normalQueue() {
        return QueueBuilder.durable(NORMAL_QUEUE)
                .build();
    }

    // 绑定正常交换机和队列(路由键匹配)
    @Bean
    public Binding normalBinding(DirectExchange normalExchange, Queue normalQueue) {
        return BindingBuilder.bind(normalQueue)
                .to(normalExchange)
                .with(NORMAL_ROUTING_KEY);
    }

    // 声明无绑定交换机(持久化,但不绑定任何队列)
    @Bean
    public DirectExchange noBindExchange() {
        return ExchangeBuilder.directExchange(NO_BIND_EXCHANGE)
                .durable(true)
                .build();
    }


    // -------------------------- 定制 RabbitTemplate(核心) --------------------------
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);

        // 1. 设置 mandatory=true:路由失败时返回消息(配合 returns 机制)
        rabbitTemplate.setMandatory(true);

        // 2. 配置 publisher confirm 回调(处理消息是否被服务器接收)
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            // correlationData:发送消息时传入的关联数据(可用于匹配消息)
            // ack:true=服务器接收成功;false=接收失败
            // cause:失败原因(ack=false 时非空)
            String messageId = correlationData != null ? correlationData.getId() : "未知ID";
            if (ack) {
                log.info("[ConfirmCallback] 消息处理成功,消息ID:{}", messageId);
                // 实际项目:可删除本地待确认消息缓存(如数据库记录)
            } else {
                log.error("[ConfirmCallback] 消息处理失败,消息ID:{},原因:{}", messageId, cause);
                // 实际项目:重试发送(需限制重试次数,避免死循环)
                retrySendFailedMessage(rabbitTemplate, correlationData, cause);
            }
        });

        // 3. 配置 publisher returns 回调(处理路由失败的消息)
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // message:路由失败的消息本身
            // replyCode/replyText:失败编码和描述
            // exchange/routingKey:发送时使用的交换机和路由键
            String messageContent = new String(message.getBody());
            log.error("[ReturnCallback] 消息路由失败:\n" +
                            "消息内容:{}\n" +
                            "返回码:{}\n" +
                            "描述:{}\n" +
                            "交换机:{}\n" +
                            "路由键:{}",
                    messageContent, replyCode, replyText, exchange, routingKey);
            // 实际项目:处理路由失败消息(如修正路由键重试、存入死信队列)
            handleReturnedMessage(message, exchange, routingKey);
        });

        return rabbitTemplate;
    }


    // -------------------------- 工具方法(实际项目的处理逻辑) --------------------------
    /**
     * 重试发送 confirm 失败的消息(简化版,实际需加重试次数限制)
     */
    private void retrySendFailedMessage(RabbitTemplate rabbitTemplate, CorrelationData correlationData, String cause) {
        // 示例:仅重试1次(实际可用 Redis/数据库记录重试次数)
        if (correlationData != null && correlationData.getReturnedMessage() != null) {
            Message message = correlationData.getReturnedMessage();
            String exchange = message.getMessageProperties().getReceivedExchange();
            String routingKey = message.getMessageProperties().getReceivedRoutingKey();
            log.info("[重试发送] 消息ID:{},交换机:{},路由键:{}", correlationData.getId(), exchange, routingKey);
            rabbitTemplate.send(exchange, routingKey, message, correlationData);
        }
    }

    /**
     * 处理路由失败的消息(示例:修正路由键后重试)
     */
    private void handleReturnedMessage(Message message, String exchange, String oldRoutingKey) {
        // 示例:若原路由键错误,修正为正常路由键重试
        if (NO_BIND_EXCHANGE.equals(exchange) && NO_BIND_ROUTING_KEY.equals(oldRoutingKey)) {
            String newRoutingKey = NORMAL_ROUTING_KEY;
            log.info("[修正路由] 消息从交换机{}的{}路由键,重试到{}的{}",
                    exchange, oldRoutingKey, NORMAL_EXCHANGE, newRoutingKey);
            rabbitTemplate.send(NORMAL_EXCHANGE, newRoutingKey, message, new CorrelationData(message.getMessageProperties().getMessageId()));
        }
    }
}

4.生产者

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
@Slf4j
@RequiredArgsConstructor
public class MessageProducerService {

    private final RabbitTemplate rabbitTemplate;

    /**
     * 发送消息(封装通用逻辑:消息ID、持久化)
     */
    public void sendMessage(String exchange, String routingKey, String content) {
        // 1. 生成唯一消息ID(用于追踪和确认回调匹配)
        String messageId = UUID.randomUUID().toString();

        // 2. 设置消息属性(持久化:deliveryMode=2)
        MessageProperties properties = new MessageProperties();
        properties.setMessageId(messageId);
        properties.setDeliveryMode(MessageProperties.DEFAULT_DELIVERY_MODE); // 2=持久化

        // 3. 构建消息
        Message message = new Message(content.getBytes(), properties);

        // 4. 发送消息(传入CorrelationData用于confirm回调关联)
        CorrelationData correlationData = new CorrelationData(messageId);
        correlationData.setReturnedMessage(message); // 存储消息,方便重试

        log.info("[发送消息] ID:{},交换机:{},路由键:{},内容:{}", messageId, exchange, routingKey, content);
        rabbitTemplate.send(exchange, routingKey, message, correlationData);
    }
}

5.单元测试

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class TestController {

    private final MessageProducerService producerService;

    // 场景1:发送到正常交换机+正确路由键(预期:confirm回调ack,returns不触发)
    @GetMapping("/test/normal")
    public String testNormal() {
        producerService.sendMessage(
                RabbitConfig.NORMAL_EXCHANGE,
                RabbitConfig.NORMAL_ROUTING_KEY,
                "这是一条正常路由的消息"
        );
        return "正常消息发送完成";
    }

    // 场景2:发送到无绑定交换机(预期:confirm回调ack,returns回调触发)
    @GetMapping("/test/no-bind")
    public String testNoBind() {
        producerService.sendMessage(
                RabbitConfig.NO_BIND_EXCHANGE,
                RabbitConfig.NO_BIND_ROUTING_KEY,
                "这是一条路由失败的消息"
        );
        return "无绑定消息发送完成";
    }

    // 场景3:发送到不存在的交换机(预期:confirm回调nack,returns不触发)
    @GetMapping("/test/non-existent")
    public String testNonExistent() {
        producerService.sendMessage(
                RabbitConfig.NON_EXISTENT_EXCHANGE,
                "any.key",
                "这是一条发送失败的消息"
        );
        return "不存在交换机消息发送完成";
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值