Skip to content

MQ(RabbitMQ)学习笔记 ​

跟着黑马 SpringCloud 微服务课程(黑马商城)学习 RabbitMQ 的完整笔记。分基础(消息模型、交换机、SpringAMQP 使用)与高级(可靠性、死信、延迟消息、惰性队列、集群)两部分,配合课程进度复习。

1. 初识MQ与同步异步通讯 ​

【重点要点】

  • 为什么用 MQ(消息队列):解决服务间同步调用的三大痛点——耦合(A 调 B 必须知道 B 存在)、性能(多个调用串行叠加耗时)、级联失败(B 挂了 A 也挂)。
  • 同步调用示例:下单 → 扣库存(HTTP)→ 减积分(HTTP)→ 发短信(HTTP),一次下单要串行等三个服务返回,总耗时长、任一失败全失败。
  • 异步调用:下单后只发一条消息到 MQ,各服务异步消费,解耦 + 削峰(高并发先入队,消费端慢慢消化)+ 性能提升(发完消息立刻返回)。
  • MQ 核心概念:Producer(生产者)、Broker(消息服务器)、Queue(队列,消息存储处)、Consumer(消费者)。
  • 常见 MQ 对比:RabbitMQ(AMQP,路由灵活、功能全面,课程用这个)、Kafka(高吞吐、日志/流式场景)、RocketMQ(阿里出品、事务消息/延迟消息好)。
  • 易错点:异步不是银盒——消息可能丢失/重复,所以后续要学可靠性(发送确认、持久化、ack、幂等);业务流程变更后消息结构要兼容(加字段别删字段)。
text
同步调用(阻塞):              异步调用(解耦):
订单服务 → 库存服务   ↩等待   订单服务 --发布消息--> [MQ队列] --消费--> 库存服务
          → 积分服务   ↩等待                         --消费--> 积分服务
          → 短信服务   ↩等待                         --消费--> 短信服务

2. RabbitMQ安装与快速入门 ​

【重点要点】

  • 安装:Docker 一条命令起 RabbitMQ(带管理插件),访问管理页 http://localhost:15672(默认账号 guest/guest)。
  • 管理页五个核心页签:Overview(总览)、Connections(连接)、Channels(信道)、Exchanges(交换机)、Queues(队列)。
  • 端口说明:5672 是 AMQP 通信端口(程序用)、15672 是管理界面端口(浏览器用)。
  • 基本工作模型:生产者 → 交换机(Exchange)→ 按绑定路由到队列(Queue)→ 消费者;RabbitMQ 里消息永远先到交换机,再由交换机按规则投递到队列。
  • 队列和交换机默认是持久化的吗?——不是,默认不持久化,服务重启会丢失(第 10 章讲持久化)。
  • 易错点:先创建交换机/队列再发消息(自动声明也可以,但显式声明更可控);管理页改配置后要刷新队列才生效;Docker 容器删了数据全没(要挂数据卷,参考 Docker 笔记的数据卷章节)。
bash
# Docker 启动 RabbitMQ(带管理界面)
docker run -d --name rabbitmq \
  -p 5672:5672 -p 15672:15672 \
  rabbitmq:3-management

3. SpringAMQP消息发送与接收 ​

【重点要点】

  • SpringAMQP 是 Spring 对 AMQP 协议的封装:发送用 RabbitTemplate,消费用 @RabbitListener,声明用 @Bean/@Queue/@Exchange/@Binding。
  • 引入依赖:spring-boot-starter-amqp;配置 spring.rabbitmq.host/port/username/password。
  • 发送:rabbitTemplate.convertAndSend("队列名或交换机名", "消息");默认发到默认交换机(名称为空字符串),按路由键=队列名投递。
  • 消费:@RabbitListener(queues = "队列名") 标注方法,参数就是消息体;类需要是 Spring Bean。
  • 最简单的入门:一个队列、生产者发、消费者收——消息会以字符串发送,默认用 Java 序列化(第 8 章换 JSON)。
  • 易错点:convertAndSend 的第二个参数是"路由键"不是"队列名"(默认交换机场景下两者同名才收得到);消费者方法所在类必须在 Spring 容器中;@RabbitListener 监听不存在的队列会启动报错。
yaml
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
java
// 生产者:发送消息到默认交换机,路由键=队列名 queue.simple
@Autowired
private RabbitTemplate rabbitTemplate;

public void sendSimple(String msg) {
    rabbitTemplate.convertAndSend("queue.simple", msg);
}
java
// 消费者:监听队列 queue.simple
@Component
public class SimpleListener {

    @RabbitListener(queues = "queue.simple")
    public void onMessage(String msg) {
        System.out.println("收到消息:" + msg);
    }
}

4. WorkQueue工作队列 ​

【重点要点】

  • 场景:一个队列多条消息,用多个消费者一起消费提高吞吐(如发短信并发处理)。
  • 默认行为:RabbitMQ 会把消息轮询(round-robin)均分给各消费者——不考虑谁快谁慢。
  • 能者多劳(公平分发):设置预取值(prefetch),消费者一次只取 1 条,处理完再取下一条,手慢的消费者不会拖后腿: spring.rabbitmq.listener.simple.prefetch: 1。
  • 预取原理:消费者主动拉取,本地缓存条数=prefetch;prefetch 越大,消息越容易堆积在快消费者手里。
  • 易错点:轮询模式下若一条消息处理很慢,其他消费者干等 → 务必配 prefetch=1 实现能者多劳;@RabbitListener 默认线程池并发消费,prefetch 是每消费者的预取数。
yaml
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1        # 能者多劳:一次只取一条

5. Fanout广播交换机 ​

【重点要点】

  • Fanout(广播)交换机:把消息广播给所有绑定它的队列,不看路由键(路由键被忽略)。
  • 适用:全局通知场景(系统公告 → 通知服务、短信服务、日志服务都收到同一份消息)。
  • 声明方式:@Bean FanoutExchange("fanout.exchange") + @Bean Binding(BindingBuilder.bind(队列).to(交换机))。
  • 易错点:Fanout 不在乎 routing key,发消息时第二个参数随便传(惯例传空串);绑定关系在管理页 Exchanges → 点交换机看 Binding 检查。
java
// 声明交换机、两个队列、两个绑定
@Configuration
public class FanoutConfig {

    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange("fanout.exchange");
    }

    @Bean
    public Queue fanoutQueue1() {
        return new Queue("fanout.queue1");
    }

    @Bean
    public Queue fanoutQueue2() {
        return new Queue("fanout.queue2");
    }

    @Bean
    public Binding fanoutBinding1() {
        return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
    }

    @Bean
    public Binding fanoutBinding2() {
        return BindingBuilder.bind(fanoutQueue2()).to(fanoutExchange());
    }
}
java
// 发送:路由键忽略,所有绑定队列都收到
rabbitTemplate.convertAndSend("fanout.exchange", "", "广播消息");

6. Direct路由交换机 ​

【重点要点】

  • Direct(路由)交换机:按路由键(routing key)精确匹配投递——队列绑定交换机时指定一个 routing key,消息的 routing key 与绑定的 key 完全相等才投递。
  • 适用:定向通知(下单成功消息 → 只发给"短信队列",不发"日志队列")。
  • 一个队列可以绑定多个 routing key;多个队列也可以绑定同一个 key(此时等价于广播到这几个队列)。
  • 易错点:routing key 是精确字符串匹配,大小写敏感,拼写不一致消息就"丢了";声明时 BindingBuilder.bind(queue).to(exchange).with("key") 的 with 参数就是 routing key。
java
@Configuration
public class DirectConfig {

    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("direct.exchange");
    }

    @Bean
    public Queue directQueue() {
        return new Queue("direct.queue");
    }

    @Bean
    public Binding directBinding() {
        // 队列绑定到交换机,路由键 order.success
        return BindingBuilder.bind(directQueue())
                .to(directExchange())
                .with("order.success");
    }
}
java
// 发送:路由键必须与绑定 key 完全一致
rabbitTemplate.convertAndSend("direct.exchange", "order.success", "下单成功通知");
// 用 "order.failed" 发则这个队列收不到

7. Topic主题交换机 ​

【重点要点】

  • Topic(主题)交换机:路由键支持通配符模糊匹配,是最灵活的交换机:
    • #:匹配零个或多个词(order.# 能匹配 order.created、order.paid、order)
    • *:匹配恰好一个词(order.* 只匹配 order.created 这类单段)
  • 适用:按主题分类的通知(所有 order.xxx 消息都进订单队列,log.error 才进错误告警队列)。
  • 绑定 key 与消息 key 都以 . 分隔单词;# 在绑定 key 中表示"这里可以是任意多段"。
  • 易错点:* 不能匹配零个词(order.* 匹配不到 order);# 和 * 的区别是"多段 vs 一段";消息路由键最多 255 字节。
java
@Configuration
public class TopicConfig {

    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange("topic.exchange");
    }

    @Bean
    public Queue topicQueue() {
        return new Queue("topic.queue");
    }

    @Bean
    public Binding topicBinding() {
        // 绑定 key:order.# 匹配所有 order 开头的消息
        return BindingBuilder.bind(topicQueue())
                .to(topicExchange())
                .with("order.#");
    }
}
java
// 发消息:order.created / order.paid / order.cancel 都会被 topic.queue 收到
rabbitTemplate.convertAndSend("topic.exchange", "order.created", "创建订单");
rabbitTemplate.convertAndSend("topic.exchange", "order.paid", "支付成功");

8. 消息转换器与JSON序列化 ​

【重点要点】

  • 问题:SpringAMQP 默认用 JDK 序列化(Java 对象 → 二进制),乱码、体积大、跨语言不兼容。
  • 解决:配置 Jackson2JsonMessageConverter,消息自动转 JSON;对象在消费者端也能反序列化回 Java 对象。
  • 配置方式:声明一个 @Bean MessageConverter(Jackson2JsonMessageConverter),SpringAMQP 自动使用;可自定义 ObjectMapper(日期格式等)。
  • 发送对象:convertAndSend 传 Java 对象即可,自动 JSON;消费者方法参数直接写对象类型。
  • 易错点:MessageConverter Bean 必须唯一,否则启动报错;消费者端类全限定名/字段要一致(JSON 反序列化按字段匹配,字段缺失给默认值);生产环境别用 JDK 序列化(安全漏洞 CVE 风险)。
java
@Configuration
public class MqConfig {

    @Bean
    public MessageConverter messageConverter() {
        // 使用 JSON 序列化消息
        return new Jackson2JsonMessageConverter();
    }
}
java
// 发送对象:自动转 JSON
@Getter @Setter
public class Order {
    private Long id;
    private Integer num;
}

rabbitTemplate.convertAndSend("direct.exchange", "order.success", new Order(1L, 10));
java
// 消费者直接接收对象
@RabbitListener(queues = "direct.queue")
public void onOrder(Order order) {
    System.out.println(order.getId() + " " + order.getNum());
}

9. 发送者的可靠性 ​

【重点要点】

  • 发送者可靠性 = 保证消息从生产者成功到达交换机。三种手段:
    • 生产者重连:网络抖动自动重试(spring.rabbitmq.template.retry.enabled: true,注意只重连不重发)。
    • publisher-confirm(发送确认):交换机收到消息后回调确认——spring.rabbitmq.publisher-confirm-type: correlated,通过 CorrelationData + ConfirmCallback 判断发送成功/失败。
    • publisher-return(退回通知):消息路由不到任何队列时回调——spring.rabbitmq.publisher-returns: true,实现 ReturnsCallback(ReturnedMessage)拿到被退回的消息。
  • confirm 与 return 的区别:confirm 证明"到交换机了",return 提醒"交换机没投递出去"(路由失败),两个配合才能覆盖发送端全链路。
  • 易错点:confirm 回调是异步的,要在回调里做补偿(记录失败/重新发送);correlated 模式必须给每条消息配 CorrelationData(用 id 关联);return 回调要设置 mandatory(setMandatory(true))才会触发。
yaml
spring:
  rabbitmq:
    publisher-confirm-type: correlated    # 开启发送确认
    publisher-returns: true               # 开启退回通知
    template:
      mandatory: true                     # 消息无法路由时触发 ReturnsCallback
java
// 发送 + 确认回调
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());

rabbitTemplate.convertAndSend("direct.exchange", "order.success", msg, cd);

cd.getFuture().whenComplete((ack, cause) -> {
    if (ack != null && ack.isAck()) {
        System.out.println("消息已确认到达交换机");
    } else {
        System.out.println("消息发送失败,原因:" + cause);
        // 补偿:记录日志 / 重发
    }
});
java
// 返回回调:路由不到队列的消息
@Configuration
public class ReturnConfig implements RabbitTemplate.ReturnsCallback {

    @Override
    public void returnedMessage(ReturnedMessage returned) {
        System.out.println("路由失败:" + returned.getReplyText()
                + ",消息:" + new String(returned.getMessage().getBody()));
    }
}

10. MQ的可靠性-持久化 ​

【重点要点】

  • MQ 自身可靠性 = 持久化,三层都要做,缺一不可:
    • 交换机持久化:声明时 durable = true(new DirectExchange("xxx", true, false))。
    • 队列持久化:声明时 durable = true(new Queue("xxx", true))。
    • 消息持久化:发送时指定 deliveryMode = 2(MessageDeliveryMode.PERSISTENT)。
  • SpringAMQP 默认:new Queue(name) 默认 durable=true;new DirectExchange(name) 默认 durable=true;convertAndSend 发送的 SimpleMessageConverter 默认持久化?——不,要显式确认(可靠做法:用 MessageProperties 设 deliveryMode)。
  • 持久化原理:消息写入磁盘 + 内存镜像;RabbitMQ 重启后从磁盘恢复未消费的消息。
  • 惰性队列(lazy queue) 是另一种"更彻底"的持久化:消息直接落磁盘(第 16 章详讲)。
  • 易错点:只持久化消息但队列非持久化 → 重启队列没了消息也没了;已有队列改 durable 不生效(要删掉重建);持久化牺牲部分吞吐(写入磁盘开销),权衡使用。
java
// 持久化的交换机 + 队列
@Bean
public DirectExchange durableExchange() {
    return new DirectExchange("durable.exchange", true, false);  // durable=true
}

@Bean
public Queue durableQueue() {
    return new Queue("durable.queue", true);                      // durable=true
}
java
// 发送持久化消息
Message message = MessageBuilder
        .withBody("内容".getBytes(StandardCharsets.UTF_8))
        .setDeliveryMode(MessageDeliveryMode.PERSISTENT)          // 消息持久化
        .build();
rabbitTemplate.convertAndSend("durable.exchange", "key", message);

11. 消费者的可靠性-手动ack ​

【重点要点】

  • 消费者的可靠性 = 确保消息被正确处理。RabbitMQ 默认自动确认(auto ack):消息一取走就确认删除——如果消费者处理中途异常,消息就丢失了。
  • 解决:手动确认(manual ack)——spring.rabbitmq.listener.simple.acknowledge-mode: manual,方法参数加 Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag。
  • 三种确认方式:
    • channel.basicAck(tag, false):确认成功,消息删除。
    • channel.basicNack(tag, false, requeue):拒绝,requeue=true 回队重试 / false 丢弃(或进死信,第 13 章)。
    • channel.basicReject(tag, requeue):单条拒绝(nack 可批量)。
  • 易错点:手动 ack 模式下忘记 ack 会一直不确认 → 消息一直不被删除、堆积(unacked 数量增长);requeue=true 无限重试会造成死循环(第 12 章讲结合重试策略);多线程消费注意 tag 与消息对应。
yaml
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual     # 手动确认
java
@RabbitListener(queues = "direct.queue")
public void onOrder(Order order, Channel channel,
                    @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
    try {
        System.out.println("处理:" + order.getId());
        // 业务逻辑
        channel.basicAck(tag, false);            // 成功:确认
    } catch (Exception e) {
        channel.basicNack(tag, false, false);    // 失败:丢弃(或进死信)
    }
}

12. 消费者的可靠性-失败重试 ​

【重点要点】

  • 手动 ack + 无限 requeue 会造成消息死循环:处理失败又回队,再失败再回队,无限占用。
  • 更好方案:消费者重试策略(SpringAMQP 内置)——处理失败自动重试 N 次,重试耗尽后执行失败策略:
    • 默认策略:rejectAndDontRequeueRecoverer(拒绝且不进队,直接丢弃/进死信)。
    • 可配置:spring.rabbitmq.listener.simple.retry.enabled: true、max-attempts(含首次,如 3)、initial-interval(首次重试间隔,如 1000ms)、multiplier(倍率)、max-interval。
  • 重试是本地重试(不在队列里重投,是消费者线程内重试),所以不会给 MQ 造成压力。
  • 重试耗尽后消息去哪:默认丢弃(rejectAndDontRequeue)→ 配置死信交换机(第 13 章)把重试失败的消息送进死信队列,人工排查。
  • 易错点:重试会阻塞消费线程(prefetch 配合考虑);幂等性必须保证(重试可能重复执行,消费端要幂等);multiplier 间隔会指数增长,别配太大。
yaml
spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true          # 开启重试
          max-attempts: 3        # 最多 3 次(含首次)
          initial-interval: 1000 # 首次间隔 1s
          multiplier: 2          # 每次间隔 ×2
        acknowledge-mode: auto   # 配合重试,一般用 auto 即可

13. 消费者的可靠性-死信交换机 ​

【重点要点】

  • 死信(Dead Letter):消息进入死信状态的三种情况:
    1. 消费者拒绝且 requeue=false(basicNack/basicReject 不重回队列);
    2. 消息过期(TTL 到了,见第 14 章);
    3. 队列长度满了(溢出丢弃的最早消息)。
  • 死信交换机(DLX):给队列指定 x-dead-letter-exchange(和可选 x-dead-letter-routing-key),死信消息自动转发到该交换机 → 进死信队列 → 由专门的消费者处理/人工排查。
  • 声明:new Queue(name, durable, exclusive, autoDelete, Map<String,Object> args),args 里放 x-dead-letter-exchange。
  • 案例:订单超时未支付 → 消息 TTL 过期 → 进死信队列 → 死信消费者做"关单"操作。
  • 易错点:死信转发时携带原消息属性(原 routing key 可作为死信路由键);声明死信交换机的队列要在交换机和队列绑定完成之后声明,避免死信投递失败;死信队列要单独建、单独监听。
java
@Configuration
public class DlxConfig {

    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange("dlx.exchange", true, false);   // 死信交换机
    }

    @Bean
    public Queue dlxQueue() {
        return new Queue("dlx.queue", true);                      // 死信队列
    }

    @Bean
    public Binding dlxBinding() {
        return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("dlx.key");
    }

    @Bean
    public Queue businessQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange", "dlx.exchange");       // 绑定死信交换机
        args.put("x-dead-letter-routing-key", "dlx.key");         // 死信路由键(可选)
        return new Queue("business.queue", true, false, false, args);
    }
}

14. 消息幂等性 ​

【重点要点】

  • 为什么必须幂等:消息在链路中可能重复投递/重复消费——生产者 confirm 失败重发、消费者本地重试(第 12 章)、MQ 网络重投、集群切换等都会让同一条消息被消费多次。
  • 幂等的定义:同一条消息消费 N 次与消费 1 次效果完全一致(不产生重复数据、不重复扣款、不重复发券)。
  • 三大方案(按推荐程度):
    1. 唯一业务键 + 数据库唯一约束(最可靠,黑马商城做法):业务表建唯一索引(如 order_no),消费时直接 insert,重复消息触发 DuplicateKeyException → 捕获后跳过。用数据库约束兜底,而不是"先查后插"(先查后插有并发漏洞)。
    2. Redis SETNX 去重标记:消费前 setIfAbsent("业务key", ...),首次返回 true 才处理,重复返回 false 直接跳过;key 加过期时间防堆积。
    3. 状态机 / 版本号校验:处理前检查业务状态(订单已是"已支付"则不再处理);或用乐观锁版本号,update ... where version = ? 影响行数为 0 说明已处理过。
  • 幂等的落脚点:消费端业务入口——发送端、MQ、消费者重试任何一环都可能重投,最终都靠消费端幂等兜底。
  • 易错点:幂等键必须用业务唯一 ID(订单号/用户ID+场景),不能用消息自带随机 ID;去重记录与业务写入要同一个事务(避免业务成功、去重标记没写,下次又处理);setnx 的过期时间要大于处理时长(处理慢导致标记过期会重复执行);错误处理时不要 ack,让重试机制接管(否则手动 ack 会吞掉还没成功的消息)。
java
// 方案1:数据库唯一约束兜底(order_no 建唯一索引)
@RabbitListener(queues = "direct.queue")
public void onOrder(Order order) {
    try {
        orderMapper.insert(order);               // 唯一索引 order_no 冲突则抛异常
    } catch (DuplicateKeyException e) {
        // 重复消息:已处理过,直接忽略
        log.warn("重复消息,已跳过:orderId={}", order.getId());
    }
}
java
// 方案2:Redis SETNX 幂等标记
public boolean tryMark(String businessKey) {
    Boolean first = stringRedisTemplate.opsForValue()
        .setIfAbsent("idempotent:" + businessKey, "1",
                     60, TimeUnit.SECONDS);      // 首次 true,重复 false
    return Boolean.TRUE.equals(first);
}

@RabbitListener(queues = "direct.queue")
public void onOrder(Order order) {
    if (!tryMark("order:" + order.getId())) {
        return;                                  // 已处理过,跳过
    }
    // 业务处理(扣库存/发券等)
}

15. 延迟消息-TTL与死信交换机 ​

【重点要点】

  • 延迟消息场景:订单超时关单(下单后 30 分钟未支付自动取消)、定时提醒等。
  • 思路:消息先不消费,到期后再处理 → 用 TTL(消息存活时间) + 死信交换机 组合实现"延迟队列"。
  • 两种 TTL 设置:
    • 队列 TTL:队列参数 x-message-ttl,队列里所有消息统一过期时间(声明时指定,简单)。
    • 消息 TTL:发送时 MessageProperties.setExpiration("30000"),每条消息单独过期时间。
  • 延迟队列实现:业务队列 设 TTL + 绑死信交换机;消息到期 → 进死信队列 → 死信消费者处理"关单"逻辑。
  • 易错点:队列 TTL 下过期是从队列头开始算(队头消息先过期),队头阻塞时后面的消息即使到期也被队头挡住(RabbitMQ 3.8 前的行为)——精确延迟用每条消息 TTL 或插件(第 15 章);TTL 单位为毫秒。
java
// 声明一个带 TTL + 死信 的延迟队列
@Bean
public Queue delayQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-message-ttl", 30000);                  // 队列 TTL 30 秒
    args.put("x-dead-letter-exchange", "dlx.exchange"); // 到期进死信
    return new Queue("delay.queue", true, false, false, args);
}
java
// 发送带 TTL 的消息(每条单独过期)
Message message = MessageBuilder
        .withBody("订单超时消息".getBytes(StandardCharsets.UTF_8))
        .setExpiration("30000")                         // 消息 TTL 30 秒
        .build();
rabbitTemplate.convertAndSend("delay.exchange", "order.timeout", message);

16. 延迟消息-延迟交换机插件 ​

【重点要点】

  • 队列 TTL 方案的缺点:队头阻塞、延迟精度一般。更专业的方案:延迟消息插件 rabbitmq_delayed_message_exchange。
  • 安装:把插件 .ez 文件放到 RabbitMQ 容器 plugins 目录,rabbitmq-plugins enable rabbitmq_delayed_message_exchange。
  • 用法:声明 x-delayed-message 类型交换机(参数 x-delayed-type: direct),发送时给消息设 x-delay 头(毫秒),交换机延迟指定时间后再路由。
  • SpringAMQP 集成:交换机类型写 x-delayed-message(自定义 CustomExchange),消息加 header x-delay。
  • 优点:每条消息独立延迟、精确、无队头阻塞;适合大量不同延迟时间的消息。
  • 易错点:必须先装插件再声明交换机,否则交换机声明失败;延迟插件交换机的绑定/路由与对应 x-delayed-type 一致(direct/topic);延迟时间从进入交换机开始算。
java
// 声明延迟交换机(x-delayed-message 类型)
@Bean
public CustomExchange delayedExchange() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-delayed-type", "direct");              // 底层按 direct 路由
    return new CustomExchange("delayed.exchange", "x-delayed-message",
            true, false, args);
}

@Bean
public Queue delayedQueue() {
    return new Queue("delayed.queue", true);
}

@Bean
public Binding delayedBinding() {
    return BindingBuilder.bind(delayedQueue())
            .to(delayedExchange()).with("delay.key").noargs();
}
java
// 发送延迟消息:x-delay 头控制延迟毫秒数
Message message = MessageBuilder
        .withBody("10秒后执行".getBytes(StandardCharsets.UTF_8))
        .setHeader("x-delay", 10000)                   // 延迟 10 秒
        .build();
rabbitTemplate.convertAndSend("delayed.exchange", "delay.key", message);

17. 惰性队列 ​

【重点要点】

  • 惰性队列(Lazy Queue):消息直接写入磁盘,而不是先放内存。解决"海量消息堆积撑爆内存"问题。
  • 对比普通队列:普通队列消息在内存,堆积多了内存爆;惰性队列磁盘存储,内存占用稳定。
  • 声明方式:队列参数 x-queue-mode: lazy;或开启全局默认 spring.rabbitmq 侧无此项,需在 Broker 配置(rabbitmq.conf: queue_default_type=quorum 之类的策略)。
  • 适用场景:消息堆积型业务(如消费者宕机、高峰积压、死信积压),内存优先保障。
  • 代价:磁盘读写比内存慢,吞吐略低;消费时从磁盘加载。
  • 易错点:不是所有队列都该设 lazy(正常低积压场景没必要);lazy 与持久化配合(都落盘);RabbitMQ 3.12+ 默认队列类型倾向 quorum(仲裁队列,见第 17 章),lazy 是经典队列的配置。
java
// 声明惰性队列
@Bean
public Queue lazyQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-queue-mode", "lazy");                  // 惰性队列
    return new Queue("lazy.queue", true, false, false, args);
}

18. MQ集群部署 ​

【重点要点】

  • 单机 MQ 是单点:挂了全部消息不可用 → 生产环境部署集群,实现高可用。
  • RabbitMQ 集群两种模式(经典队列):
    • 普通集群(默认):多个节点共享元数据(交换机/队列定义),但消息本体只在所在节点——某个节点挂了,它上面的队列不可用,高可用性有限。
    • 镜像队列(Mirror Queue,旧):消息同步复制到集群内多个节点(副本),主挂自动切换从,高可用更好;缺点是同步开销大。
    • 仲裁队列(Quorum Queue,新推荐):基于 Raft 协议的队列,多个节点投票选主,数据多副本、故障自动恢复——RabbitMQ 3.8+ 官方推荐替代镜像队列。
  • 部署方式:Docker 多节点(--link/同一网络、共享 cookie),管理页看到集群节点;SpringAMQP 客户端配 spring.rabbitmq.addresses: node1:5672,node2:5672(多个地址自动 failover)。
  • 易错点:集群节点必须相同 erlang cookie;普通集群不是高可用(别误以为挂一个节点消息还在);仲裁队列要求队列声明 x-queue-type: quorum;客户端连集群要多地址配置,别只连一个节点。
yaml
spring:
  rabbitmq:
    addresses: 192.168.1.10:5672,192.168.1.11:5672   # 集群多地址
    username: guest
    password: guest
java
// 仲裁队列声明(Raft 高可用)
@Bean
public Queue quorumQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-queue-type", "quorum");               // 仲裁队列
    return new Queue("quorum.queue", true, false, false, args);
}

以「技术栈」为核心的个人知识库