黑马 RabbitMq 高级篇 学习记录

前言

本文内容作了适度扩展,不止于黑马教的内容

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 对评估可靠性很重要:

  1. Classic Queue + 持久化消息 :消息写入 msg_store_persistent,由 queue_index_max_journal_entries 控制 journal 刷盘频率。默认每 16384 条或 200ms 刷一次。
  2. Lazy Queue:消息直接写入 segment 文件,依赖操作系统 page cache。
  3. 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 通过 concurrentConsumersmaxConcurrentConsumers 控制消费线程数:

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.rejectbasic.nack,且 requeue=false 消息格式校验失败
过期(expired) 消息 TTL 到期 超时订单取消
队列溢出(maxlen) 队列消息数超过 x-max-lengthx-max-length-bytes 积压保护,旧消息被挤出

2.2 DLX 核心配置

死信队列并非特殊队列类型------任何队列都可以通过 x-dead-letter-exchangex-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 的典型用法

  1. 延迟队列:消息过期 → DLX → 消费队列(见第三章)
  2. 失败重试的"墓地":与消费者重试配合,重试耗尽后消息落入死信队列等待人工处理
  3. 队列溢出保护 :设置 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,然后根据业务容忍度做取舍。

相关推荐
老码观察1 小时前
分布式系统核心理论与实践:从CAP到工程落地
分布式
Yolo566Q1 小时前
环境土壤物理模型HYDRUS1D/2D/3D实践技术应用系统性学习
大数据·开发语言·gpt·学习·arcgis·r语言
金色光环1 小时前
【DSP学习笔记】 F28335中断系统理解-基于普中DSP28335开发攻略
笔记·单片机·学习·dsp开发
青稞社区.1 小时前
OpenAI 翁家翌:“启发式学习”的强化学习新范式
人工智能·经验分享·学习·agi
晓梦林1 小时前
Laoda靶场学习笔记
笔记·学习
承渊政道2 小时前
Oracle迁移避坑:一个(+)写错,LEFT JOIN可能变INNER JOIN
运维·服务器·数据库·数据仓库·学习·安全·oracle
知识分享小能手2 小时前
R语言入门学习教程,从入门到精通,R语言流程控制语句(5)
开发语言·学习·r语言
YangYang9YangYan2 小时前
2026营销新人学习数据分析的应用
学习·数据挖掘·数据分析
燐妤2 小时前
前端HTML编程4:深入学习CSS
前端·学习·html
赵渝强老师2 小时前
【赵渝强老师】Hadoop的伪分布部署模式
大数据·hadoop·分布式