前言
本文内容作了适度扩展,不止于黑马教的内容
RabbitMQ 实现了 AMQP 0-9-1 协议,在分布式系统中用于异步通信、服务解耦和流量削峰。基础的收发消息只能覆盖简单场景,生产环境还需要解决消息丢失、重复消费、延迟任务和高可用等问题。本文覆盖消息可靠性全链路、死信队列、延迟消息的两种实现、集群部署(含负载均衡与 Docker 搭建)、跨集群通信(Federation / Shovel)、监控、安全与性能调优,每个主题都附带可运行的配置和代码。
一、消息可靠性全链路
消息丢失发生在三个环节:生产者 → Broker、Broker 存储、Broker → 消费者。每一层都需要独立的可靠性保障。
1.1 生产者端:确保消息抵达 Broker(发送者可靠性)
生产者面临两类问题:连接断开导致无法发送,以及发送后不知道 Broker 是否收到。分别用重连和确认机制解决。
1.1.1 连接重试
Spring AMQP 的连接重试是阻塞式的------重试期间当前线程被阻塞。核心配置:
yaml
spring:
rabbitmq:
host: 192.168.56.2
port: 5672
virtual-host: "/hmall"
username: hmall
password:123 # 敏感信息走环境变量
connection-timeout: 1s
template:
retry:
enabled: true
initial-interval: 1000ms
multiplier: 1
max-attempts: 3
阻塞式重试会在连接恢复前卡住发送线程。如果发送链路在核心业务线程上(比如同步响应给用户),要么关掉重试自己异步处理,要么把消息发送移到独立线程池。
Spring AMQP 底层使用的 CachingConnectionFactory 默认缓存 Channel。AMQP 中一个 TCP Connection 可承载多个 Channel(多路复用),Channel 是轻量级的虚拟连接。关键参数:
java
@Bean
public CachingConnectionFactory connectionFactory() {
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setHost("192.168.56.2");
factory.setPort(5672);
factory.setVirtualHost("my_vhost");
factory.setChannelCacheSize(25); // Channel 缓存上限
factory.setConnectionCacheSize(1); // 通常一个物理连接足够
return factory;
}
Spring AMQP ConnectionFactory 对比:
| 实现 | 特点 |
|---|---|
CachingConnectionFactory |
默认,缓存 Channel,支持 Confirm 和 Return |
PooledChannelConnectionFactory |
基于 Apache Pool2,Channel 池化管理,适合高并发发送 |
SingleConnectionFactory |
仅一个 Connection,不缓存,测试用 |
ThreadChannelConnectionFactory |
每个线程绑定一个 Channel,适合固定线程模型 |
对于高吞吐发送场景,可切换到 PooledChannelConnectionFactory:
java
@Bean
public ConnectionFactory connectionFactory() {
PooledChannelConnectionFactory factory = new PooledChannelConnectionFactory();
factory.setHost("192.168.56.2");
factory.setPort(5672);
factory.setMaxConnections(1);
factory.setMaxChannels(50);
return factory;
}
1.1.2 生产者确认(Publisher Confirm)
RabbitMQ 提供两种确认:
- Publisher Confirm :确认消息是否到达 Exchange(
basic.ack/basic.nack) - Publisher Return :消息到达 Exchange 但无法路由到队列时,回调
basic.return
协议层面:客户端发送 confirm.select 开启确认。Broker 在消息持久化后异步返回 basic.ack(带上 deliveryTag)。Spring AMQP 封装了这个过程。
yaml
spring:
rabbitmq:
publisher-confirm-type: correlated # 异步回调,生产首选
publisher-return: true
三种 publisher-confirm-type:
| 值 | 行为 |
|---|---|
none |
不开启确认 |
simple |
同步阻塞等 ack,发一条等一条,吞吐极低 |
correlated |
异步回调,通过 CorrelationData 关联每条消息与回执 |
ReturnCallback(全局) ------每个 RabbitTemplate 只能设置一个:
java
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setReturnsCallback(returned -> {
log.error("路由失败: exchange={}, routingKey={}, replyCode={}, replyText={}, body={}",
returned.getExchange(), returned.getRoutingKey(),
returned.getReplyCode(), returned.getReplyText(),
new String(returned.getMessage().getBody()));
// 落入DB补偿表,定时任务重新投递
});
template.setMessageConverter(jacksonConverter());
return template;
}
ConfirmCallback(每条消息独立配置):
java
public void sendWithConfirm(String exchange, String routingKey, Object payload) {
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
cd.getFuture().thenAccept(ack -> {
if (ack.isAck()) {
log.debug("消息已达 Exchange: id={}", cd.getId());
} else {
log.error("发送失败: id={}, reason={}", cd.getId(), ack.getReason());
// 写入 send_fail_log 表,定时任务补偿
}
});
rabbitTemplate.convertAndSend(exchange, routingKey, payload, cd);
}
Confirm + Return 组合结果:
| 场景 | Confirm 结果 | Return 是否触发 |
|---|---|---|
| 到达 Exchange 并成功路由 | ACK | 否 |
| 到达 Exchange 但无法路由 | ACK | 是 |
| Exchange 不存在 | NACK | 否 |
| 连接断开 | 超时,无回调 | 否 |
路由失败时虽然返回 ACK(消息确实到了 Exchange),但 Return 回调会给出失败原因。两者同时开启才能完整追踪消息去向。
AMQP 事务 vs Confirm :AMQP 支持 txSelect/txCommit/txRollback 事务模式,但每次提交都是同步等待,吞吐只有 Confirm 的百分之一以下。生产环境一律用 Confirm,不使用事务。
什么时候可以不开确认:日志采集、非关键监控指标、客户端埋点上报等允许少量丢失的场景------省一次网络往返,延迟明显降低。
1.1.3 发送端的最终保障:落库补偿
即使 Confirm + Return 全覆盖,仍存在极端情况(如应用进程在收到 ACK 前崩溃)。对核心业务(支付、订单),在发送前先将消息写入本地消息表,再通过定时任务扫描未确认记录进行补发:
业务操作 → 本地事务 { 写业务数据 + 写消息表(status=PENDING) }
→ 发送 MQ 消息
→ ConfirmCallback 更新消息表 status=SENT / 记录失败原因
定时任务 → 扫描 status=PENDING 且超过 N 秒的记录 → 补发
1.2 Broker 端:确保消息存储不丢失(MQ可靠性)
RabbitMQ 默认将消息存储在内存中。如果节点宕机且消息未持久化到磁盘,消息就丢了。另外消息积压过多会导致内存水位告警,触发 Flow Control 阻塞生产者。
1.2.1 数据持久化(三个层面缺一不可)
交换机持久化 :声明时 durable=true(Spring AMQP 中默认已是持久化)。
队列持久化 :声明时 durable=true。注意,如果队列已存在且为非持久化,需要删除重建。
消息持久化 :delivery_mode=2。持久化消息在到达队列后,Broker 会先写入磁盘再返回 Confirm ACK(取决于队列类型,详见下文)。
Spring AMQP 中消息持久化通过 MessageConverter 统一设置:
java
@Bean
public MessageConverter messageConverter() {
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
// Spring AMQP 默认已设置 delivery_mode=2 给持久化消息
// 自定义 MessageIdGenerator 用于幂等追踪
converter.setMessageIdGenerator(() -> UUID.randomUUID().toString());
return converter;
}
持久化代价 :每次消息写入需要 fsync 到磁盘。单机 TPS 可能从数万降到数千。对于允许少量丢失的场景(如日志),使用 delivery_mode=1 + Lazy Queue(消息仍写盘但批量 fsync)是更好的平衡。
1.2.2 队列类型全景对比
RabbitMQ 提供了多种队列类型,选择正确的类型直接影响可靠性和性能:
| 类型 | 引入版本 | 存储位置 | 数据冗余 | 适用场景 |
|---|---|---|---|---|
| Classic(经典队列) | 所有版本 | 内存(可选持久化到磁盘) | 无(除非配镜像) | 低吞吐、旧版本兼容 |
| Lazy(惰性队列) | 3.6 | 磁盘为主,内存仅缓冲 2048 条 | 无(除非配镜像) | 大积压、不确定的消费速度 |
| Quorum(仲裁队列) | 3.8 | 磁盘(Raft 日志) | 多节点复制 | 高可用、数据安全要求高 |
| Stream(流队列) | 3.9 | 磁盘(append-only segment) | 可配置多副本 | 大吞吐、重复消费、重放 |
Classic Queue :消息先存内存,按需刷盘(queue_index_embed_msgs_below 阈值控制)。默认在 3.12+ 变为 Lazy 行为。
Lazy Queue:每个消息到达后直接写入磁盘,内存只保留索引和最近 2048 条消息的缓存。配置方式:
java
// 方式1:QueueBuilder
@Bean
public Queue lazyQueue() {
return QueueBuilder.durable("lazy.queue").lazy().build();
}
// 方式2:注解参数
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "lazy.queue", durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy")),
exchange = @Exchange(name = "my.exchange", type = ExchangeTypes.TOPIC),
key = "my.key"
))
public void onMessage(Message msg) { ... }
Quorum Queue:基于 Raft 协议,消息写入多数派节点后返回 ACK。代替镜像队列(mirrored queue)的推荐方案。核心特点:
- 强一致性:消息不会因脑裂丢失
- 自动故障转移:Leader 宕机后自动选举新 Leader
- 消息 TTL 和 Dead Letter 的语义与经典队列略有不同
声明方式------将 x-queue-type 设为 quorum:
java
@Bean
public Queue quorumQueue() {
return QueueBuilder.durable("quorum.queue")
.quorum() // x-queue-type=quorum
.build();
}
Quorum Queue 的初始成员数由 x-quorum-initial-group-size 决定,一般为 3 或 5。在 3 节点集群中,能容忍 1 个节点故障。
Stream Queue:RabbitMQ 3.9 新增,设计理念类似 Kafka------不可变、append-only 日志。支持:
- 多个消费者重复消费同一条消息(非破坏性读取)
- Offset 跟踪,消费者可从任意位置开始消费
- 大吞吐量场景(日志、事件溯源)
java
@Bean
public Queue streamQueue() {
return QueueBuilder.durable("stream.queue")
.stream() // x-queue-type=stream
.build();
}
1.2.3 消息写入磁盘的时机
理解 Broker 何时真正 fsync 对评估可靠性很重要:
- Classic Queue + 持久化消息 :消息写入
msg_store_persistent,由queue_index_max_journal_entries控制 journal 刷盘频率。默认每 16384 条或 200ms 刷一次。 - Lazy Queue:消息直接写入 segment 文件,依赖操作系统 page cache。
- Quorum Queue :消息写入 Raft 日志,由
raft.wal.max_batch_size控制批量写入。
对可靠性要求极高的场景,可配置 queue_master_locator=min-masters 确保队列 Leader 分布在不同的磁盘节点上。
1.3 消费者端:确保消息被正确处理(消费者可靠性)
消费者可能因为业务异常、服务重启、网络断开等原因处理失败。需要「消费者确认 + 本地重试 + 失败路由 + 幂等」四层防护。
1.3.1 消费者确认(Consumer Acknowledgement)
RabbitMQ 的消费者确认有三种回执:
- basic.ack:处理成功,Broker 删除消息
- basic.nack(requeue=true):处理失败,消息重新入队
- basic.nack (requeue=false)或 basic.reject:处理失败,消息丢弃或成为死信
Spring AMQP 提供三种确认模式:
| 模式 | 配置值 | 行为 | 推荐度 |
|---|---|---|---|
| None | none |
投递即 ACK,不管处理结果 | 不用 |
| Manual | manual |
业务代码手动调 channel.basicAck() |
特殊场景 |
| Auto | auto |
Spring AOP 环绕:正常返回→ACK,抛异常→NACK | 推荐 |
yaml
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto
prefetch: 1 # 每次拉取1条,处理完再拉下一条
Auto 模式下,Spring 根据异常类型自动判断:
AmqpRejectAndDontRequeueException→ REJECT(不重新入队)ImmediateAcknowledgeAmqpException→ ACK(即使业务抛出此异常也 ACK,日志场景用)- 其他异常 → NACK(重新入队,配合本地重试)
1.3.2 Consumer Prefetch 详解
prefetch 不是批量取消息的条数,而是「消费者允许的未确认消息上限」。设置不当会直接影响消费性能和公平性:
prefetch=1:每次只拿 1 条,处理完才拿下一个。公平,适合低延迟、处理时间不均匀的场景prefetch=250(默认):同时推送 250 条。吞吐高但可能分配不均------快的消费者拿满后闲着,慢的还在处理prefetch=0:无限制,Broker 不间断推送。可能导致消费者 OOM
java
// 编程式配置(优先级高于 YAML)
@Bean
public SimpleRabbitListenerContainerFactory listenerContainerFactory(
ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setPrefetchCount(10);
factory.setConcurrentConsumers(3);
factory.setMaxConcurrentConsumers(10);
return factory;
}
1.3.3 消费者并发
Spring AMQP 通过 concurrentConsumers 和 maxConcurrentConsumers 控制消费线程数:
yaml
spring:
rabbitmq:
listener:
simple:
concurrency: 3 # 初始消费者线程数
max-concurrency: 10 # 最大消费者线程数(队列积压时动态扩容)
扩容逻辑:当队列中有消息等待 + 当前线程全部忙碌时,逐步增加到 max-concurrency。空闲后逐步回收。
注意:并发 + prefetch 共同决定"在途消息"数量上限 = max-concurrency × prefetch。对于 Quorum Queue,过多在途消息会增加 Raft 日志压力。
1.3.4 消费失败的重试与恢复
默认行为:消费者抛异常 → Spring 返回 NACK(requeue=true)→ 消息回到队首 → 下次投递 → 又抛异常 → 无限循环。这会导致消息处理压力持续高涨。
解决分两层:本地重试 和 重试耗尽后的处理。
第一层:本地重试(不重新入队,在消费者本地重试):
yaml
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true
initial-interval: 1000ms # 初试间隔 1 秒
multiplier: 2 # 每次翻倍(1s → 2s → 4s)
max-attempts: 3 # 共 3 次
stateless: true # 无状态(有事务操作改为 false)
stateless 含义 :true 表示每次重试都是独立的,中间件层面不会回滚事务。涉及 @Transactional 的关键业务应设为 false,但此时只能使用 RejectAndDontRequeueRecoverer。
第二层:重试耗尽后的 MessageRecoverer:
| 实现 | 行为 | 适用场景 |
|---|---|---|
RejectAndDontRequeueRecoverer |
拒绝,丢弃(默认) | 允许丢消息的场景 |
ImmediateRequeueMessageRecoverer |
重新入队 | 不推荐------容易进入死循环 |
RepublishMessageRecoverer |
投递到指定的异常 Exchange | 生产首选,人工介入 |
RepublishMessageRecoverer 完整配置:
java
@Configuration
@ConditionalOnProperty(
prefix = "spring.rabbitmq.listener.simple.retry",
name = "enabled", havingValue = "true"
)
public class MQErrorConfig {
@Bean
public Queue errorQueue() {
return new Queue("error.queue");
}
@Bean
public DirectExchange errorExchange() {
return new DirectExchange("error.direct");
}
@Bean
public Binding errorBinding() {
return BindingBuilder.bind(errorQueue()).to(errorExchange()).with("error");
}
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
// 重试耗尽后 → error.direct → error.queue
RepublishMessageRecoverer recoverer = new RepublishMessageRecoverer(
rabbitTemplate, "error.direct", "error");
// 可选:在转发的消息头中记录原始异常信息
return recoverer;
}
}
异常队列中的消息需要配套处理机制:监控告警 + 人工排查 + 修复后重新发布。
1.3.5 业务幂等性
RabbitMQ 不保证 exactly-once 投递------重复投递在以下情况是常态:
- 网络波动导致 Confirm 超时,生产者重发
- 消费者 ACK 丢失,Broker 重新投递
- 镜像队列/仲裁队列主节点切换期间
两种常用幂等方案:
方案 A:唯一消息 ID + 去重表
java
@Bean
public MessageConverter messageConverter() {
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
converter.setCreateMessageIds(true); // 自动生成 UUID 作为 messageId
return converter;
}
@RabbitListener(queues = "order.queue")
public void handleOrder(OrderMsg msg, Message amqpMessage) {
String msgId = amqpMessage.getMessageProperties().getMessageId();
// Redis SET NX + EX: key=msgId, value=1, ttl=24h
if (!redisTemplate.opsForValue().setIfAbsent("msg:" + msgId, "1", Duration.ofHours(24))) {
return; // 已处理
}
// 业务处理
orderService.process(msg);
}
方案 B:业务状态机(推荐用于订单、交易类)
不依赖额外存储,利用业务状态本身做判断:
java
@RabbitListener(queues = "order.pay.queue")
public void onPaySuccess(Long orderId) {
Order order = orderService.getById(orderId);
if (order == null || order.getStatus() != OrderStatus.UNPAID) {
return; // 已处理或状态不对,跳过
}
// 利用数据库行锁 + 状态更新保证原子性
orderService.markPaid(orderId); // UPDATE ... SET status=PAID WHERE id=? AND status=UNPAID
}
两种方案可以组合:方案 A 做快速去重,方案 B 做最终保障。
最终兜底:即使所有防护都失效,也应该有定时任务扫描业务数据(如超时 30 分钟的未支付订单主动查询支付状态),保证最终一致性。
二、死信队列(Dead Letter Exchange)
死信是 RabbitMQ 中一个重要的消息路由机制。当消息在队列中变成"死信"时,会被转发到绑定的死信交换机(DLX),实现异常消息的自动分流。
2.1 消息变成死信的三种情况
| 情况 | 触发条件 | 示例 |
|---|---|---|
| 被拒绝(rejected) | 消费者调用 basic.reject 或 basic.nack,且 requeue=false |
消息格式校验失败 |
| 过期(expired) | 消息 TTL 到期 | 超时订单取消 |
| 队列溢出(maxlen) | 队列消息数超过 x-max-length 或 x-max-length-bytes |
积压保护,旧消息被挤出 |
2.2 DLX 核心配置
死信队列并非特殊队列类型------任何队列都可以通过 x-dead-letter-exchange 和 x-dead-letter-routing-key 指定死信目标:
java
@Configuration
public class DLXConfig {
// 常规业务队列 → 绑定死信交换机
@Bean
public Queue businessQueue() {
return QueueBuilder.durable("order.queue")
.deadLetterExchange("dlx.exchange") // 死信交换机
.deadLetterRoutingKey("dlx.order") // 死信路由键
.ttl(30000) // 消息 30 秒过期
.maxLength(10000) // 队列上限 10000 条
.build();
}
// 死信交换机
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange("dlx.exchange");
}
// 死信队列------真正消费死信的地方
@Bean
public Queue dlxQueue() {
return QueueBuilder.durable("dlx.order.queue").build();
}
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("dlx.order");
}
}
死信队列消费者可以读取死信来源信息:
java
@RabbitListener(queues = "dlx.order.queue")
public void handleDlx(Message msg, @Header(name = "x-death", required = false) List<Map<String, ?>> deathInfo) {
// x-death 头中包含:原队列名、死信原因、过期时间等
if (deathInfo != null) {
for (Map<String, ?> entry : deathInfo) {
log.warn("死信原因: {}, 原队列: {}, 原路由键: {}",
entry.get("reason"), entry.get("queue"), entry.get("routing-keys"));
// reason 可能是: rejected / expired / maxlen
}
}
// 根据原因分类处理
}
2.3 消息 TTL 的两种设置方式
队列级别 TTL:所有消息统一过期时间,简单但不够灵活。
java
QueueBuilder.durable("queue").ttl(60000).build(); // 队列内所有消息 60 秒过期
消息级别 TTL:每条消息独立设置,灵活但需要发送端配合。
java
MessagePostProcessor processor = msg -> {
msg.getMessageProperties().setExpiration("10000"); // 字符串,单位毫秒
return msg;
};
rabbitTemplate.convertAndSend("ex", "key", payload, processor);
注意:队列 TTL + 消息 TTL 同时存在时,以较小的值为准。
2.4 DLX 的典型用法
- 延迟队列:消息过期 → DLX → 消费队列(见第三章)
- 失败重试的"墓地":与消费者重试配合,重试耗尽后消息落入死信队列等待人工处理
- 队列溢出保护 :设置
x-max-length,超限时旧消息自动入 DLX 而非丢失
三、延迟消息的两种实现
RabbitMQ 没有原生的延迟消息功能,但可以通过"死信队列"或"延迟消息插件"实现。
3.1 方案 1:死信队列(DLX + TTL)
原理:消息发到没有消费者的队列 → TTL 到期 → 成为死信 → 被 DLX 路由到真正的消费队列。
完整配置:
java
@Configuration
public class DelayByDLXConfig {
private static final String EXCHANGE = "order.event.exchange";
private static final String DELAY_QUEUE = "order.delay.queue"; // 无消费者,等过期
private static final String RELEASE_QUEUE = "order.release.queue"; // 有消费者
private static final String RK_DELAY = "order.create";
private static final String RK_RELEASE = "order.release";
@Bean
public TopicExchange orderExchange() {
return new TopicExchange(EXCHANGE);
}
// 死信队列------消息在此等待过期
@Bean
public Queue delayQueue() {
return QueueBuilder.durable(DELAY_QUEUE)
.deadLetterExchange(EXCHANGE) // 过期后回同一交换机
.deadLetterRoutingKey(RK_RELEASE) // 但换路由键
.ttl(60000) // 固定 60 秒
.build();
}
// 实际消费队列
@Bean
public Queue releaseQueue() {
return QueueBuilder.durable(RELEASE_QUEUE).build();
}
@Bean
public Binding delayBinding() {
return BindingBuilder.bind(delayQueue()).to(orderExchange()).with(RK_DELAY);
}
@Bean
public Binding releaseBinding() {
return BindingBuilder.bind(releaseQueue()).to(orderExchange()).with(RK_RELEASE);
}
}
// 生产者
rabbitTemplate.convertAndSend("order.event.exchange", "order.create", orderMsg);
// 消费者
@RabbitListener(queues = "order.release.queue")
public void onRelease(OrderMsg msg) {
// 60 秒后收到,执行取消逻辑
}
局限性:队列 TTL 固定,无法动态调整。如果有 5 分钟、10 分钟、30 分钟三种延迟,需要建三个死信队列。
3.2 方案 2:延迟消息插件
RabbitMQ 官方提供 rabbitmq_delayed_message_exchange 插件,支持任意延迟时间。
安装(Docker 环境):
bash
# 1. 下载对应版本的 .ez 文件放入插件目录
# 查看插件目录位置
docker inspect mq | grep -A 5 "plugins"
# 2. 将插件文件放入后启用
docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange
# 3. 重启
docker restart mq
使用:
java
// 声明延迟交换机------核心是 delayed="true"
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.target.queue", durable = "true"),
exchange = @Exchange(
name = "delay.exchange",
type = ExchangeTypes.DIRECT, // 延迟交换机不支持 Topic!
delayed = "true" // 标记为延迟交换机
),
key = "delay.key"
))
public void onDelayedMsg(String msg) {
log.info("延迟消息到达: {}", msg);
// 执行业务
}
// 生产者------设置动态延迟时间
@Test
void sendDelayed() {
rabbitTemplate.convertAndSend("delay.exchange", "delay.key",
"30秒后的提醒",
msg -> {
msg.getMessageProperties().setDelayLong(30_000L); // 毫秒
return msg;
});
log.info("延迟消息已发送");
}
延迟插件的限制:
- Exchange 类型只能是 Direct 或 Topic(不是所有类型)
- 延迟时间上限取决于实现,通常不要超过几天
- 大量的延迟消息会占用 Broker 内存(插件用 Mnesia 表存储调度信息)
- 消息在延迟期间 Broker 重启可能导致时间重新计算(取决于版本)
3.3 两种方案对比
| 维度 | 死信队列(DLX + TTL) | 延迟插件 |
|---|---|---|
| 安装 | 原生,无需安装 | 需安装插件 |
| 延迟时间 | 固定(队列级别)或受限于较小值 | 动态,每条消息独立设置 |
| 精度 | 秒级(取决于 TTL 检查周期) | 毫秒级 |
| 扩展性 | 不同延迟需要多个队列 | 一个交换机处理所有延迟 |
| 可靠性 | 依赖持久队列 | 延迟消息存在 Mnesia 表,重启可能丢失 |
| 适用场景 | 延迟时间固定的少量场景 | 灵活延迟、多时间级别 |
选择建议:延迟种类 ≤ 2 个且时间固定时用 DLX;否则用插件。对可靠性要求极高的延迟任务(如 30 分钟后取消订单),建议同时用定时任务扫描数据库做兜底------MQ 延迟作为主力,定时扫描作为补偿。
四、集群部署
单节点有单点故障风险。将多个 RabbitMQ 节点组成集群可以实现负载分担和高可用。
4.1 集群基础概念
元数据 vs 消息数据:
- 元数据(Exchange、Queue、Binding、User、vHost、Policy)在所有节点间自动同步
- 消息数据默认只存在创建队列的那个节点上(Classic 队列),其他节点存指针
磁盘节点 vs 内存节点 :仅影响元数据存储位置。消息数据是否持久化由 delivery_mode 和队列类型决定。
- 磁盘节点:元数据写入磁盘,重启不丢失。集群至少需要一个磁盘节点
- 内存节点:元数据仅存内存,重启后从磁盘节点同步。性能略好,但一般没必要------现代服务器内存足够
Erlang Cookie :集群中的所有节点必须有相同的 .erlang.cookie 文件(通常位于 /var/lib/rabbitmq/.erlang.cookie),这是节点间认证的密钥。
4.2 Docker Compose 搭建三节点集群
yaml
# docker-compose.yml
version: "3.8"
services:
rabbitmq1:
image: rabbitmq:3.12-management
hostname: rabbitmq1
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=admin123
- RABBITMQ_ERLANG_COOKIE=my_secret_cookie
ports:
- "5672:5672" # AMQP
- "15672:15672" # Management UI
volumes:
- rabbitmq1_data:/var/lib/rabbitmq
rabbitmq2:
image: rabbitmq:3.12-management
hostname: rabbitmq2
environment:
- RABBITMQ_ERLANG_COOKIE=my_secret_cookie
ports:
- "5673:5672"
- "15673:15672"
volumes:
- rabbitmq2_data:/var/lib/rabbitmq
rabbitmq3:
image: rabbitmq:3.12-management
hostname: rabbitmq3
environment:
- RABBITMQ_ERLANG_COOKIE=my_secret_cookie
ports:
- "5674:5672"
- "15674:15672"
volumes:
- rabbitmq3_data:/var/lib/rabbitmq
volumes:
rabbitmq1_data:
rabbitmq2_data:
rabbitmq3_data:
启动后将 rabbitmq2 和 rabbitmq3 加入 rabbitmq1 的集群:
bash
# 启动所有节点
docker compose up -d
# 将 rabbitmq2 加入集群
docker exec -it rabbitmq2 rabbitmqctl stop_app
docker exec -it rabbitmq2 rabbitmqctl reset # 清空本节点数据
docker exec -it rabbitmq2 rabbitmqctl join_cluster rabbit@rabbitmq1
docker exec -it rabbitmq2 rabbitmqctl start_app
# 将 rabbitmq3 加入集群
docker exec -it rabbitmq3 rabbitmqctl stop_app
docker exec -it rabbitmq3 rabbitmqctl reset
docker exec -it rabbitmq3 rabbitmqctl join_cluster rabbit@rabbitmq1
docker exec -it rabbitmq3 rabbitmqctl start_app
# 查看集群状态
docker exec -it rabbitmq1 rabbitmqctl cluster_status
磁盘 / 内存节点指定 :join_cluster 默认以磁盘节点加入。如需内存节点:
bash
rabbitmqctl join_cluster rabbit@rabbitmq1 --ram
至少保留 2 个磁盘节点------只有一个磁盘节点时,它宕机后集群元数据变更会失败。
节点离开集群:
bash
rabbitmqctl stop_app
rabbitmqctl reset # 同时离开集群
rabbitmqctl start_app
4.3 三种数据冗余方式
4.3.1 普通集群(默认)
消息数据只存在队列所在节点。消费者连到非所在节点时,集群内部做 TCP 转发。不提供高可用------队列所在节点宕机,该队列不可用。
4.3.2 镜像队列(Mirrored Queue,传统方案)
基于普通集群,额外将队列消息复制到多个节点。通过 Policy 控制:
bash
# 在任意节点上执行
rabbitmqctl set_policy ha-all "^ha\." '{
"ha-mode": "all",
"ha-sync-mode": "automatic"
}'
Policy 匹配规则:
ha-mode=all:复制到所有节点ha-mode=exactly, ha-params=N:复制到 N 个节点(含 Master)ha-mode=nodes, ha-params=["rabbit@node1","rabbit@node2"]:指定节点列表
ha-sync-mode=automatic 表示新镜像节点自动同步已有消息。在大量积压时同步可能阻塞队列------对大积压队列建议手动同步:
bash
rabbitmqctl sync_queue <queue-name>
镜像队列的缺点:
- 主节点完成写入后通过 GM(Guaranteed Multicast)协议同步到镜像,有复制延迟
- 网络分区时可能出现脑裂(两个节点同时认为自己是 Master)
- RabbitMQ 3.8+ 官方推荐新项目使用 Quorum Queue 代替
4.3.3 仲裁队列(Quorum Queue,推荐)
RabbitMQ 3.8 引入,基于 Raft 协议实现强一致性复制。解决镜像队列的老大难问题:
- 写入消息需要多数派确认,不会出现脑裂造成的数据不一致
- Leader 节点故障时自动选举新 Leader,对外无感知
- 消费使用"单活跃消费者"模式(polling-based),天然支持 consumer failover
声明仲裁队列:
java
@Bean
public Queue quorumQueue() {
return QueueBuilder.durable("my.quorum.queue")
.quorum() // x-queue-type=quorum
.build();
}
或通过 Policy 将匹配的队列自动转为仲裁队列:
bash
rabbitmqctl set_policy quorum-queues "^q\." '{"queue-mode": "quorum"}' --apply-to queues
Quorum Queue 的注意事项:
- 不支持消息 TTL(3.10+ 部分支持),需要 DLX 的场景注意兼容性
x-max-length行为不同于 Classic 队列------丢失的是队尾消息而非队首- 内存占用高于 Classic 队列(Raft 日志 + 索引)
- 单条消息超过 5MB 不建议使用 Quorum Queue(Raft 协议限制)
4.4 负载均衡(HAProxy)
客户端不应直连某个 RabbitMQ 节点,而是通过负载均衡器连接,享受故障转移和连接分发:
client → HAProxy (TCP LB) → rabbitmq1:5672
→ rabbitmq2:5672
→ rabbitmq3:5672
HAProxy 配置 (haproxy.cfg):
global
log 127.0.0.1 local0 info
maxconn 4096
defaults
log global
mode tcp
option tcplog
timeout connect 5s
timeout client 30s
timeout server 30s
frontend rabbitmq_amqp
bind *:5672
default_backend rabbitmq_amqp_backend
backend rabbitmq_amqp_backend
balance roundrobin
server rabbitmq1 192.168.56.10:5672 check inter 5s rise 2 fall 3
server rabbitmq2 192.168.56.11:5672 check inter 5s rise 2 fall 3
server rabbitmq3 192.168.56.12:5672 check inter 5s rise 2 fall 3
Spring Boot 连接 HAProxy:
yaml
spring:
rabbitmq:
host: haproxy-host # HAProxy 地址
port: 5672
# addresses 用于直连多个节点(不用 HAProxy 时)
# addresses: rabbitmq1:5672, rabbitmq2:5672, rabbitmq3:5672
选择直连还是 HAProxy?
- HAProxy:统一的入口,故障转移对应用完全透明,运维方便
- 直连(addresses):少一层转发,降低延迟,但应用需自己处理连接故障转移
4.5 网络分区(Split-Brain)处理
集群节点间网络断开时会形成分区。RabbitMQ 提供三种处理模式:
| 模式 | 行为 | 适用场景 |
|---|---|---|
ignore |
不做任何处理,等网络恢复 | 单机房、网络稳定 |
pause-minority |
少数派节点暂停服务 | 多机房部署 |
autoheal |
网络恢复时自动选胜方重建集群 | 非关键集群 |
配置(/etc/rabbitmq/rabbitmq.conf):
cluster_partition_handling = pause-minority
生产环境推荐 pause-minority------少数派节点停止服务而非分裂成两个独立集群,避免数据不一致。但这意味着需要奇数个节点(3、5、7),使少数派在节点数一半以下时生效。
五、跨集群通信:Federation 与 Shovel
跨数据中心、异地多活或跨 vHost 的消息同步需要使用 Federation 或 Shovel 插件。
5.1 Federation 插件
Federation 在集群间建立逻辑连接,将上游(upstream)的消息"拉"到下游(downstream),下游节点表现为本地队列或 Exchange。支持:
- Exchange Federation:同步上游 Exchange 的消息到下游同名 Exchange
- Queue Federation:从上游队列拉取消息到下游本地队列
典型场景:两个数据中心各部署一套 RabbitMQ 集群,通过 Federation 同步消息。
配置 Exchange Federation:
bash
# 1. 在两个集群上都启用 Federation 插件
rabbitmq-plugins enable rabbitmq_federation rabbitmq_federation_management
# 2. 在下游集群(需要接收消息的那一侧)配置 upstream
rabbitmqctl set_parameter federation-upstream dc-upstream \
'{"uri": "amqp://admin:password@upstream-cluster-lb:5672/%2f"}'
# 3. 在下游集群配置 policy,将 Federation 应用到特定 Exchange
rabbitmqctl set_policy federate-exchanges "^fed\\." \
'{"federation-upstream": "dc-upstream"}' \
--apply-to exchanges
上游集群中所有匹配 ^fed\. 的 Exchange 的消息,会自动被拉取到下游集群的同名 Exchange。
5.2 Shovel 插件
Shovel 比 Federation 更低层------它是一个消息搬运工具,从源(队列)消费消息并发布到目标(Exchange)。支持动态配置和静态配置。
与 Federation 的区别:
| 维度 | Federation | Shovel |
|---|---|---|
| 工作层级 | Exchange / Queue 逻辑连接 | 消息级的搬运 |
| 配置方式 | Policy 动态匹配 | 静态配置或 Parameter |
| 适用场景 | 大规模 Exchange 同步 | 指定队列的定向搬运 |
| WAN 优化 | 内置批处理 | 需手动配置批量 ACK |
动态 Shovel 配置示例:
bash
# 在目标集群配置 Shovel:从源拉取
rabbitmqctl set_parameter shovel cross-dc-shovel \
'{
"src-uri": "amqp://source-cluster:5672",
"src-queue": "orders.from.dc1",
"dest-uri": "amqp://localhost:5672",
"dest-exchange": "orders.consolidated",
"prefetch-count": 100,
"reconnect-delay": 5,
"ack-mode": "on-confirm"
}'
5.3 选型建议
- 同城双活、Exchange 批量同步 → Federation
- 特定队列的跨集群搬运、不同版本 RabbitMQ 之间 → Shovel
- 两个方案都依赖网络稳定,跨公网建议配置 TLS
六、监控与运维
6.1 Management 插件
RabbitMQ 内置 Management 插件,提供 Web UI(默认端口 15672)和 HTTP API。
bash
# 启用
rabbitmq-plugins enable rabbitmq_management
# HTTP API 示例
curl -u admin:admin http://localhost:15672/api/overview # 集群概览
curl -u admin:admin http://localhost:15672/api/queues # 所有队列
curl -u admin:admin http://localhost:15672/api/queues/%2f/order.queue # 特定队列
Management UI 中需要重点关注:
- Overview → Memory used (内存使用)、Disk space(磁盘空间)
- Queues → Ready (待消费数)、Unacked (已投递未确认数)、Messages(总数)
- Queues → Message rates(Publish / Deliver / Ack 速率)
- Nodes → File descriptors used(接近上限时会出现连接拒绝)
6.2 Prometheus + Grafana
RabbitMQ 3.8+ 内置 Prometheus 插件:
bash
rabbitmq-plugins enable rabbitmq_prometheus
Prometheus 抓取端点:http://rabbitmq-host:15692/metrics
关键指标:
| Prometheus Metric | 含义 | 告警建议 |
|---|---|---|
rabbitmq_queue_messages_ready |
队列中待消费消息数 | > 阈值说明积压 |
rabbitmq_queue_messages_unacked |
已投递未确认数 | > 阈值可能消费者异常 |
rabbitmq_connections_total |
连接总数 | 突降说明连接断开 |
rabbitmq_node_mem_used |
节点内存使用 | > 0.8 * mem_limit |
rabbitmq_node_disk_space_available |
磁盘可用空间 | < 2GB 触发阻塞 |
rabbitmq_detailed_queue_messages_published_total |
发布速率 | 趋势判断 |
rabbitmq_channel_messages_confirmed_total |
Confirm 确认数 | 对比发布量看成功率 |
Grafana 仪表板:RabbitMQ 官方提供仪表板模板(ID: 10991),导入即可使用。
6.3 日志追踪
生产环境应启用 Firehose Tracer 或 rabbitmq_tracing 插件来追踪消息流向:
bash
rabbitmq-plugins enable rabbitmq_tracing
# 在 Management UI 的 Admin → Tracing 中添加 trace,指定 vhost 和过滤条件
对于 Spring AMQP,日志级别调整:
yaml
logging:
level:
org.springframework.amqp.rabbit: DEBUG # 输出确认、重试等详细日志
6.4 日常巡检清单
- 节点状态:
rabbitmq-diagnostics status→ 检查是否有节点未运行 - 集群健康:
rabbitmq-diagnostics cluster_status→ 分区检测 - 磁盘水位:
rabbitmq-diagnostics environment | grep disk_free_limit - 内存水位:
rabbitmq-diagnostics environment | grep vm_memory_high_watermark - 连接数:
rabbitmqctl list_connections | wc -l - 队列积压:
rabbitmqctl list_queues name messages | sort -t$'\t' -k2 -rn | head -20(积压 Top 20)
七、安全配置
7.1 用户与权限
RabbitMQ 的权限模型基于 vHost 隔离。用户被授予对特定 vHost 的 configure (创建/删除资源)、write (发布消息)、read(消费消息)权限。
bash
# 创建 vHost
rabbitmqctl add_vhost app_vhost
# 创建用户
rabbitmqctl add_user app_user secure_password
# 授予权限:vHost 级别
rabbitmqctl set_permissions -p app_vhost app_user "^app\." "^app\." "^app\."
# 授予管理员标签(可登录 Management UI)
rabbitmqctl set_user_tags app_user monitoring
权限正则说明:三个正则分别控制 configure、write、read 可以操作的资源(Exchange 和 Queue 名称)。
Spring AMQP 中的 vHost 隔离:
yaml
spring:
rabbitmq:
virtual-host: app_vhost # 不同业务线用不同 vHost
username: app_user
password: ${RABBITMQ_PASSWORD}
7.2 TLS / SSL 加密
生产环境跨网络传输时应开启 TLS:
bash
# rabbitmq.conf
listeners.ssl.default = 5671
ssl_options.cacertfile = /etc/rabbitmq/certs/ca_certificate.pem
ssl_options.certfile = /etc/rabbitmq/certs/server_certificate.pem
ssl_options.keyfile = /etc/rabbitmq/certs/server_key.pem
ssl_options.verify = verify_peer
ssl_options.fail_if_no_peer_cert = true
Spring AMQP 配置 TLS:
yaml
spring:
rabbitmq:
ssl:
enabled: true
algorithm: TLSv1.3
key-store: classpath:client-truststore.jks
key-store-password: ${TRUSTSTORE_PASSWORD}
validate-server-certificate: true
7.3 其他安全措施
-
删除默认
guest用户(或限制其仅 localhost 访问) -
Management API 绑定内网 IP,禁用公网暴露
-
使用
rabbitmq-auth-backend-ldap等插件集成企业认证体系 -
通过
rabbitmq.conf限制连接频率:限制每个连接每秒最多 50 个 Channel 打开请求
channel_max = 50
限制每个 vHost 最大连接数
default_vhost_connection_limit = 500
八、性能调优
8.1 内存与磁盘水位
RabbitMQ 使用内存和磁盘水位阈值来触发 Flow Control------当阈值被冲破,所有发布者被阻塞(TCP back pressure):
# rabbitmq.conf
vm_memory_high_watermark.relative = 0.6 # 内存使用 60% 时告警,触发 GC 和 Flow Control
vm_memory_high_watermark.absolute = 2GB # 或使用绝对值
disk_free_limit.absolute = 2GB # 磁盘剩余 < 2GB 时阻塞
在内存水位以下,RabbitMQ 会尽力将消息保留在内存中以获得更好性能。超过水位后,消息开始被刷到磁盘。
8.2 队列与消息大小
- 单条消息建议 < 10MB。更大消息考虑使用对象存储(如 S3)+ 消息体中放 URL
- 经典队列不建议超过 10 万条消息积压。大积压用 Lazy Queue 或 Stream Queue
- 对于 Stream Queue,Segment 大小可通过
x-stream-max-segment-size-bytes调整(默认 500MB)
8.3 生产者优化
- 批量发送 + 异步 Confirm:单个 Channel 上发多条消息后统一等待 Confirm,而非发一条等一条
- 使用
PooledChannelConnectionFactory提升并发发送能力 - 避免在 Confirm 回调中做重业务逻辑------它运行在 I/O 线程上
8.4 消费者优化
prefetch不要设为 0------那意味着无限制推送- 对于处理时间不均匀的场景,
prefetch=1最佳 - 对于处理时间均匀且追求吞吐的场景,可设
prefetch=50~200 - 消费者线程数不要超过 CPU 核数太多------RabbitMQ Channel 间的上下文切换反而降低吞吐
8.5 OS 调优
# 增大文件描述符限制
ulimit -n 65536
# 减少 TCP 连接的 TIME_WAIT
net.ipv4.tcp_fin_timeout = 30
# rabbitmq.conf 中的 TCP 参数
tcp_listen_options.backlog = 1024
tcp_listen_options.nodelay = true # 禁用 Nagle 算法
tcp_listen_options.linger.on = true
tcp_listen_options.linger.timeout = 0
九、生产环境规范
9.1 命名规范
| 组件 | 命名格式 | 示例 |
|---|---|---|
| Exchange | EX.{SourceApp}.{Module}.{Event} |
EX.order.payment.paid |
| Queue | MQ.{SourceApp}.{TargetApp}.{BizType} |
MQ.order.notification.push |
| RoutingKey | {domain}.{entity}.{action} |
order.payment.paid.success |
| vHost | /{env}/{biz_line} |
/prod/trade |
命名要点:
- RoutingKey 用
.分层,适配 Topic Exchange 的通配符(*单层、#多层) - Queue 名称体现生产者和消费者,方便排查"谁发了、谁消费"
- vHost 按环境+业务线隔离,不要所有业务共用
/
9.2 环境隔离
/dev/trade → 开发环境
/test/trade → 测试环境
/prod/trade → 生产环境
不同环境的 Exchange、Queue 命名保持一致,通过 vHost 区分,方便代码统一和问题排查。
9.3 异常处理规范
- 所有
@RabbitListener方法内部捕获异常,不要将非业务异常抛给 Spring AMQP - 对不可恢复的异常(如 JSON 格式错误),抛出
AmqpRejectAndDontRequeueException - 对可重试的异常(如数据库连接超时),抛出原始异常让 Spring 重试
- 所有异常队列配置告警,不同业务线可以共用一个异常交换机,但建议各建各的异常队列
9.4 部署与运维规范
- 生产集群至少 3 个节点(Quorum Queue 需要 3 节点实现 Raft 多数派)
- 保留 2 个磁盘节点,其余可以是内存节点(如果需要)
- HAProxy 或 LVS 部署在 RabbitMQ 前面,客户端不直连节点
- 所有 RabbitMQ 节点时间同步(NTP),避免 TTL 计算偏差
- 常规备份:定时备份 RabbitMQ 配置和定义(
rabbitmqctl export_definitions) - 灾备演练:定期演练从备份恢复 + Federation/Shovel 切换
bash
# 导出全部定义(Exchange、Queue、Binding、Policy、User 等)
rabbitmqctl export_definitions /backup/rabbitmq-definitions-$(date +%Y%m%d).json
# 恢复
rabbitmqctl import_definitions /backup/rabbitmq-definitions-20250510.json
十、总结
RabbitMQ 生产级使用可以归纳为几条核心决策:
队列选型:
- 新项目、高可用 → Quorum Queue(RabbitMQ 3.8+)
- 大积压、日志类 → Stream Queue(RabbitMQ 3.9+)
- 简单场景、大量临时队列 → Classic Queue
可靠性分层:
- 核心链路(支付、交易):Confirm + 持久化 + 本地重试 + RepublishMessageRecoverer + 幂等 + 落库补偿 + 定时兜底
- 一般业务:Confirm + 持久化 + 重试 + 幂等
- 非关键链路(日志、埋点):关闭 Confirm,关闭重试,优先吞吐
延迟消息:
- 固定 1-2 种延迟 → DLX + TTL
- 多种动态延迟 → 延迟插件
- 可靠性要求高 → 延迟消息 + DB 定时扫描兜底
高可用:
- 3 节点集群 + Quorum Queue / 镜像队列 + HAProxy
- 跨集群同步 → Federation(Exchange 级)或 Shovel(Queue 级)
- 监控告警:积压、内存、磁盘、连接数
每个项目对可靠性、延迟、吞吐的需求不同,没有万能配置。关键是理解每条消息经过了哪些节点、哪些缓冲区、哪些 fsync,然后根据业务容忍度做取舍。
