通过 Java 注解(Spring AMQP)的方式实现 RabbitMQ 消息不丢失,这是生产环境中必须要解决的核心问题。消息丢失可能发生在生产者发送阶段 、RabbitMQ 服务端存储阶段 、消费者接收处理阶段三个环节,针对每个环节给出注解 + 配置的完整解决方案,确保端到端的消息可靠性。
一、先明确:消息不丢失的核心原则
要保证消息不丢失,必须覆盖三个核心环节的防护:
- 生产者侧:确保消息成功发送到 RabbitMQ(失败则重试 / 兜底);
- MQ 服务端:确保消息持久化(宕机重启后不丢失);
- 消费者侧:确保消息处理完成后再确认(处理失败则重试 / 进入死信队列)。
二、完整实现方案(注解 + 配置)
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 和 @Queue 的 durable = "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),避免死循环,同时将消息送入死信队列; - 死信队列:兜底重试失败的消息,便于人工排查和处理。
四、测试验证
启动项目后,调用生产者发送消息,模拟不同场景验证可靠性:
- 生产者发送失败:关闭 RabbitMQ,发送消息 → 触发 Confirm 回调,记录失败日志;
- 路由失败:发送到不存在的路由键 → 触发 Return 回调,记录路由失败日志;
- 消费者处理失败:在业务逻辑中抛出异常 → 触发重试(3 次),最后进入死信队列;
- MQ 宕机重启:发送消息后关闭 MQ 再重启 → 消息仍存在,消费者重启后可正常消费。
总结
- 生产者侧 :开启
publisher-confirm+publisher-returns,携带CorrelationData追踪消息,失败则重试 / 入库兜底; - MQ 服务端 :通过注解配置
durable = "true"实现交换机 / 队列持久化,消息设置为PERSISTENT持久化存储; - 消费者侧:开启手动 ACK,配置消费重试和死信队列,确保处理完成后才确认,失败则兜底到死信队列。
这一套组合拳能覆盖 99% 的生产场景,确保消息端到端不丢失。核心注解 / 配置是:durable = "true"(持久化)、acknowledge-mode: manual(手动 ACK)、publisher-confirm-type: correlated(生产者确认)。