👋 关于作者
你好,这里是一条泥憨鱼 | 专注于 后端开发、AI 前沿技术与计算机基础 。

🏠 个人主页:一条泥憨鱼
通过技术博客记录技术成长,分享学习心得与经验。期待与您共同交流!
目录
- 可靠性全景
- 生产者可靠性:重连
- 生产者可靠性:确认原理
- 生产者可靠性:确认代码实现
- MQ 可靠性:数据持久化
- MQ 可靠性:Lazy Queue
- 消费者可靠性:消费者确认
- 消费者可靠性:失败重试机制
- 消费者可靠性:业务幂等性
- 延迟消息:什么是延迟消息
- 延迟消息:死信交换机
- 延迟消息:延迟消息插件
- 延迟消息:取消超时订单的基本思路
- 延迟消息:发送与监听检测消息
- 综合案例总结
- 配置与参数速查
- 本篇复习清单
1. 可靠性全景
先看这门课高级篇的目录(也是本章骨架):

课程用一个真实问题切入:支付服务把订单标记为已支付,再通过 MQ 通知交易服务改状态。可交易服务的库里仍然是「未支付」。

钱扣了、订单没变,这是最不能接受的故障。消息可能丢在四个位置:
| 环节 | 可能出的问题 | 对应解决方案 |
|---|---|---|
| 生产者 → MQ | 网络抖动、未连上 Broker、消息没送到 | 生产者重连 + 生产者确认(Confirm / Return) |
| MQ 内部 | 宕机导致内存中的消息丢失;积压打爆内存 | 持久化 + Lazy Queue |
| MQ → 消费者 | 消费者处理失败却已 ack;异常无限重试 | 消费者确认 + 失败重试 + MessageRecoverer |
| 消费者业务 | 消息重复投递导致重复扣款/重复改状态 | 业务幂等 |
2. 生产者可靠性:重连
问题
网络波动时,客户端与 MQ 的连接可能断开,导致发送失败。
解决
开启连接失败后的重试机制:

yaml
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
| 参数 | 含义 |
|---|---|
connection-timeout |
建连超时时间 |
initial-interval |
第一次重试前等待多久 |
multiplier |
退避倍数(1 = 等间隔,2 = 指数退避) |
max-attempts |
含首次在内最多尝试几次 |
⚠️ 重要注意
SpringAMQP 提供的重试机制是 阻塞式 的------多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。
- 业务对性能有要求:建议禁用重试 ,或者只保留
publisher-confirm - 一定要用:合理配置等待时长和重试次数,并把发消息的代码放到异步线程里执行
这条注意点是面试高频:「重试机制有什么缺点?」------阻塞线程,可能把上游拖死。
3. 生产者可靠性:确认原理
重连只能解决「连不上」,解决不了「连上了但消息在路上丢了」。所以需要 MQ 给发送方一个回执。
RabbitMQ 提供了 Publisher Confirm 和 Publisher Return 两种确认机制。开启后,MQ 成功收到消息会返回确认消息给生产者。返回结果有以下几种情况:

| 情况 | 返回 |
|---|---|
| 消息投递到了 MQ,但是路由失败 | 通过 Publisher Return 返回路由异常原因,然后返回 ACK(告知投递成功) |
| 临时消息投递到了 MQ,并且入队成功 | 返回 ACK,告知投递成功 |
| 持久消息投递到了 MQ,并且入队完成持久化 | 返回 ACK,告知投递成功 |
| 其它情况 | 返回 NACK,告知投递失败 |
注意
路由失败也返回 ACK。 因为对 MQ 来说「消息我收到了」,只是没找到队列。所以:
- Confirm 管的是:消息有没有到达 Broker(交换机)
- Return 管的是:消息有没有被路由到队列
只开 confirm 不开 return,等于漏掉了「路由失败」这个场景。 两个必须一起开。
4. 生产者可靠性:确认代码实现
步骤 1:yml 开启

yaml
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制
publisher-confirm-type 三种模式:
| 模式 | 含义 |
|---|---|
none |
关闭 confirm 机制 |
simple |
同步阻塞等待 MQ 的回执消息 |
correlated |
MQ 异步回调方式返回回执消息(推荐) |
simple会同步阻塞,和重连机制一样有阻塞问题;生产环境用correlated。
步骤 2:配置 ReturnCallback
每个 RabbitTemplate 只能配置一个 ReturnCallback,因此需要在项目启动过程中统一配置:

java
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取 RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 设置 ReturnCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
});
}
}
为什么用 ApplicationContextAware 而不是在方法上 @PostConstruct?因为要保证在 RabbitTemplate 初始化完成后再设置回调,避免被其他配置覆盖。
步骤 3:发送时指定消息 ID 和 ConfirmCallback

java
@Test
void testPublisherConfirm() throws InterruptedException {
// 1. 创建 CorrelationData
CorrelationData cd = new CorrelationData();
// 2. 给 Future 添加 ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
// 2.1. Future 发生异常时的处理逻辑,基本不会触发
log.error("handle message ack fail", ex);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
// 2.2. Future 接收到回执的处理逻辑,参数中的 result 就是回执内容
if (result.isAck()) { // result.isAck(),boolean 类型,true 代表 ack 回执,false 代表 nack 回执
log.debug("发送消息成功,收到 ack!");
} else { // result.getReason(),String 类型,返回 nack 时的异常描述
log.error("发送消息失败,收到 nack, reason : {}", result.getReason());
}
}
});
// 3. 发送消息
rabbitTemplate.convertAndSend("hmall.direct", "red1", "hello", cd);
}
三个对象的关系
| 对象 | 作用 |
|---|---|
CorrelationData |
一次发送的关联数据,内部带一个 Future<Confirm> |
ConfirmCallback(挂在 Future 上) |
处理 ack / nack:消息到没到 Broker |
ReturnCallback(挂在 Template 上) |
处理 路由失败:到了 Broker 但没进队列 |
⚠️ 性能提醒
开启 confirm 后,每条消息都要等回执,吞吐会下降。不是所有消息都需要确认------核心业务消息(支付、订单)开,日志类消息可以不开。
5. MQ 可靠性:数据持久化
问题
默认情况下,RabbitMQ 会把收到的消息保存在内存中以降低消息收发的延迟。这会导致两个问题:

- 一旦 MQ 宕机,内存中的消息会丢失
- 内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发 MQ 阻塞
持久化的三个方面

| 对象 | 怎么持久化 |
|---|---|
| 交换机 | 声明时 durable=true |
| 队列 | 声明时 durable=true |
| 消息 | 发送时设置 delivery_mode=2 |
控制台验证消息持久化:发送消息时在 Properties 里填 delivery_mode = 2。
java
// SpringAMQP 中默认就是持久化消息
Message message = MessageBuilder.withBody("hello".getBytes())
.setDeliveryMode(MessageDeliveryMode.PERSISTENT) // delivery_mode = 2
.build();
SpringAMQP 的默认值:
Queue/Exchange默认durable = true(用new Queue("x")就是持久的)convertAndSend发送的消息默认PERSISTENT
注意
- 持久化要写磁盘,会降低吞吐、增加延迟,非关键消息可以不持久化
- 持久化 ≠ 100% 不丢:写入磁盘有缓冲区,极端断电仍可能丢。所以持久化必须配合生产者确认一起用
6. MQ 可靠性:Lazy Queue
背景
持久化解决了「宕机丢消息」,但没解决「内存被积压打爆」。消息还是先进内存,堆积多了照样 OOM。
是什么
从 RabbitMQ 3.6.0 版本开始,增加了 Lazy Queue 的概念,也就是惰性队列。

三个特征
- 接收到消息后直接存入磁盘而非内存(内存中只保留最近的消息,默认 2048 条)
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持数百万条的消息存储
版本变化(考点)
在 3.12 版本后,所有队列都是 Lazy Queue 模式,无法更改。 也就是说升级到 3.12+ 之后不需要也不能再手动选择 classic/lazy 了。
怎么设置(3.12 之前)
要设置一个队列为惰性队列,只需要在声明队列时,指定 x-queue-mode 属性为 lazy 即可。三种方式:
① 控制台

创建队列时在 Arguments 里加 x-queue-mode = lazy(页面下方有 Lazy mode 快捷按钮)。
② @Bean + QueueBuilder

java
@Bean
public Queue lazyQueue() {
return QueueBuilder
.durable("lazy.queue")
.lazy() // 开启 Lazy 模式
.build();
}
③ @RabbitListener + @Argument
java
@RabbitListener(queuesToDeclare = @Queue(
name = "lazy.queue",
durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void listenLazyQueue(String msg) {
log.info("接收到 lazy.queue 的消息:{}", msg);
}
权衡
| Classic Queue | Lazy Queue | |
|---|---|---|
| 消息位置 | 内存为主 | 磁盘为主 |
| 吞吐/延迟 | 高/低 | 低/高 |
| 堆积能力 | 差(内存限制) | 强(数百万条) |
结论:消息量大、可能堆积的场景(削峰填谷)用 Lazy;追求低延迟且消息量小的场景用 Classic。3.12+ 没得选。
7. 消费者可靠性:消费者确认
问题
生产者确认解决了「消息到 MQ」,持久化解决了「MQ 宕机」,但消费者拿到消息后处理失败怎么办?如果消息一投递就删掉,处理失败就永远丢了。
机制
为了确认消费者是否成功处理消息,RabbitMQ 提供了消费者确认机制(Consumer Acknowledgement)。消费者处理完消息后,应向 RabbitMQ 发送一个回执,告知处理状态。回执有三种:

| 回执 | 含义 | MQ 的动作 |
|---|---|---|
| ack | 成功处理消息 | 从队列中删除该消息 |
| nack | 消息处理失败 | 需要再次投递消息 |
| reject | 消息处理失败并拒绝该消息 | 从队列中删除该消息 |
nack和reject的区别:前者「我处理不了,你再发一次」;后者「这消息我不要了」。reject配合死信交换机就是「进死信队列」。
SpringAMQP 的三种模式
SpringAMQP 已经实现了消息确认功能,并允许通过配置文件选择 ACK 处理方式:

| 模式 | 行为 | 评价 |
|---|---|---|
none |
不处理。消息投递给消费者后立刻 ack,消息立刻从 MQ 删除 | 非常不安全,不建议使用 |
manual |
手动模式。需要自己在业务代码中调用 API,发送 ack 或 reject |
存在业务侵入,但更灵活 |
auto |
自动模式。SpringAMQP 利用 AOP 对消息处理逻辑做环绕增强 | 推荐 |
auto 的判定规则:
- 业务正常执行 → 自动返回
ack - 业务出现异常时,根据异常类型判断:
- 业务异常 → 自动返回
nack - 消息处理或校验异常 (如消息转换失败)→ 自动返回
reject
- 业务异常 → 自动返回
yaml
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 默认就是 auto
为什么区分 nack 和 reject?业务异常(比如库存不足)重试可能成功,所以 nack 重投;消息格式错误(反序列化失败)重试一万次也没用,所以 reject 直接丢弃。
8. 消费者可靠性:失败重试机制
问题:无限 requeue
消费者出现异常后,消息会不断 requeue(重新入队) 到队列,再重新发送给消费者,然后再次异常,再次 requeue......无限循环,导致 MQ 的消息处理飙升,带来不必要的压力。
解决:Spring 的本地重试
利用 Spring 的 retry 机制,在消费者出现异常时本地重试,而不是无限制地 requeue 到 MQ 队列:

yaml
spring:
rabbitmq:
listener:
simple:
prefetch: 1
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初始的失败等待时长为1秒
multiplier: 1 # 下次失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
关键点 :这是本地重试 ------在消费者进程内重试,不把消息退回 MQ。重试次数耗尽后,才交给 MessageRecoverer 决定怎么处理。
stateless 的含义:有状态重试会在重试期间保留原始消息上下文(包括事务),业务里有事务必须设 false。
重试耗尽后:MessageRecoverer 三种实现
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有 MessageRecoverer 接口来处理,它包含三种不同的实现:

| 实现 | 行为 |
|---|---|
| RejectAndDontRequeueRecoverer | 重试耗尽后直接 reject,丢弃消息 。默认就是这种方式 |
| ImmediateRequeueMessageRecoverer | 重试耗尽后返回 nack,消息重新入队(有再次死循环的风险) |
| RepublishMessageRecoverer | 重试耗尽后将失败消息投递到指定的交换机 |
推荐:投递到异常交换机,人工处理

java
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) {
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
流程:simple.queue 消费失败 → 本地重试 3 次 → 仍失败 → 投递到 error.direct → 进入 error.queue → 人工介入(报警、补偿、修复后重放)。
注意:需要先声明
error.direct交换机、error.queue队列及绑定关系。
总结

消费者如何保证消息一定被消费?
- 开启消费者确认机制为
auto,由 Spring 确认消息处理成功后返回ack,异常时返回nack/reject - 开启消费者失败重试机制,并设置
MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理
9. 消费者可靠性:业务幂等性
什么是幂等
幂等是一个数学概念,用函数表达式来描述是:f(x) = f(f(x))。
在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的。

| 幂等 | 非幂等 |
|---|---|
| 查询业务:如根据 id 查询商品 | 用户下单业务:需要扣减库存 |
| 删除业务:如根据 id 删除商品 | 用户退款业务:需要恢复余额 |
为什么会有重复消费
- 生产者 confirm 后 ack 丢失,生产者重发
- 消费者处理成功但 ack 丢失,MQ 重新投递
- 网络抖动、消费者重试
MQ 只能保证「至少一次」(at least once),做不到「恰好一次」。 所以必须在业务层兜底。
方案一:唯一消息 ID
给每个消息都设置一个唯一 id,利用 id 区分是否是重复消息:

- 每一条消息都生成一个唯一的 id,与消息一起投递给消费者
- 消费者接收到消息后处理自己的业务,业务处理成功后将消息 ID 保存到数据库
- 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息,放弃处理
开启方式(在消息转换器上):
java
@Bean
public MessageConverter messageConverter() {
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
// 配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jjmc.setCreateMessageIds(true);
return jjmc;
}
消费端取 id:
java
@RabbitListener(queues = "trade.queue")
public void listenTrade(@Header(AmqpHeaders.MESSAGE_ID) String messageId, OrderDTO dto) {
// 先查库判断 messageId 是否已处理
}
优点 :通用,与业务无关。缺点:多一张表/一次查询,存储成本。
方案二:业务判断
结合业务逻辑,基于业务本身做判断。以课程业务为例:支付后要把订单状态改为已支付,应该在修改订单状态前先查询订单状态 ,判断状态是否是未支付。只有未支付订单才需要修改,其它状态不做处理:

支付服务:扣减用户余额 → 更新支付流水状态 → MQ 通知支付结果
交易服务:查询订单 → 判断订单是否未支付 → 是:标记订单为已支付 → 结束
→ 否:结束
优点 :不需要额外存储,一次查询就能判断。缺点 :只适用于有明确状态机的业务,且判断和更新要保证原子性(update ... where status = 未支付)。
两种方案对比
| 唯一消息 ID | 业务判断 | |
|---|---|---|
| 通用性 | 高,适合所有消息 | 低,依赖业务状态 |
| 额外存储 | 需要消息 ID 表 | 不需要 |
| 实现成本 | 中 | 低(但要求业务有状态) |
| 可靠性 | 依赖 id 表 | 依赖状态机 + 原子更新 |
实际项目常见做法:两者都做,业务判断为主,消息 ID 兜底。
10. 延迟消息:什么是延迟消息
定义
延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息。
延迟任务:设置在一定时间之后才执行的任务。

典型场景
电商下单后 30 分钟未支付则取消订单并恢复库存。用户下单时发一条「30 分钟后检查支付状态」的消息,到点消费,查库判断。
直接睡 30 分钟行不行
可以,但有两个问题:
- 如果并发较高,30 分钟可能堆积消息过多,对 MQ 压力很大
- 大多数订单在下单后 1 分钟内就会支付,却需要在 MQ 内等待 30 分钟,浪费资源
所以需要「延迟消息」这种能力,而不是靠消费者自己 sleep。
11. 延迟消息:死信交换机
什么是死信
当一个队列中的消息满足下列情况之一时,就会成为死信(dead letter):

- 消费者使用
basic.reject或basic.nack声明消费失败,并且消息的requeue参数设置为false - 消息是一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
什么是死信交换机
如果队列通过 dead-letter-exchange 属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中。这个交换机称为死信交换机(Dead Letter Exchange,简称 DLX)。
publisher → simple.direct → simple.queue (TTL=30s) → dlx.direct → dlx.queue → consumer
用 TTL + DLX 实现延迟消息
思路:给队列设置 TTL(Time To Live,存活时间),消息在队列里等 TTL 到期变成死信,死信被转发到 DLX,消费者监听 DLX 绑定的队列。
问题(为什么课程不推荐):
- TTL 粒度粗:队列的 TTL 是统一的,不同延迟时间要建多个队列
- 队头阻塞:队列是 FIFO 的,前面的消息没过期就会挡住后面的消息,导致延迟不准
所以课程最终选择了插件方案。
12. 延迟消息:延迟消息插件
原理
RabbitMQ 官方也推出了一个插件,原生支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机 ,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列。

声明延迟交换机
java
@Bean
public DirectExchange delayExchange() {
return ExchangeBuilder
.directExchange("delay.direct")
.delayed() // 设置 delay 的属性为 true
.durable(true) // 持久化
.build();
}
或者用注解:
java
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue", durable = "true"),
exchange = @Exchange(name = "delay.direct", delayed = "true"),
key = "delay"
))
public void listenDelayMessage(String msg) {
log.info("接收到delay.queue的延迟消息:{}", msg);
}
发送延迟消息
发送消息时需要通过消息头 x-delay 来设置过期时间(单位毫秒):

java
@Test
void testPublisherDelayMessage() {
// 1. 创建消息
String message = "hello, delayed message";
// 2. 发送消息,利用消息后置处理器添加消息头
rabbitTemplate.convertAndSend("delay.direct", "delay", message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 添加延迟消息属性
message.getMessageProperties().setDelay(5000);
return message;
}
});
}
setDelay(5000) = 延迟 5 秒。等价于设置消息头:
java
message.getMessageProperties().setHeader("x-delay", 5000);
使用前提
- 必须安装
rabbitmq_delayed_message_exchange插件并启用 - 交换机声明时
delayed=true,否则消息不会延迟,会被立即投递 - 消息需要持久化时,延迟消息也会写入磁盘
13. 延迟消息:取消超时订单的基本思路
目标
下单后 30 分钟未支付 → 取消订单、恢复库存。
两个问题
- 如果并发较高,30 分钟可能堆积消息过多,对 MQ 压力很大
- 大多数订单在下单后 1 分钟内就会支付,却需要在 MQ 内等待 30 分钟,浪费资源
解决方案:分段检测
不是「一条消息等 30 分钟」,而是「短间隔反复检测,直到超过最长等待时间」:
下单业务:
创建订单 → 发送延迟消息(延迟 10s)→ 结束
延迟消息处理:
查询支付状态
├─ 已支付 → 标记已支付 → 结束
└─ 未支付 → 获取下次延迟时间
├─ 还有延迟时间 → 重发延迟消息(下一段)
└─ 没有延迟时间(已超过 30 分钟)→ 取消订单 → 恢复库存 → 结束

好处:
- 消息在队列里只停留一小段(如 10 秒),不会积压 30 分钟
- 用户 1 分钟内支付的话,第 1 次检测就结束了,后续延迟消息不用再发
- 检测次数和间隔可配置,灵活
这就是「延迟消息 + 轮询检测」的组合,比单纯的「一条 30 分钟延迟消息」更省资源。
14. 延迟消息:发送与监听检测消息
发送端
下单成功后,向延迟交换机发送一条「检测订单支付状态」的消息:
java
// 伪代码:下单成功后
rabbitTemplate.convertAndSend(
"order.delay.direct",
"order.delay",
new OrderDelayMessage(orderId), // 消息体带上订单 id 和当前已等待时长
message -> {
message.getMessageProperties().setDelay(nextDelayMillis()); // 下一段延迟时间
return message;
}
);
要点:
- 消息体必须带 orderId,否则消费者不知道该查哪个订单
- 需要记录已等待总时长,用于判断是否到 30 分钟
- 延迟时间建议分段递增(如 10s → 10s → 20s → ...),兼顾及时性和 MQ 压力
消费端
java
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "order.delay.queue", durable = "true"),
exchange = @Exchange(name = "order.delay.direct", type = ExchangeTypes.DIRECT, delayed = "true"),
key = "order.delay"
))
public void listenOrderDelay(OrderDelayMessage msg) {
// 1. 查询订单支付状态
// 2. 已支付 → 直接返回(幂等:改状态用 update ... where status = 未支付)
// 3. 未支付且未超时 → 重新发送下一段延迟消息
// 4. 未支付且已超时 → 取消订单 + 恢复库存
}
必须注意 :消费端要保证幂等------可能收到重复的检测消息,取消订单前要判断状态;恢复库存也不能重复执行。
15. 综合案例总结

如何保证支付服务与交易服务之间的订单状态一致性?
- 首先,支付服务会在用户支付成功以后利用 MQ 消息通知交易服务,完成订单状态同步
- 其次,为了保证 MQ 消息的可靠性,采用了生产者确认机制、消费者确认、消费者失败重试 等策略,确保消息投递和处理的可靠性。同时也开启了 MQ 的持久化,避免因服务宕机导致消息丢失
- 最后,在交易服务更新订单状态时做了业务幂等判断,避免因消息重复消费导致订单状态异常
如果交易服务消息处理失败,有没有兜底方案?
可以在交易服务设置定时任务 ,定期查询订单支付状态。这样即便 MQ 通知失败,还可以利用定时任务作为兜底方案,确保订单支付状态的最终一致性。
这句话可以直接背:MQ 保证实时性,定时任务保证最终一致性;MQ 不可靠时,靠对账兜底。
16. 配置与参数速查
完整 yml 配置(生产可用模板)
yaml
spring:
rabbitmq:
host: 192.168.150.101
port: 5672
virtual-host: /hmall
username: hmall
password: 123
# ---- 生产者 ----
connection-timeout: 1s
publisher-confirm-type: correlated # none / simple / correlated
publisher-returns: true
# ---- 消费者 ----
listener:
simple:
prefetch: 1
acknowledge-mode: auto # none / manual / auto
retry:
enabled: true
initial-interval: 1000ms
multiplier: 1
max-attempts: 3
stateless: true
# ---- 生产端发送重试(可选,阻塞式,谨慎开启)----
template:
retry:
enabled: true
initial-interval: 1000ms
multiplier: 1
max-attempts: 3
可靠性四段速查
| 环节 | 手段 | 关键配置/代码 |
|---|---|---|
| 发送到 MQ | 重连 | connection-timeout + template.retry |
| 发送到 MQ | 确认 | publisher-confirm-type: correlated + publisher-returns: true + ReturnCallback + CorrelationData |
| MQ 存储 | 持久化 | 交换机/队列 durable=true,消息 delivery_mode=2 |
| MQ 存储 | 惰性队列 | x-queue-mode=lazy,3.12+ 默认 |
| MQ 到消费者 | 确认 | acknowledge-mode: auto |
| 消费失败 | 重试 | listener.simple.retry.* |
| 重试耗尽 | 兜底 | RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error") |
| 重复消费 | 幂等 | 唯一消息 ID / 业务状态判断 |
常用注解
| 注解/类 | 用途 |
|---|---|
@RabbitListener(queues = "x") |
监听已有队列 |
@RabbitListener(queuesToDeclare = @Queue(...)) |
监听并声明队列 |
@RabbitListener(bindings = @QueueBinding(...)) |
监听并声明队列+交换机+绑定 |
@Queue / @Exchange / @QueueBinding / @Argument |
注解式声明拓扑 |
QueueBuilder / ExchangeBuilder / BindingBuilder |
编程式声明拓扑 |
MessageConverter / Jackson2JsonMessageConverter |
消息序列化 |
MessageRecoverer / RepublishMessageRecoverer |
重试耗尽后的处理 |
17. 本篇复习清单
Q1:消息可能在哪些环节丢失?分别怎么解决?
① 生产者→MQ:重连 + Confirm/Return;② MQ 内部:持久化 + Lazy Queue;③ MQ→消费者:消费者确认 + 失败重试 + MessageRecoverer;④ 重复投递:业务幂等。
Q2:Publisher Confirm 和 Publisher Return 的区别?
Confirm 告知消息是否到达 Broker(ack/nack);Return 告知消息路由到队列失败的原因。路由失败时 Confirm 仍返回 ACK,所以两者必须同时开。
Q3:publisher-confirm-type 三种模式?
none 关闭;simple 同步阻塞等回执;correlated 异步回调(推荐)。
Q4:持久化包括哪三方面?
交换机持久化、队列持久化、消息持久化(delivery_mode=2)。
Q5:Lazy Queue 的特点?
消息直接写磁盘、内存只留最近 2048 条、消费者消费时才加载、支持数百万条;3.12 后所有队列强制 lazy。
Q6:消费者确认三种回执的区别?
ack 删除;nack 重新投递;reject 删除(可进死信)。
Q7:SpringAMQP 三种确认模式?
none 投递即 ack(不安全);manual 手动调 API;auto AOP 环绕,正常 ack、业务异常 nack、消息处理/校验异常 reject(推荐)。
Q8:为什么要失败重试?重试耗尽怎么办?
不重试会无限 requeue 打爆 MQ。本地重试(max-attempts)后交给 MessageRecoverer:默认丢弃 / 重新入队 / 投递到异常交换机人工处理(推荐)。
Q9:什么是幂等?两种实现方案?
同一业务执行一次或多次对状态影响一致。方案一唯一消息 ID + 去重表;方案二业务状态判断(只处理未支付订单)。
Q10:延迟消息的两种实现方式?
① TTL + 死信交换机(有队头阻塞、需要多队列,不推荐);② 延迟消息插件(x-delayed-message 交换机 + x-delay 消息头,推荐)。
Q11:取消超时订单为什么不用「一条 30 分钟延迟消息」?
并发高时积压 30 分钟消息、大多数订单 1 分钟内已支付却白等 30 分钟。改用分段延迟检测。
Q12:怎么保证支付与交易服务的最终一致性?
MQ 通知 + 生产者确认 + 消费者确认 + 失败重试 + 持久化 + 业务幂等;再用定时任务定期查订单状态兜底。
上一篇 ← RabbitMQ 基础篇