【RabbitMQ】高级篇

👋 关于作者

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

🏠 个人主页:一条泥憨鱼

通过技术博客记录技术成长,分享学习心得与经验。期待与您共同交流!

目录

  1. 可靠性全景
  2. 生产者可靠性:重连
  3. 生产者可靠性:确认原理
  4. 生产者可靠性:确认代码实现
  5. MQ 可靠性:数据持久化
  6. MQ 可靠性:Lazy Queue
  7. 消费者可靠性:消费者确认
  8. 消费者可靠性:失败重试机制
  9. 消费者可靠性:业务幂等性
  10. 延迟消息:什么是延迟消息
  11. 延迟消息:死信交换机
  12. 延迟消息:延迟消息插件
  13. 延迟消息:取消超时订单的基本思路
  14. 延迟消息:发送与监听检测消息
  15. 综合案例总结
  16. 配置与参数速查
  17. 本篇复习清单

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 队列及绑定关系。

总结

消费者如何保证消息一定被消费?

  1. 开启消费者确认机制为 auto,由 Spring 确认消息处理成功后返回 ack,异常时返回 nack / reject
  2. 开启消费者失败重试机制,并设置 MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理

9. 消费者可靠性:业务幂等性

什么是幂等

幂等是一个数学概念,用函数表达式来描述是:f(x) = f(f(x))。

在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的。

幂等 非幂等
查询业务:如根据 id 查询商品 用户下单业务:需要扣减库存
删除业务:如根据 id 删除商品 用户退款业务:需要恢复余额

为什么会有重复消费

  • 生产者 confirm 后 ack 丢失,生产者重发
  • 消费者处理成功但 ack 丢失,MQ 重新投递
  • 网络抖动、消费者重试

MQ 只能保证「至少一次」(at least once),做不到「恰好一次」。 所以必须在业务层兜底。

方案一:唯一消息 ID

给每个消息都设置一个唯一 id,利用 id 区分是否是重复消息:

  1. 每一条消息都生成一个唯一的 id,与消息一起投递给消费者
  2. 消费者接收到消息后处理自己的业务,业务处理成功后将消息 ID 保存到数据库
  3. 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息,放弃处理

开启方式(在消息转换器上):

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 绑定的队列。

问题(为什么课程不推荐):

  1. TTL 粒度粗:队列的 TTL 是统一的,不同延迟时间要建多个队列
  2. 队头阻塞:队列是 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. 综合案例总结

如何保证支付服务与交易服务之间的订单状态一致性?

  1. 首先,支付服务会在用户支付成功以后利用 MQ 消息通知交易服务,完成订单状态同步
  2. 其次,为了保证 MQ 消息的可靠性,采用了生产者确认机制、消费者确认、消费者失败重试 等策略,确保消息投递和处理的可靠性。同时也开启了 MQ 的持久化,避免因服务宕机导致消息丢失
  3. 最后,在交易服务更新订单状态时做了业务幂等判断,避免因消息重复消费导致订单状态异常

如果交易服务消息处理失败,有没有兜底方案?

可以在交易服务设置定时任务 ,定期查询订单支付状态。这样即便 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 基础篇

相关推荐
京师20万禁军教头1 小时前
50-常用类-Arrays
java
寰梦1 小时前
Spring Boot + MinIO 大文件上传完整实战:MD5 秒传、分片上传、断点续传与引用一致性
java·spring boot·文件上传·minio
棣廷2 小时前
初识Spark Core
大数据·分布式·spark
云和数据.ChenGuang9 小时前
Python 重写(Override)深度解析:从 MRO 到 super() 的完整机制
java·开发语言·人工智能·python·langchain·1024程序员节
摇滚侠9 小时前
《On Java 中文版 基础卷》阅读笔记 实现隐藏 12
java·数据库·笔记
凯凯姜11 小时前
分布式锁场景问题
分布式
silvamux_12 小时前
10款模型写种草笔记:可发布性、返工来源与成本数据缺口复盘
java·数据库·笔记
ESDWAN13 小时前
SD-WAN 主备切换为什么仍然会断网?从检测机制到会话恢复分析
开发语言·php
步行cgn13 小时前
StringHttpMessageConverter 详解:Spring MVC 中字符串读写的默认转换器
java·spring·mvc