消息可靠性
消息丢失可能发生在每个交互端,需要保证消息的可靠性需要从生产者、mq服务端、生产者三个方面考虑。
生产者
1、连接重试
网络异常会导致消息发送失败,消费者端可以开启重试机制,保证消息不会因网络波动导致发送失败,但是重试是阻塞式的,会阻塞用户线程,生产需谨慎开启。
java
spring:
rabbitmq:
# 连接超时
connection-timeout: 1000
template:
retry:
enabled: true # ✅开启发送重试
initial-interval: 1000 # 第一次重试等待1s
multiplier: 1.5 # 退避倍数:下一次等待时间=上一次*1.5
max-interval: 5000 # 最大等待上限
max-retries: 3 #最大重试次数
2、开启生产者确认
生产者可以开启消息确认机制和退回机制,确认和回退函数都是设置在rabbitTemplate上,若是只有一个rabbitTemplate默认所有消息放送都会生效。需要指定部分生效可以在容器中注入多个rabbitTemlate。
java
spring:
rabbitmq:
publisher-confirm-type: correlated # 消息确认
publisher-returns: true #消息回退
1、开启 Publisher Confirm
确认机制是确认消息是否到达交换机,有三种模式可选:SIMPLE-同步、CORRELATED-异步、NONE-关闭(默认)。正常情况消息都能到达交换机,即使达到内存阈值,rabbitmq默认会阻塞不接受新消息投递,也不会触发confirm回调。

2、开启publisher-returns
开机消息回退机制后,当消息到达了交换机,但是没有成功路由到队列,消息将被回退。通过回调函数处理回退消息,一般由路由key配置错误导致。回调函数是设置在rabbitTemplate中,一个应用只会设置一个回调函数,它处理所有回退消息。
通过ApplicationContextAware接口完成回调函数设置

发送一个路由key不存在的消息,回调接口将被执行。

消息队列端
1、消息持久化
当生产者发送消息到消息队列后,若没有开启消息持久化,消息还是存储在内存中。若消费者还没消费,mq服务发生宕机就会导致消息丢失。所以要保证消息的可靠性mq服务端需开启持久化。除了消息,mq服务中还有交换机和队列也都需要开启持久化,保证服务端宕机后数据不丢失。
2、Lazy queue
惰性队列是RabbitMQ3.6.0新增的特性,惰性队列默认将消息直接存储到磁盘,内存中只保留少量消息。内存占用量低,性能更好。普通队列默认内存优先,内存满了再刷盘,刷盘是有性能开销。惰性队列更适合高并发场景和大量消息堆积场景。
消费端
消息队列默认将消息投递到消费者后就删除对应消息,若是消费端在处理消失时异常,该条消息就会丢失。要保证消息在消费端不丢失,需要消费者自行开启自行确认ack。告诉消息队列对应消息是否消费成功。springAMQP中已经实现了消息确认机制,通过添加配置就实现不同方式的消息确认。
java
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # manual 手动ACK;auto自动;none不确认
自动模式是通过spring 的aop 对消息处理逻辑做环绕增强,当消息处理正常时自动返回basicAck,若消息处理抛出异常则返回basicNack (requeue=true)进行消息重试。none不确认,消息到达消费端后消息就会从队列中删除存在消息丢失风险。manual手动方式需要消费者自行返回确认状态,例如:
java
@RabbitListener(queues = "my.direct.queue")
public void handleMessage(Message message, Channel channel) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// 1.业务处理逻辑
log.info("处理消息:{}", new String(message.getBody()));
// 2.业务成功,手动ACK,告诉MQ可以删除消息
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.error("消息处理异常", e);
// 3.业务失败:basicNack,消息重回队列,重新投递
// multiple=false:只拒绝当前这条消息
channel.basicNack(deliveryTag, false, true);
}
}
异常类型及是否重试如下:
| 场景 | 底层调用 | requeue | 消息行为 |
|---|---|---|---|
| 消费方法正常返回 | basicAck | --- | 删除消息 |
| 普通业务异常 | basicNack | true(默认) | 重回队列重试,无限循环 |
| AmqpRejectAndDontRequeueException | basicNack | false | 拒绝,不回队列,进死信 |
| ImmediateAcknowledgeAmqpException | basicAck | --- | 确认删除,不重试 |
| ImmediateRequeueAmqpException | basicNack | true | 强制重回队列重试 |
| 消息转换 / 参数校验异常(还没进消费方法) | basicNack | false | 拒绝,进死信 |
这三个异常默认都需要业务手动抛出,只有搭配 RetryTemplate 重试恢复器的时候,框架才会自动抛出。
- AmqpRejectAndDontRequeueException:业务遇到不可修复毒消息手动抛出;本地重试耗尽,RejectAndDontRequeueRecoverer 也会自动抛出。底层 nack、requeue=false,消息进死信队列。
- ImmediateAcknowledgeAmqpException:只能业务手动抛出,底层直接 basicAck,消息直接删除,不重试也不进死信。
- ImmediateRequeueAmqpException:业务手动抛出,或者 ImmediateRequeueMessageRecoverer 自动抛出;强制 nack requeue=true,消息重回队列重试,不受 default-requeue-rejected=false 全局配置影响。
消费端本地重试
通过添加配置,可以实现消费端的本地重试
java
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启重试
initial-interval: 1000ms # 初始重试间隔
multiplier: 2 # 重试间隔乘数
max-retries: 3 # 最大重试次数
max-interval: 10000ms # 最大重试间隔
重试次数耗尽后,默认会抛出AmqpRejectAndDontRequeueException 异常,消息不会重新回队列,若是有绑定实现交换机,则交给死信交换机否则丢弃。也可以自己创建一个MessageRecoverer,处理重试超次数消息。
例如,创建一个处理错误消息的交换机error.direct和队列error.queue,并创建绑定关系,通过路由key"error"路由,消息重试超次数时将消息投递到error.direct交换机。
java
/**
* 处理时重试超次数的消息
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.rabbitmq.listener.simple.retry", name = "enabled", havingValue = "true")
public class errorExchangeConfig {
@Bean
public DirectExchange errorExchange() {
// 创建报错交换机
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue() {
// 创建报错队列
return new Queue("error.queue");
}
@Bean
public Binding errorBinding() {
// 绑定报错队列和交换机,路由key为error
return BindingBuilder.bind(errorQueue()).to(errorExchange()).with("error");
}
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
// 处理重试超次数的消息
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
}
消息幂等
消息幂等的本质:同一条消息多次投递,业务只执行一次,不会产生重复数据 / 重复扣款 。
重复消息来源:消费者处理成功,ACK 前网络断开 / 进程宕机 → MQ 重投消息。
1、数据库唯一索引
每条消息携带唯一业务编号(消息 ID / 业务单号) ,业务表对该字段建立唯一索引。
消费时直接插入;重复投递时,数据库抛出唯一索引冲突异常,捕获异常直接返回,不重复执行业务。
适用:新增订单、流水记录等写入场景。
2、数据库状态表
单独一张幂等记录表,存储消息唯一 ID + 消息处理状态(未处理 / 处理成功 / 处理失败) 。
消费逻辑:
- 开启事务,先查询幂等表是否存在该 msgId
- 不存在:执行业务 SQL,插入幂等记录;提交事务
- 已存在:直接跳过,返回成功
注意:必须给 msgId 加唯一索引,防止并发情况下两个线程同时查询都查不到,导致重复执行业务(竞态问题)。
3、Redis 幂等
消费前,用 SETNX(带过期时间)存入消息唯一 ID。
- set 成功:代表首次消费,执行业务逻辑;
- set 失败:消息已处理,直接返回。
设置过期时间,清理历史消息,防止 Redis 内存溢出。
适用:高并发、允许兜底补偿的场景,不能单独作为资金类强一致性场景的唯一幂等方案。
4、业务自身天然幂等
理:改造业务逻辑,接口本身执行多少次结果不变。
例如:更新语句 update order set status=1 where id=xxx and status=0
条件更新,重复执行不会改变数据。第一次执行影响行数 = 1;再次执行影响行数 = 0。
5、消息 ID + 本地缓存
Caffeine/Guava 本地缓存记录已处理 msgId,设置过期时间。
✅ 优点:速度极快。
❌ 缺点:单机有效,集群环境失效;重启丢失缓存,只能做一层前置过滤,不能作为唯一方案。一般搭配 Redis / 数据库兜底。
延时消息
消息发送到mq后不立即处理,而是等待一定时间后再处理,使用于抢票订单超时等场景。
1、死信交换机
死信:消息无法被正常消费,被转发到死信交换机 DLX ,再路由到死信队列 DLQ。
前提:队列必须预先配置 x-dead-letter-exchange(DLX),否则消息满足下面条件只会直接丢弃,不会变成死信。
死信产生条件
- 消息被拒绝,且 requeue=false
调用basicNack/basicReject,requeue=false
- Spring AUTO 模式抛出
AmqpRejectAndDontRequeueException就属于这种场景 - 消息转换异常
MessageConversionException,框架自动 nack、requeue=false
- 消息过期(TTL 超时)
两种 TTL 设置方式:
- 消息级别 TTL :发送消息时设置
expiration属性,单条消息过期时间 - 队列级别 TTL :队列参数
x-message-ttl,队列内所有消息统一过期时间
队列 TTL:消息一旦到时间立刻判定过期;
消息 TTL:只有消息到达队列头部才会判断是否过期(坑!)
消息过期后,变为死信。
- 队列达到最大长度(消息数超限)
队列声明参数x-max-length:队列最多容纳 N 条消息。
当队列消息数量已满,新消息入队会让队头旧消息变成死信。
注意:是丢弃队头,不是丢弃新来的消息。
还有
x-max-length-bytes:队列占用总字节数上限,达到上限,队头消息死信。
设置死信交换机
队列若想设置死信交换机可以通过设置队列参数:x-dead-letter-exchange,当队列中出现死信时,就会被投递到该交换机。
关键参数
x-dead-letter-exchange:死信交换机名称(必填)x-dead-letter-routing-key:死信转发时使用的 routingKey(可选;不填默认沿用原消息 routingKey)
注意:死信交换机可以是 direct /topic/fanout,和普通交换机无区别。
2、延时交换机插件
rabbitmq-delayed-message-exchange 延时交换机插件,安装插件后,新增一种交换机类型:x-delayed-message。
生产者发送消息,带上 x-delay 头(毫秒延时);
交换机在 Broker 内部暂存消息,等到延时时间到达,才路由消息到目标队列。
安装插件
java
# 下载对应rabbitmq版本插件,放到plugins目录
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
# 重启rabbitmq生效
Spring 代码声明延时交换机
java
@Bean
public CustomExchange delayedExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct"); // 底层路由类型
return new CustomExchange("delayed.exchange", "x-delayed-message", true, false, args);
}
@Bean
public Queue targetQueue(){
return QueueBuilder.durable("target.queue").build();
}
@Bean
public Binding binding(Queue targetQueue, CustomExchange delayedExchange){
return BindingBuilder.bind(targetQueue).to(delayedExchange).with("delay.key").noargs();
}
发送延时消息
java
MessageProperties props = MessagePropertiesBuilder.newInstance()
.setHeader("x-delay", 5000) // 延时5秒
.build();
rabbitTemplate.send("delayed.exchange", "delay.key", new Message("msg".getBytes(), props));