MQ入门详解之RabbitMQ

一、MQ的基本概念

MQ(Message Queue,消息队列)是一种基于“先进先出”(FIFO)数据结构的跨进程通信机制,用于在分布式系统中传递消息。其核心原理是:生产者将消息发送到队列,消费者从队列中按顺序获取并处理消息,实现应用程序间的异步通信和解耦。


二、MQ的核心作用

  1. 异步处理
    将耗时操作(如发送邮件、短信通知)放入队列异步执行,减少请求响应时间,提高系统吞吐量。
    示例:用户注册后,主流程只需写入数据库即可返回结果,后续通知操作通过MQ异步处理38。

  2. 系统解耦
    通过MQ隔离服务间的直接调用,降低系统耦合性。
    示例:电商系统中,订单服务只需将订单消息发送到MQ,库存、物流等服务各自订阅消息处理,互不影响。

  3. 流量削峰
    在高并发场景(如秒杀)中缓存请求,以稳定速率处理,避免系统过载。
    示例:数据库仅支持每秒2000次写入,MQ可将突发流量缓冲后匀速处理,防止数据库崩溃。

  4. 数据分发
    支持数据在多系统间流转,例如日志采集、缓存同步等场景。


三、MQ的优缺点

优点
  • 提升性能:异步处理减少响应延迟,提高吞吐量。

  • 增强稳定性:解耦后单点故障不影响整体系统。

  • 扩展灵活:新增消费者无需修改生产者代码。

缺点
  1. 系统可用性降低:MQ宕机会导致业务中断,需通过集群(如RocketMQ的分布式架构)保证高可用。

  2. 复杂度增加:需处理消息丢失、重复消费、顺序性等问题。

  3. 一致性问题:若部分消费者处理失败,需通过事务或补偿机制保证数据最终一致性37。


四、常见MQ产品对比

特性RabbitMQKafkaRocketMQActiveMQ
开发语言ErlangScala/JavaJavaJava
吞吐量万级百万级十万级万级
可靠性高(镜像队列)中(异步刷盘)高(同步刷盘)
适用场景企业级应用大数据、日志高并发、事务场景传统JMS应用
特点功能全面高吞吐、低延迟阿里生态支持协议支持丰富

选型建议

  • 高吞吐场景:Kafka(日志分析)、RocketMQ(电商交易)。

  • 高可靠性需求:RabbitMQ(金融业务)。

  • 分布式事务:RocketMQ支持事务消息。

五、适用场景与不适用场景

适用场景
  • 异步任务:非实时操作(如通知、日志)。

  • 高并发缓冲:秒杀、抢购。

  • 跨系统协作:微服务架构下的数据分发。

不适用场景
  • 强一致性需求:如实时扣款(需同步调用)。

  • 简单调用链路:若系统间交互简单,引入MQ可能增加复杂度。


六、RabbitMQ 介绍

6.1基本概念与架构

RabbitMQ 是一个基于 Erlang 语言开发的开源消息代理(Message Broker),遵循 AMQP(高级消息队列协议) 标准,专为分布式系统设计,支持异步通信、系统解耦和负载均衡。其核心组件包括:

  1. Broker:消息代理服务器,负责接收、存储和转发消息。

  2. Virtual Host:虚拟主机,用于逻辑隔离不同租户的资源(如 Exchange、Queue),类似于命名空间。

  3. Exchange:消息路由中心,根据绑定规则(Binding)和路由键(Routing Key)将消息分发到队列。支持多种类型(如 Direct、Topic、Fanout)。

  4. Queue:存储消息的缓冲区,遵循先进先出(FIFO)原则,支持持久化和内存缓存。

  5. Connection 与 Channel:Connection 是生产者和消费者与 Broker 的 TCP 连接,而 Channel 是 Connection 内部的逻辑通道,用于减少频繁建立 TCP 连接的开销。

6.2核心特性

  1. 可靠性
    通过消息持久化(磁盘存储)、传输确认(Producer Confirm)和消费确认(Consumer Ack)机制,确保消息不丢失。

  2. 灵活路由
    支持多种 Exchange 类型和复杂的路由规则。例如:

    • Direct Exchange:精确匹配路由键,用于点对点通信。

    • Topic Exchange:支持通配符(* 和 #),实现多层级路由。

    • Fanout Exchange:广播模式,将消息发送到所有绑定队列。

  3. 高可用与扩展性

    • 集群模式:支持普通集群(元数据同步)和镜像集群(数据主动同步),后者通过 HA 策略实现队列镜像,避免单点故障。

    • 插件机制:提供管理界面、监控工具等插件,支持功能扩展。

  4. 多语言与协议支持
    提供 Java、Python、.NET 等客户端库,并兼容 MQTT、STOMP 等协议36。


6.3部署与运维

  1. 安装方式

    • 单机部署:需先安装 Erlang,再安装 RabbitMQ,并启动管理插件(rabbitmq_management)以启用 Web 界面。

    • Docker 部署:通过镜像快速启动,支持环境变量配置用户和端口映射,例如:

      docker run -d -p 5672:5672 -p 15672:15672 rabbitmq:management
      ```:cite[1]:cite[7]
  2. 集群配置

    • 普通模式:队列元数据同步,但消息仅存储在创建节点,故障时可能丢失数据。

    • 镜像模式:队列数据在多个节点间同步,提供高可用性,但牺牲部分性能。

应用场景
  1. 异步任务处理:将耗时操作(如邮件发送)放入队列,提升系统响应速度。

  2. 系统解耦:分离服务间的直接依赖,例如订单系统与库存系统通过消息队列通信。

  3. 削峰填谷:缓冲突发流量,避免服务过载。

  4. 日志收集:集中处理分布式系统的日志,便于统计分析。


        我们开发业务功能的时候,肯定不会在控制台收发消息,而是应该基于编程的方式。由于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类型的ExchangeDirect相比,都是可以根据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 ConfirmPublisher Return两种。在开启确认机制的情况下,当生产者发送消息给MQ后,MQ会根据消息处理的情况返回不同的回执

具体如图所示:

  • 当消息投递到MQ,但是路由失败时,通过Publisher Return返回异常信息,同时返回ack的确认信息,代表投递成功

  • 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功

  • 持久消息投递到了MQ,并且入队完成持久化,返回ACK ,告知投递成功

  • 其它情况都会返回NACK,告知投递失败

   acknack属于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,发送ackreject,存在业务入侵,但更灵活

  • 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可显著提升系统扩展性和稳定性,成为现代架构的核心组件之一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值