外观
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-management3. 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: guestjava
// 生产者:发送消息到默认交换机,路由键=队列名 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;消费者方法参数直接写对象类型。 - 易错点:
MessageConverterBean 必须唯一,否则启动报错;消费者端类全限定名/字段要一致(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 # 消息无法路由时触发 ReturnsCallbackjava
// 发送 + 确认回调
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):消息进入死信状态的三种情况:
- 消费者拒绝且
requeue=false(basicNack/basicReject 不重回队列); - 消息过期(TTL 到了,见第 14 章);
- 队列长度满了(溢出丢弃的最早消息)。
- 消费者拒绝且
- 死信交换机(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 次效果完全一致(不产生重复数据、不重复扣款、不重复发券)。
- 三大方案(按推荐程度):
- 唯一业务键 + 数据库唯一约束(最可靠,黑马商城做法):业务表建唯一索引(如
order_no),消费时直接 insert,重复消息触发DuplicateKeyException→ 捕获后跳过。用数据库约束兜底,而不是"先查后插"(先查后插有并发漏洞)。 - Redis SETNX 去重标记:消费前
setIfAbsent("业务key", ...),首次返回 true 才处理,重复返回 false 直接跳过;key 加过期时间防堆积。 - 状态机 / 版本号校验:处理前检查业务状态(订单已是"已支付"则不再处理);或用乐观锁版本号,
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 + 绑死信交换机;消息到期 → 进死信队列 → 死信消费者处理"关单"逻辑。 - 易错点:队列 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),消息加 headerx-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: guestjava
// 仲裁队列声明(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);
}