为什么要用 MQ
解耦系统组件
- 系统间依赖关系简化:
在未使用 MQ 时,系统组件间常直接调用,如模块 A 调用模块 B,模块 B 再调用模块 C,会形成紧密依赖关系。一旦某个模块接口变化或出现故障,易影响关联模块。引入 MQ 后,各模块只需与 MQ 交互,发送或接收消息,模块间不再直接依赖。例如电商系统中,订单模块生成订单后将消息发至 MQ,库存模块和物流模块从 MQ 获取消息并各自处理,大大降低了耦合度。 - 便于系统的扩展和维护:
业务发展需新增或修改功能模块时,只需关注该模块与 MQ 的交互逻辑,不用过多考虑对其他模块影响,降低了系统维护和扩展的复杂性,提升了整体灵活性。
异步处理提高性能
- 任务并行化:
一些耗时操作若采用同步方式执行,会让用户长时间等待,影响体验。比如用户注册时,除基本注册信息写入数据库外,还可能需发送欢迎邮件、短信通知等操作。同步执行要等这些额外操作完成才返回注册成功结果,耗时较长。通过 MQ 可将这些操作变为异步,注册模块把相关任务消息发至 MQ 后立即返回注册成功提示,发送邮件、短信的服务从 MQ 获取消息后异步执行,多个任务并行处理,提高了系统整体响应速度。 - 提升系统吞吐量:
系统可快速处理核心业务逻辑,将其他非核心、耗时任务交给 MQ 排队调度,使系统能同时处理更多请求,提升单位时间内业务处理量,增强并发处理能力。
流量削峰填谷
- 应对突发高流量:
电商促销活动(如 “双十一”“618” 等)期间,短时间内大量用户请求涌入系统,若系统按峰值流量直接处理,可能导致服务器资源耗尽、系统崩溃。通过 MQ,可将突发的大量请求先缓冲到队列中,按照系统能承受的处理速度从队列慢慢取消息处理,就像把洪水引入水库再调控放水速度一样,避免系统被高流量冲垮,保障系统稳定性。 - 均衡资源利用:
在低谷期,系统可慢慢处理队列中积压任务,使系统资源在不同时段得到较均衡利用,避免高峰期资源紧张、低谷期闲置浪费。
保障消息可靠传递
- 确保消息不丢失:
MQ 具备多种保障消息可靠传递的机制。例如 RabbitMQ 有消息确认机制,生产者发消息后可等 MQ 的确认回复,知晓消息是否被接收存储;消费者获取消息后也可手动确认,确认后消息才从队列删除,可防止消息因网络故障、系统重启等原因丢失,在对数据准确性要求高的业务场景(如金融交易记录传递)中很重要。 - 支持消息持久化:
MQ 能将消息持久化存储到磁盘等介质上,即便遇到服务器意外断电等情况,恢复后仍可读取之前存储的消息继续处理,进一步提升消息传递可靠性。
交换机的四种类型
Fanout(扇出)类型
- 消息转发规则:
生产者将消息发送到 Fanout 类型的交换机时,该交换机会把接收到的消息广播式地转发到所有与之绑定的队列中,不考虑消息本身的任何内容属性,只要队列绑定了这个交换机,就会收到消息,类似广播电台广播节目,所有收听该电台频率的收音机都能接收声音。 - 应用场景:
适用于需将消息同时通知到多个不同接收方,实现一对多广播消息传递的情况。比如在新闻发布系统中,有新的新闻资讯产生时,通过 Fanout 交换机将消息发送到各个不同的客户端展示队列(如网页端展示队列、移动端展示队列等),让不同客户端都能获取消息进行展示,快速实现消息全面广播。
Direct(直连)类型
- 消息转发规则:
消息发送到 Direct 类型交换机时会带有一个路由键(Routing Key),交换机根据这个路由键精准地将消息转发到与之绑定且路由键完全匹配的队列中,即只有队列绑定的路由键和消息携带的路由键一致时,该队列才能接收到消息,相当于一对一的精准投递。 - 应用场景:
常用于有明确消息分类和对应接收者的场景,使消息能准确到达指定处理模块。例如在电商系统中,订单相关消息可根据不同业务类型(如 “下单”“支付”“退款” 等作为不同路由键)发送到 Direct 交换机,然后不同业务模块对应的队列(下单处理队列、支付处理队列、退款处理队列等)通过绑定相应路由键来获取关心的消息进行处理,确保消息准确流向对应业务逻辑。
Topic(主题)类型
- 消息转发规则:
消息同样带有路由键,不过路由键可由多个单词组成,中间用 “.” 分隔。队列在绑定交换机时可使用通配符(“*” 匹配一个单词,“#” 匹配零个或多个单词)来确定接收哪些路由键对应的消息。这样交换机可根据队列绑定规则及消息路由键情况灵活进行消息转发,比 Direct 类型更具灵活性。 - 应用场景:
在一些业务场景中,消息需按一定主题或规则分类和分发时比较适用。例如在物联网系统中,设备发送的消息路由键可设置为 “device_type.location.status” 格式,不同监控模块对应的队列通过合适通配符绑定(如 “*.livingroom.#” 可接收客厅相关设备的各种状态消息)来获取消息进行分析处理,能满足更复杂的消息匹配和分发需求。
Headers(头信息)类型
- 消息转发规则:
该类型交换机不依赖路由键转发消息,而是基于消息的头信息(Headers)进行匹配转发。生产者发送消息时可添加多个头信息键值对,队列在绑定交换机时设定相应头信息匹配规则(如匹配所有头信息、部分头信息或特定头信息组合等),当消息的头信息满足队列绑定的匹配规则时,交换机就会将消息转发到该队列。不过这种类型相对复杂,使用频率比前三种低。 - 应用场景:
在需要根据消息附带的多种属性、元数据等进行灵活匹配和转发的特殊场景中会用到。例如在复杂的企业级系统中,有多种不同来源、不同优先级、不同格式要求的消息,通过在消息头中设置 “来源部门”“优先级”“消息格式” 等头信息,然后不同业务模块根据自己关注的头信息组合来绑定交换机,从而获取符合要求的消息进行处理。
RabbitMQ 常见的工作模式
简单模式(Simple Mode)
- 模式特点:
由一个生产者(Producer)、一个队列(Queue)和一个消费者(Consumer)组成。生产者将消息发送到队列中,消费者从队列里获取消息并进行处理。是最基础、最简单的消息传递模式,适用于简单的一对一消息传递场景。 - 示例应用场景:
比如在小型的日志收集系统中,某个应用程序作为生产者将产生的日志消息发送到队列,而日志处理的服务作为消费者从队列获取日志并存储到数据库等操作。
工作队列模式(Work Queue Mode)
- 模式特点:
包含一个生产者和多个消费者,生产者将消息发送到同一个队列。多个消费者共同监听该队列,消息会被平均分配给这些消费者(或按能者多劳方式分配,取决于具体配置),实现多个消费者并行处理消息来分担任务负载,常用于需对任务并行处理以提高效率的场景。 - 示例应用场景:
在邮件发送系统中,有大量邮件需发送,生产者将邮件发送任务消息放入队列,多个消费者(邮件发送服务实例)从队列获取任务分别发送邮件,加快邮件发送整体速度。
发布 / 订阅模式(Publish/Subscribe Mode)
- 模式特点:
有一个生产者以及多个消费者,不过这里的消息传递不是直接到队列,而是生产者将消息发送到一个交换机(Exchange),交换机类型为 Fanout(扇出),它会把接收到的消息广播式地转发到所有与之绑定的队列,然后每个队列对应的消费者都能接收到消息,实现一对多的消息广播。 - 示例应用场景:
在新闻发布系统中,新闻内容生产者将新发布的新闻消息发送到交换机,多个不同的客户端(如网页端、移动端等不同展示应用作为消费者)对应的队列都绑定了该交换机,它们都能获取到新闻消息并展示给用户。
路由模式(Routing Mode)
- 模式特点:
同样是生产者将消息发送到交换机,但此时交换机类型为 Direct(直连)。消息在发送时会带有一个路由键(Routing Key),交换机根据这个路由键将消息精准地转发到与之绑定且路由键匹配的队列中,不同的队列可绑定不同的路由键来有选择性地接收消息,实现更灵活的消息定向分发。 - 示例应用场景:
在电商系统中,订单处理相关的消息根据不同业务类型(如下单、支付、退款等作为不同路由键)发送到交换机,然后不同业务模块对应的队列(下单处理队列、支付处理队列、退款处理队列等)通过绑定相应路由键来获取自己关心的消息进行处理。
主题模式(Topic Mode)
- 模式特点:
生产者发送消息到交换机(类型为 Topic),消息的路由键可由多个单词组成,中间用 “.” 分隔,队列在绑定交换机时可使用通配符(如 “*” 匹配一个单词,“#” 匹配零个或多个单词)来确定接收哪些路由键对应的消息,这种模式比路由模式更加灵活,能根据一定规则匹配多种消息情况。 - 示例应用场景:
在物联网设备监控系统中,设备发送的消息路由键可设置为 “device_type.location.status” 格式,不同监控模块对应的队列通过合适通配符绑定(如 “*.livingroom.#” 可接收客厅相关设备的各种状态消息)来获取消息进行分析处理。
如何保证消息的可靠性
一、生产者端可靠性保障
-
问题原因:
- 网络故障:连接 MQ 失败或发送过程中网络中断。
- 路由失败:消息到达 Exchange 后无匹配队列(RoutingKey 错误)或 Exchange 不存在。
- 未持久化:消息未设置持久化,MQ 内存数据丢失。
-
解决措施:
- 生产者重试机制:
- 作用:解决网络瞬时故障导致的连接 / 发送失败。
- 配置(Spring Boot):
- 生产者重试机制:
yaml
spring:
rabbitmq:
connection-timeout: 1s # 连接超时时间
template:
retry:
enabled: true # 开启重试
initial-interval: 1000ms # 初始等待时间
multiplier: 1 # 等待时长倍数(避免阻塞过久)
max-attempts: 3 # 最大重试次数
-
注意:阻塞式重试可能影响性能,高并发场景建议结合异步线程发送消息。
-
生产者确认机制(Publisher Confirm + Return):
- 作用:确保消息到达 MQ 且正确路由。
- Confirm Callback:确认消息是否到达 Exchange(ack/nack)。
- Return Callback:处理路由失败(Exchange 存在但无匹配队列)。
- 配置(Spring Boot):
yaml
spring:
rabbitmq:
publisher-confirm-type: correlated # 异步回调确认
publisher-returns: true # 开启路由失败回调
- 代码示例:
java
// 配置 ReturnCallback(处理路由失败)
rabbitTemplate.setReturnsCallback(returned -> {
log.error("路由失败:Exchange={}, RoutingKey={}, 原因={}",
returned.getExchange(), returned.getRoutingKey(), returned.getReplyText());
// 补偿:发送到备份队列或记录日志
});
// 发送消息时携带 CorrelationData 处理 Confirm
CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
cd.getFuture().addCallback(
result -> {
if (result.isAck()) {
log.info("消息确认成功");
} else {
log.error("消息确认失败:{}", result.getReason());
// 补偿:重试发送或人工干预
}
}
);
rabbitTemplate.convertAndSend("exchange", "routingKey", "msg", cd);
- 消息持久化:
- 作用:确保消息内容在 MQ 重启后不丢失。
- 配置:
- 交换机持久化:
new DirectExchange("exchange", true, false)
(durable=true
)。 - 队列持久化:
QueueBuilder.durable("queue").build()
。 - 消息持久化:
MessageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
(Spring 自动配置)。
- 交换机持久化:
二、MQ 端可靠性保障
-
问题原因:
- 内存存储:默认消息存储在内存,MQ 宕机或重启导致数据丢失。
- 消息堆积:消费者处理慢导致内存溢出,触发
PageOut
阻塞队列。
-
解决措施:
-
数据持久化:
- 三要素:交换机、队列、消息均需持久化(缺一不可)。
- 效果:MQ 重启后,持久化的队列和消息会从磁盘恢复。
-
惰性队列(Lazy Queue):
- 作用:解决大规模消息堆积时的内存压力,直接将消息存磁盘(懒加载)。
- 配置:
- 控制台 / 代码声明队列时添加参数:
x-queue-mode=lazy
。 - Spring AMQP 声明惰性队列代码示例:
- 控制台 / 代码声明队列时添加参数:
-
java
@Bean
public Queue lazyQueue() {
return QueueBuilder.durable("lazy.queue").lazy().build(); // lazy() 开启惰性模式
}
- 适用场景:高吞吐量、允许轻微延迟的业务(如日志、异步通知)。
三、消费者端可靠性保障
-
问题原因:
- 处理中断:消费者接收消息后宕机或处理异常,未确认消息。
- 重复消费:消息未正确确认导致重新投递,引发业务重复执行。
- 无限重试:异常未处理导致消息反复入队,压垮 MQ。
-
解决措施:
- 消费者确认机制(Manual Ack):
- 作用:手动控制消息确认,避免自动确认导致的丢失。
- 配置(Spring Boot):
- 消费者确认机制(Manual Ack):
yaml
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual # 手动确认
- 代码示例:
java
@RabbitListener(queues = "queue", ackMode = "MANUAL")
public void handleMessage(String msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
try {
// 业务处理
channel.basicAck(tag, false); // 单条确认
} catch (Exception e) {
channel.basicNack(tag, false, true); // 失败后重新入队(可配置重试次数)
}
}
- 本地重试机制:
- 作用:避免异常时无限 requeue,在消费者本地重试。
- 配置:
yaml
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启重试
max-attempts: 3 # 最大重试次数
stateless: true # 无状态重试(默认)
-
效果:重试失败后,默认返回
reject
丢弃消息,需结合死信队列处理最终失败。 -
死信队列(DLQ):
- 作用:处理多次重试仍失败的消息,避免无限循环。
- 配置:
- 队列绑定死信交换机代码示例:
java
@Bean
public Queue reliableQueue() {
return QueueBuilder.durable("queue")
.withArgument("x-dead-letter-exchange", "dlx.exchange") // 死信交换机
.withArgument("x-dead-letter-routing-key", "dlq.key") // 死信路由键
.build();
}
- 消费者处理死信队列代码示例:
java
@RabbitListener(queues = "dlq.queue")
public void handleDeadLetter(String msg) {
log.warn("死信消息:{}", msg);
// 补偿:人工处理或记录到数据库
}
- 业务幂等性:
- 作用:避免重复消费导致业务状态不一致(如订单重复支付)。
- 方案:
- 唯一消息 ID:利用
MessageConverter
生成唯一 ID,消费前校验是否已处理。 - 业务状态判断:通过 SQL 条件确保操作幂等(如
UPDATE order SET status=2 WHERE id=? AND status=1
)。
- 唯一消息 ID:利用
四、兜底方案:定时任务主动查询
-
适用场景:
即使上述机制仍有极小概率失败(如 MQ 集群整体故障),需通过业务层兜底。 -
实现逻辑:
- 交易服务定时扫描:定期查询未支付订单,调用支付服务接口确认实际支付状态。
- 状态修正:若支付服务确认已支付,强制更新订单状态为 “已支付”。
- 代码示例:
java
// 示例:定时任务查询订单状态
@Scheduled(cron = "0 0/20 * * *?") // 每20秒执行一次