RabbitMQ 高级篇:消息可靠性、幂等性与延迟消息
整理自黑马程序员《SpringCloud微服务开发与实战》项目
对应课程章节:MQ高级(消息可靠性、LazyQueue、死信交换机、延迟插件)
一、消息可靠性:不丢消息的三道屏障
消息丢失可能发生在生产者 → MQ → 消费者的任何一个环节,解决方案需层层设防。
1. 生产者可靠性(发送端确认)

问题:网络抖动导致消息未到达交换机,或路由键错误导致消息无法投递到队列。
解决方案 :开启 Publisher Confirm 和 Publisher Return 机制。
配置与代码实现:
yaml
spring:
rabbitmq:
# 1. 开启确认机制(correlated:异步回调,推荐)
publisher-confirm-type: correlated
# 2. 开启回退机制(消息无法路由到队列时返回)
publisher-returns: true
template:
mandatory: true # 必须设为true,ReturnCallback才生效
java
@Slf4j
@Configuration
public class MqConfirmConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 设置ReturnCallback(路由失败回调)
rabbitTemplate.setReturnsCallback(returned -> {
log.error("消息路由到队列失败,交换机:{},路由键:{},消息:{}",
returned.getExchange(), returned.getRoutingKey(), returned.getMessage());
// 可在此处记录日志或重发消息
});
// 设置ConfirmCallback(消息到达交换机回调)
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.debug("消息发送到交换机成功,ID:{}", correlationData.getId());
} else {
log.error("消息发送到交换机失败,ID:{},原因:{}", correlationData.getId(), cause);
}
});
}
}
注意 :发送消息时需传入
CorrelationData(包含唯一ID),用于在回调中区分消息。
2. MQ可靠性(数据持久化)
问题:MQ宕机导致内存中的消息丢失。
解决方案:将消息、队列、交换机全部持久化到磁盘。
| 组件 | 持久化配置(SpringAMQP默认行为) | 说明 |
|---|---|---|
| 交换机 | @Exchange(durable = "true") |
SpringAMQP 默认 durable=true |
| 队列 | @Queue(durable = "true") |
必须显式声明,否则重启MQ队列消失 |
| 消息 | MessageDeliveryMode.PERSISTENT |
SpringAMQP 默认已设置 |
生产建议 :直接使用 Lazy Queue(惰性队列),它是解决消息堆积和持久化的终极方案。
- 原理:消息直接写入磁盘,消费时才加载到内存。
- 配置 :在队列声明时设置
x-queue-mode: lazy,或通过管理界面设置。
3. 消费者可靠性(消费端确认)
问题:消费者拿到消息后,业务处理失败或消费者宕机,导致消息丢失。
解决方案 :采用 ACK 确认机制 + 失败重试策略。
yaml
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 自动ACK(业务执行成功自动确认,异常则拒绝)
retry:
enabled: true # 开启消费者重试
max-attempts: 3 # 最大重试次数
initial-interval: 1000ms # 重试间隔
失败处理策略:
- RejectAndDontRequeueRecoverer(默认):重试耗尽后,丢弃消息(慎用)。
- RepublishMessageRecoverer(推荐):重试耗尽后,将消息投递到指定的"异常交换机",人工介入处理。
二、消费者幂等性:防止重复消费
问题根源:网络抖动导致 MQ 未收到 ACK,消息被重新投递,造成业务重复执行(如:扣款两次)。
解决方案 :"业务判断"优于"消息ID去重"。
1. 业务状态判断法(推荐)
在处理消息前,先查询当前业务状态。以"支付成功"消息为例:
java
@Transactional
public void handlePaySuccess(Long orderId) {
// 1. 查询当前订单状态
Order order = orderMapper.selectById(orderId);
// 2. 幂等判断:只有未支付状态的订单才处理
if (order != null && order.getStatus() == OrderStatus.UNPAID) {
// 执行业务:更新为已支付
order.setStatus(OrderStatus.PAID);
orderMapper.updateById(order);
} else {
// 已支付过,直接确认消息(幂等)
log.info("订单已处理,跳过重复消费,订单ID:{}", orderId);
}
}
2. 唯一约束/Redis防重
- 数据库唯一索引:利用数据库主键或业务唯一键(如:支付流水号)防止重复插入。
- Redis Token :消费前执行
setnx(key, 1),若已存在则说明已消费。
三、延迟消息:订单超时未支付取消
业务场景:用户下单后30分钟未支付,自动取消订单并释放库存。
方案一:死信交换机(DLX)+ TTL(不推荐,仅作了解)

原理:设置一个"缓冲队列"(无消费者),并给该队列设置 TTL(过期时间)和死信交换机。消息过期后变成"死信",自动转发到真正的业务队列。
缺陷:
- 队列级 TTL:队列中所有消息的延迟时间必须一致。
- 灵活性差:无法实现"前一条消息延迟10s,后一条延迟1h"的场景。
方案二:RabbitMQ 延迟插件(推荐)
步骤:
- 安装插件 :在 RabbitMQ 容器中执行
rabbitmq-plugins enable rabbitmq_delayed_message_exchange。 - 声明延迟交换机 :类型必须为
x-delayed-message,并指定路由模式(如direct)。 - 发送消息 :在
MessageProperties中设置setDelay(毫秒)。
代码实现:
java
// 1. 声明延迟交换机(注解方式)
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "order.delay.queue", durable = "true"),
exchange = @Exchange(
name = "order.delay.exchange",
type = "x-delayed-message", // 关键:使用延迟插件类型
arguments = @Argument(name = "x-delayed-type", value = "direct")
),
key = "order.delay"
))
// 2. 发送延迟消息(下单时)
public void sendDelayMessage(Long orderId) {
rabbitTemplate.convertAndSend("order.delay.exchange", "order.delay", orderId, message -> {
// 设置延迟时间(30分钟)
message.getMessageProperties().setDelay(30 * 60 * 1000);
return message;
});
}
// 3. 监听延迟队列(处理超时订单)
@RabbitListener(queues = "order.delay.queue")
public void cancelOrder(Long orderId) {
// 查询订单状态,若未支付则取消
orderService.cancelOrderIfUnpaid(orderId);
}
四、黑马商城业务改造总结
| 业务场景 | 可靠性保障 | 关键配置/代码 |
|---|---|---|
| 支付成功通知 | 生产者确认 + 消费者幂等 | publisher-confirm-type: correlated + 订单状态判断 |
| 下单清理购物车 | 消息持久化 + LazyQueue | 队列声明 durable=true,使用惰性队列防堆积 |
| 超时订单取消 | 延迟插件 + 幂等性 | x-delayed-message 交换机,防止重复取消 |
最佳实践口诀:
- 消息必持久:队列、交换机、消息全部持久化。
- 确认不能少:生产端 Confirm,消费端 ACK。
- 幂等靠业务:不要依赖消息ID,直接判断业务状态。
- 延迟用插件 :死信队列已过时,
rabbitmq_delayed_message_exchange是首选。