【RabbitMQ高级】如何保证消息的可靠性?

目录

消息可能丢失在哪里?

一、生产者的可靠性

[1.1 生产者重连](#1.1 生产者重连)

[1.2 生产者确认](#1.2 生产者确认)

[Publisher Confirm(发布确认)](#Publisher Confirm(发布确认))

[Publisher Return](#Publisher Return)

二、Broker(MQ)的可靠性

[2.1 三层持久化](#2.1 三层持久化)

[2.2 Lazy Queue](#2.2 Lazy Queue)

三、消费者的可靠性

[3.1 消费者确认机制](#3.1 消费者确认机制)

[3.2 消费失败处理](#3.2 消费失败处理)

失败重试机制

失败消息处理策略

[3.3 业务幂等性](#3.3 业务幂等性)

方案一:唯一消息ID

方案二:业务判断

四、延迟消息

[4.1 通过死信交换机实现](#4.1 通过死信交换机实现)

[4.2 RabbitMQ插件](#4.2 RabbitMQ插件)


消息可能丢失在哪里?

XML 复制代码
┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│ Producer │───▶│ Exchange │───▶│  Queue   │───▶│ Consumer │
└──────────┘     └──────────┘     └──────────┘     └──────────┘
     ①                ②               ③                ④
  网络抖动         路由失败         Broker宕机        消费崩溃
  未到达Broker     没匹配到队列     数据未持久化      没来得及ACK

一、生产者的可靠性

1.1 生产者重连

有时候可能会由于网络波动导致客户端连接MQ失败。此时可以开启生产者重连机制,让客户端重新连接MQ。

在application.yaml文件中加上如下配置:

XML 复制代码
spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    # ... 其他连接参数 ...
    template:
      retry:
        enabled: true # 🌟 开启生产者重试机制(默认是 false)
        initial-interval: 1000ms # 第一次重试前的等待时间(1秒)
        multiplier: 2 # 递增倍数(下一次等待时间 = 上一次等待时间 * multiplier)
        max-interval: 10000ms # 最大等待时间(退避上限,最多等10秒)
        max-attempts: 3 # 最大重试次数(包含第一次正常的发送请求,实际上是重试2次)

注意:

在生产环境的高并发场景下,这往往会引发严重的雪崩效应 。因为rabbitTemplate.convertAndSend 默认是同步(阻塞)调用的。 当 Spring 在进行重试等待时(比如等 1秒、等 2秒),执行这段代码的那个业务线程会被卡住。

那么应该如何防止发生雪崩现象呢?

  • 配置极其保守的重试策略: 既然阻塞不可避免,我们就尽量缩短阻塞时间。将 max-attempts 设为 2,initial-interval 设为 100ms。其目的是:只防瞬间的网络闪断,绝不硬扛 MQ 的宕机。 连不上就赶紧让它报错。

  • 捕获异常并落库: 在发送消息的代码处加上 try-catch。如果发不出去了(重试也失败了),千万不能把异常吞掉或者直接抛给前端,而是要立刻把这条消息存到数据库的一张专门的 message_fail_log 异常消息表里。

  • 定时任务重发: 启动一个定时任务,每隔一两分钟去扫描这张异常表,如果 MQ 恢复了,由定时任务异步地重新发送这些滞留的消息。

1.2 生产者确认

生产者端要解决的核心问题是:消息发出去了,但 Broker 有没有真的收到?

Publisher Confirm(发布确认)

生产者发送消息后,Broker 会异步回调通知:这条消息我收到了(ACK)还是没收到(NACK)。

SpringAMQP实现生产者确认有三种模式:

none:关闭confirm机制

simple:同步阻塞等待MQ的回执消息

correlated:MQ异步回调方式返回回执消息

XML 复制代码
spring:
  rabbitmq:
    publisher-confirm-type: correlated
    publisher-returns: true

Publisher Return

Confirm 只能确认消息到达了 Exchange ,但如果 Exchange 根据 Routing Key 找不到任何匹配的队列,消息会被静默丢弃。Return机制就是为了解决这个问题

XML 复制代码
spring:
  rabbitmq:
    publisher-returns: true
    template:
      mandatory: true   # 必须设置,否则 Return 回调不触发

如何处理生产者的确认消息?

  • 生产者确认需要额外的网络和系统资源开销,尽量不要使用
  • 如果一定要使用,无需开启Publisher-Return机制,因为一般路由失败是自己业务问题
  • 对于nack消息可以有限次数重试,依然失败则记录异常消息

二、Broker(MQ)的可靠性

消息已经到达 Broker 了,接下来的问题是:Broker 宕机重启后,消息还在不在?

2.1 三层持久化

  • ① 交换机持久化
  • ② 队列持久化
  • ③ 消息持久化

RabbitMQ的持久化分为两个阶段:

XML 复制代码
阶段一:写入内存 + Page Cache
  消息到达 → 存入内存表 → 同时写入操作系统的页缓存(Page Cache)

阶段二:刷盘
  RabbitMQ 有两种策略:
  ┌──────────────────────────────────────────────────┐
  │ ① 异步刷盘(默认)                                │
  │    由操作系统决定何时将 Page Cache 写入磁盘          │
  │    通常每 1~2 秒刷一次,性能高但极端情况丢少量数据    │
  │                                                    │
  │ ② 同步刷盘                                        │
  │    每条消息都 fsync 到磁盘后再返回 ACK              │
  │    性能下降约 10 倍,但数据最安全                    │
  └──────────────────────────────────────────────────┘

2.2 Lazy Queue

  • 普通队列(Standard Queue)的贪婪策略: 默认情况下,RabbitMQ 追求极致的延迟。只要服务器内存够用,普通队列会尽可能把所有的消息都缓存在内存(RAM)里。只有当内存快触发内存高水位线时,它才会把消息从内存刷到磁盘上(这就是极其消耗性能的 Paged Out 过程)。

  • 惰性队列(Lazy Queue)的佛系策略: 惰性队列从一开始就"躺平"了。当它接收到消息时,直接将消息持久化写入磁盘,内存中只保留极小的一点点索引信息(为了维持队列的形态)。只有在消费者真正需要消费这条消息的那一瞬间,RabbitMQ 才会把它从磁盘读进内存。

在3.12版本之后,所有队列都是Lazy Queue模式,无法更改。

三、消费者的可靠性

3.1 消费者确认机制

SpringAMQP 消费者的确认模式分为三种:

模式 行为 风险
AUTO ACK(自动确认) 消息一送达消费者,Broker 就立刻从队列删除 如果消费者收到消息后崩溃,消息永久丢失
MANUAL ACK(手动确认) 消费者处理完业务后,主动发 ACK 存在业务侵入
AUTO(自动模式) 业务正常执行则自动返回ACK,业务异常根据异常判断不同返回结果
XML 复制代码
AUTO ACK 的致命流程:

Consumer 收到消息 → Broker 立即删除消息
  → Consumer 正在处理,突然崩溃
  → 消息已经在队列中被删了
  → 数据丢失,无法恢复!

MANUAL ACK 的安全流程:

Consumer 收到消息 → Broker 不删除,标记为 Unacked
  → Consumer 处理完毕,发送 ACK
  → Broker 收到 ACK,删除消息
  → 如果 Consumer 崩溃(连接断开)
  → Broker 自动将 Unacked 消息重新入队
  → 其他消费者可以重新消费

消费者发送给MQ的消息回执有三种状态:

  1. ack:成功处理消息,RabbitMQ从队列中删除该消息
  2. nack:消息处理失败,RabbitMQ需要再次投递消息
  3. reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

3.2 消费失败处理

失败重试机制

当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue无限循环,导致mq的消息处理飙升,带来不必要的压力。

我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列:

失败消息处理策略

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:

3.3 业务幂等性

方案一:唯一消息ID

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

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

方案二:业务判断

结合业务逻辑,基于业务本身做判断。以我们的业务为例:我们要在支付后修改订单状态为已支付,应该在修改订单状态前先查询订单状态,判断状态是否是未支付。只有未支付订单才需要修改,其它状态不做处理。

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

我们可以在交易服务设置定时任务,定期查询订单支付状态。这样即便MQ通知失败,还可以利用定时任务作为兜底方案,确保订单支付状态的最终一致性。

四、延迟消息

延迟消息(Delayed Message)是指:消息发送后不会立即被消费者接收到,而是在指定的延迟时间后才投递给消费者。

4.1 通过死信交换机实现

让消息在队列中"等待"到过期,变成死信后被消费者"准时"收到。

消息在队列中是排队的,不支持插队:

队列中的消息顺序:

消息A: TTL=10分钟\] \[消息B: TTL=30分钟\] \[消息C: TTL=5分钟

问题:消息C(TTL=5分钟)排在消息B(TTL=30分钟)后面

→ 消息C 会在消息B 消费后才会变成死信

→ 消息C 的实际延迟 = 30分钟,而不是 5分钟!

这就是"队头阻塞"问题:RabbitMQ 只检查队头消息是否过期

4.2 RabbitMQ插件

RabbitMQ 社区提供了 rabbitmq_delayed_message_exchange 插件,是目前最推荐的延迟消息方案。它在 Exchange 层面实现了延迟,彻底解决了队列头阻塞问题。

插件的原理是设计了一种支持延迟消息功能的交换机当消息投递到交换机后可以暂存一定时间,到期后再投递到队列。

相关推荐
xiaoshuaishuai81 小时前
C# 多线程之间对比
java·开发语言·c#
越努力越幸运662 小时前
Java 无需 Office 环境实现 Word 转 HTML
java
用户8176967132352 小时前
Java OOM 排查完整指南:从告警到根因,MAT 堆分析全流程实战
java
要开心吖ZSH2 小时前
AI医疗分诊与健康咨询助手agent开发——(0)项目背景与概要
java·ai·agent·健康医疗·rag
后青春期的诗go3 小时前
泛微OA-E9与第三方系统集成开发企业级实战记录(十五)
java·泛微·集成开发·e9
吃口巧乐兹3 小时前
理解 Agent 中的 Slash Command:从概念到自定义命令实践
java·github
夕除5 小时前
shizhan--10
java·开发语言
吴声子夜歌5 小时前
JVM——并发容器实现原理
java·jvm·并发容器
xier_ran5 小时前
【infra之路】PagedAttention
java·开发语言