一、MQ的基本概念
MQ(Message Queue,消息队列)是一种基于“先进先出”(FIFO)数据结构的跨进程通信机制,用于在分布式系统中传递消息。其核心原理是:生产者将消息发送到队列,消费者从队列中按顺序获取并处理消息,实现应用程序间的异步通信和解耦。
二、MQ的核心作用
-
异步处理
将耗时操作(如发送邮件、短信通知)放入队列异步执行,减少请求响应时间,提高系统吞吐量。
示例:用户注册后,主流程只需写入数据库即可返回结果,后续通知操作通过MQ异步处理38。 -
系统解耦
通过MQ隔离服务间的直接调用,降低系统耦合性。
示例:电商系统中,订单服务只需将订单消息发送到MQ,库存、物流等服务各自订阅消息处理,互不影响。 -
流量削峰
在高并发场景(如秒杀)中缓存请求,以稳定速率处理,避免系统过载。
示例:数据库仅支持每秒2000次写入,MQ可将突发流量缓冲后匀速处理,防止数据库崩溃。 -
数据分发
支持数据在多系统间流转,例如日志采集、缓存同步等场景。
三、MQ的优缺点
优点
-
提升性能:异步处理减少响应延迟,提高吞吐量。
-
增强稳定性:解耦后单点故障不影响整体系统。
-
扩展灵活:新增消费者无需修改生产者代码。
缺点
-
系统可用性降低:MQ宕机会导致业务中断,需通过集群(如RocketMQ的分布式架构)保证高可用。
-
复杂度增加:需处理消息丢失、重复消费、顺序性等问题。
-
一致性问题:若部分消费者处理失败,需通过事务或补偿机制保证数据最终一致性37。
四、常见MQ产品对比
特性 | RabbitMQ | Kafka | RocketMQ | ActiveMQ |
---|---|---|---|---|
开发语言 | Erlang | Scala/Java | Java | Java |
吞吐量 | 万级 | 百万级 | 十万级 | 万级 |
可靠性 | 高(镜像队列) | 中(异步刷盘) | 高(同步刷盘) | 中 |
适用场景 | 企业级应用 | 大数据、日志 | 高并发、事务场景 | 传统JMS应用 |
特点 | 功能全面 | 高吞吐、低延迟 | 阿里生态支持 | 协议支持丰富 |
选型建议:
-
高吞吐场景:Kafka(日志分析)、RocketMQ(电商交易)。
-
高可靠性需求:RabbitMQ(金融业务)。
-
分布式事务:RocketMQ支持事务消息。
五、适用场景与不适用场景
适用场景
-
异步任务:非实时操作(如通知、日志)。
-
高并发缓冲:秒杀、抢购。
-
跨系统协作:微服务架构下的数据分发。
不适用场景
-
强一致性需求:如实时扣款(需同步调用)。
-
简单调用链路:若系统间交互简单,引入MQ可能增加复杂度。
六、RabbitMQ 介绍
6.1基本概念与架构
RabbitMQ 是一个基于 Erlang 语言开发的开源消息代理(Message Broker),遵循 AMQP(高级消息队列协议) 标准,专为分布式系统设计,支持异步通信、系统解耦和负载均衡。其核心组件包括:
-
Broker:消息代理服务器,负责接收、存储和转发消息。
-
Virtual Host:虚拟主机,用于逻辑隔离不同租户的资源(如 Exchange、Queue),类似于命名空间。
-
Exchange:消息路由中心,根据绑定规则(Binding)和路由键(Routing Key)将消息分发到队列。支持多种类型(如 Direct、Topic、Fanout)。
-
Queue:存储消息的缓冲区,遵循先进先出(FIFO)原则,支持持久化和内存缓存。
-
Connection 与 Channel:Connection 是生产者和消费者与 Broker 的 TCP 连接,而 Channel 是 Connection 内部的逻辑通道,用于减少频繁建立 TCP 连接的开销。
6.2核心特性
-
可靠性
通过消息持久化(磁盘存储)、传输确认(Producer Confirm)和消费确认(Consumer Ack)机制,确保消息不丢失。 -
灵活路由
支持多种 Exchange 类型和复杂的路由规则。例如:-
Direct Exchange:精确匹配路由键,用于点对点通信。
-
Topic Exchange:支持通配符(
*
和#
),实现多层级路由。 -
Fanout Exchange:广播模式,将消息发送到所有绑定队列。
-
-
高可用与扩展性
-
集群模式:支持普通集群(元数据同步)和镜像集群(数据主动同步),后者通过 HA 策略实现队列镜像,避免单点故障。
-
插件机制:提供管理界面、监控工具等插件,支持功能扩展。
-
-
多语言与协议支持
提供 Java、Python、.NET 等客户端库,并兼容 MQTT、STOMP 等协议36。
6.3部署与运维
-
安装方式
-
单机部署:需先安装 Erlang,再安装 RabbitMQ,并启动管理插件(
rabbitmq_management
)以启用 Web 界面。 -
Docker 部署:通过镜像快速启动,支持环境变量配置用户和端口映射,例如:
docker run -d -p 5672:5672 -p 15672:15672 rabbitmq:management ```:cite[1]:cite[7]
-
-
集群配置
-
普通模式:队列元数据同步,但消息仅存储在创建节点,故障时可能丢失数据。
-
镜像模式:队列数据在多个节点间同步,提供高可用性,但牺牲部分性能。
-
应用场景
-
异步任务处理:将耗时操作(如邮件发送)放入队列,提升系统响应速度。
-
系统解耦:分离服务间的直接依赖,例如订单系统与库存系统通过消息队列通信。
-
削峰填谷:缓冲突发流量,避免服务过载。
-
日志收集:集中处理分布式系统的日志,便于统计分析。
我们开发业务功能的时候,肯定不会在控制台收发消息,而是应该基于编程的方式。由于RabbitMQ
采用了AMQP协议,因此它具备跨语言的特性。任何语言只要遵循AMQP协议收发消息,都可以与RabbitMQ
交互。并且RabbitMQ
官方也提供了各种不同语言的客户端。
但是,RabbitMQ官方提供的Java客户端编码相对复杂,一般生产环境下我们更多会结合Spring来使用。而Spring的官方刚好基于RabbitMQ提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配,使用起来非常方便。
引入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置yaml文件
spring:
rabbitmq:
host: 192.168.150.101 # 你的虚拟机IP
port: 5672 # 端口,根据你的docker启动时的配置决定
virtual-host: /hmall # 虚拟主机
username: hmall # 用户名
password: 123 # 密码
注意:收发双方都需要对应的配置
6.4队列与交换机的生成和绑定
队列与交换机生成绑定可以基于图形界面和编码两种方式,在实际开发时,队列和交换机是程序员定义的,将来项目上线,又要交给运维去创建。那么程序员就需要把程序中运行的所有队列和交换机都写下来,交给运维。在这个过程中是很容易出现错误的。
因此推荐的做法是由程序启动时检查队列和交换机是否存在,如果不存在自动创建。
6.5基于SpringAMQP消息三种的收发
一、fanout收发
-
1) 可以有多个队列
-
2) 每个队列都要绑定到Exchange(交换机)
-
3) 生产者发送的消息,只能发送到交换机
-
4) 交换机把消息发送给绑定过的所有队列
-
5) 订阅队列的消费者都能拿到消息
消息发出:
@Test
public void testFanoutExchange() {
// 交换机名称
String exchangeName = "hmall.fanout";
// 消息
String message = "hello, everyone!";
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
消息接收:
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
System.out.println("消费者1接收到Fanout消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
System.out.println("消费者2接收到Fanout消息:【" + msg + "】");
}
二、direct收发
在Direct模型下:
-
队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key) -
消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。 -
Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key
进行判断,只有队列的Routingkey
与消息的Routing key
完全一致,才会接收到消息
消息发出:
@Test
public void testSendDirectExchange() {
// 交换机名称
String exchangeName = "hmall.direct";
// 消息
String message = "红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";
// 发送消息,指定的key为blue
rabbitTemplate.convertAndSend(exchangeName, "blue", message);
}
消息接收:
@RabbitListener(queues = "direct.queue1")
public void listenDirectQueue1(String msg) {
System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "direct.queue2")
public void listenDirectQueue2(String msg) {
System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
}
由于只有direct.queue1的key绑定了blue所以只有direct.queue1可以收到消息
Direct交换机与Fanout交换机的差异?
-
Fanout交换机将消息路由给每一个与之绑定的队列
-
Direct交换机根据RoutingKey判断路由给哪个队列
-
如果多个队列具有相同的RoutingKey,则与Fanout功能类似
三、topic收发
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定BindingKey
的时候使用通配符!
通配符规则:
-
#
:匹配0个或多个词 -
*
:匹配不多不少恰好1个词
队列绑定:
消息发出:
/**
* topicExchange
*/
@Test
public void testSendTopicExchange() {
// 交换机名称
String exchangeName = "hmall.topic";
// 消息
String message = "喜报!孙悟空大战哥斯拉,胜!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}
消息接收:
@RabbitListener(queues = "topic.queue1")
public void listenTopicQueue1(String msg){
System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "topic.queue2")
public void listenTopicQueue2(String msg){
System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
}
两者都可以接收到消息
Direct交换机与Topic交换机的差异?
-
Topic交换机接收的消息RoutingKey必须是多个单词,以
.
分割 -
Topic交换机与队列绑定时的bindingKey可以指定通配符
-
#
:代表0个或多个词 -
*
:代表1个词
6.6MQ消息的可靠性
消息从发送者发送消息,到消费者处理消息,需要经过的流程是这样的:
消息从生产者到消费者的每一步都可能导致消息丢失:
-
发送消息时丢失:
-
生产者发送消息时连接MQ失败
-
生产者发送消息到达MQ后未找到
Exchange
-
生产者发送消息到达MQ的
Exchange
后,未找到合适的Queue
-
消息到达MQ后,处理消息的进程发生异常
-
-
MQ导致消息丢失:
-
消息到达MQ,保存到队列后,尚未消费就突然宕机
-
-
消费者处理消息时:
-
消息接收后尚未处理突然宕机
-
消息接收后处理过程中抛出异常
-
综上,我们要解决消息丢失问题,保证MQ的可靠性,就必须从3个方面入手:
-
确保生产者一定把消息发送到MQ
-
确保MQ不会将消息弄丢
-
确保消费者一定要处理消息
一、发送者确认
RabbitMQ提供了生产者消息确认机制,包括Publisher Confirm
和Publisher Return
两种。在开启确认机制的情况下,当生产者发送消息给MQ后,MQ会根据消息处理的情况返回不同的回执。
具体如图所示:
-
当消息投递到MQ,但是路由失败时,通过Publisher Return返回异常信息,同时返回ack的确认信息,代表投递成功
-
临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
-
持久消息投递到了MQ,并且入队完成持久化,返回ACK ,告知投递成功
-
其它情况都会返回NACK,告知投递失败
ack
和nack
属于Publisher Confirm机制,ack
是投递成功;nack
是投递失败。而return
则属于Publisher Return机制,默认两种机制都是关闭状态,需要通过配置文件来开启。
二、MQ持久化
消息到达MQ以后,如果MQ不能及时保存,也会导致消息丢失,所以MQ的可靠性也非常重要。
为了提升性能,默认情况下MQ的数据都是在内存存储的临时数据,重启后就会消失。为了保证数据的可靠性,必须配置数据持久化,包括:
-
交换机持久化
-
队列持久化
-
消息持久化
Durability
参数:设置为Durable
就是持久化模式,Transient
就是临时模式。
注意:在开启持久化机制以后,如果同时还开启了生产者确认,那么MQ会在消息持久化以后才发送ACK回执,进一步确保消息的可靠性。
不过出于性能考虑,为了减少IO次数,发送到MQ的消息并不是逐条持久化到数据库的,而是每隔一段时间批量持久化。一般间隔在100毫秒左右,这就会导致ACK有一定的延迟,因此建议生产者确认全部采用异步方式。
三、消费者确认
当RabbitMQ向消费者投递消息以后,需要知道消费者的处理状态如何。因为消息投递给消费者并不代表就一定被正确消费了,可能出现的故障有很多,比如:
-
消息投递的过程中出现了网络故障
-
消费者接收到消息后突然宕机
-
消费者接收到消息后,因处理不当导致异常
-
...
一旦发生上述情况,消息也会丢失。因此,RabbitMQ必须知道消费者的处理状态,一旦消息处理失败才能重新投递消息。
当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:
-
ack:成功处理消息,RabbitMQ从队列中删除该消息
-
nack:消息处理失败,RabbitMQ需要再次投递消息
-
reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息
消息回执的处理代码比较统一,因此SpringAMQP实现了消息确认。并允许我们通过配置文件设置ACK处理方式,有三种模式:
-
none
:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用 -
manual
:手动模式。需要自己在业务代码中调用api,发送ack
或reject
,存在业务入侵,但更灵活 -
auto
:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack
. 当业务出现异常时,根据异常判断返回不同结果:-
如果是业务异常,会自动返回
nack
; -
如果是消息处理或校验异常,自动返回
reject
;
-
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: none # 不做处理
失败重试机制
当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者。如果消费者再次执行依然出错,消息会再次requeue到队列,再次投递,直到消息处理成功为止。
极端情况就是消费者一直无法执行成功,那么消息requeue就会无限循环,导致mq的消息处理飙升,带来不必要的压力。
对此,我们修改消费者服务的application.yml文件,添加内容:
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初识的失败等待时长为1秒
multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
-
开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试
-
重试达到最大次数后,Spring会返回reject,消息会被丢弃
此外,本地测试达到最大重试次数后,消息会被丢弃。这在某些对于消息可靠性要求较高的业务场景下,不太合适,Spring允许我们自定义重试次数耗尽后的消息处理策略,这个策略是由MessageRecovery
接口来定义的,它有3个不同实现:
-
RejectAndDontRequeueRecoverer
:重试耗尽后,直接reject
,丢弃消息。默认就是这种方式 -
ImmediateRequeueMessageRecoverer
:重试耗尽后,返回nack
,消息重新入队 -
RepublishMessageRecoverer
:重试耗尽后,将失败消息投递到指定的交换机
比较合理的一种处理方案是RepublishMessageRecoverer
,失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。
四、兜底处理
虽然我们利用各种机制尽可能增加了消息的可靠性,但也不好说能保证消息100%的可靠。万一真的MQ通知失败该怎么办呢?
通常,在消息队列(MQ)的使用中,为了保证消息的可靠性,延迟消息可以作为一种有效的兜底方案。
原理:
正常的消息发送后会立即进入队列等待消费,而延迟消息则是在发送后不会马上进入可消费状态,而是根据设定的延迟时间,在到达指定时间后才会被投递到消费端。当我们担心消息在正常发送和处理过程中可能出现丢失、未被正确处理等情况时,就可以利用延迟消息来进行补偿。比如,先正常发送业务消息,若在一定时间内没有收到业务处理成功的反馈,就发送一条延迟消息。延迟消息到达后,再次检查业务状态,如果仍未处理成功,则可以进行重试处理等操作,以此来保证消息最终被正确处理,实现消息的可靠性。
RabbitMQ:本身不直接支持延迟消息,需要借助插件(如 rabbitmq-delayed-message-exchange
)来实现。该插件会创建一个特殊的交换器(Exchange),当消息发送到这个交换器时,可以设置消息的延迟时间。交换器会根据延迟时间将消息暂存,到达时间后再将消息路由到对应的队列中供消费者消费。
官方文档说明:
https://blog.rabbitmq.com/posts/2015/04/scheduling-messages-with-rabbitmq
插件下载地址:
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
下载时注意与自己使用的RabbitMQ版本相对应。
五、延迟消息的收发
一、消息发送
发送消息时,必须通过x-delay属性设定延迟时间
@Test
void testPublisherDelayMessage() {
// 1.创建消息
String message = "hello, delayed message";
// 2.发送消息,利用消息后置处理器添加消息头
rabbitTemplate.convertAndSend("delay.direct", "delay", message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 添加延迟消息属性
message.getMessageProperties().setDelay(5000);
return message;
}
});
}
二、消息接收
基于注解方式(推荐):
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue", durable = "true"),
exchange = @Exchange(name = "delay.direct", delayed = "true"),
key = "delay"
))
public void listenDelayMessage(String msg){
log.info("接收到delay.queue的延迟消息:{}", msg);
}
基于@Bean
的方式
package com.itheima.consumer.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class DelayExchangeConfig {
@Bean
public DirectExchange delayExchange(){
return ExchangeBuilder
.directExchange("delay.direct") // 指定交换机类型和名称
.delayed() // 设置delay的属性为true
.durable(true) // 持久化
.build();
}
@Bean
public Queue delayedQueue(){
return new Queue("delay.queue");
}
@Bean
public Binding delayQueueBinding(){
return BindingBuilder.bind(delayedQueue()).to(delayExchange()).with("delay");
}
}
注意:
延迟消息插件内部会维护一个本地数据库表,同时使用Elang Timers功能实现计时。如果消息的延迟时间设置较长,可能会导致堆积的延迟消息非常多,会带来较大的CPU开销,同时延迟消息的时间会存在误差。
因此,不建议设置延迟时间过长的延迟消息。
七、总结
MQ通过异步、解耦和削峰能力优化分布式系统性能,但需权衡其引入的复杂性和运维成本。选型时需结合业务需求(如吞吐量、可靠性),并针对消息丢失、重复消费等问题设计容错机制。合理使用MQ可显著提升系统扩展性和稳定性,成为现代架构的核心组件之一。