消息从生产者-->交换机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 服务器,服务器会按流程处理:
- 验证消息是否发送到了存在的交换机(若交换机不存在且未设置
mandatory=false,可能直接失败); - 根据交换机类型和路由键(routingKey),将消息路由到匹配的队列;
- 若消息或队列设置了持久化,服务器会将消息写入磁盘(确保重启不丢失);
- 当以上步骤全部完成(或确定失败),服务器会生成
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 "不存在交换机消息发送完成";
}
}
1320

被折叠的 条评论
为什么被折叠?



