深入浅出消息队列 (RabbitMQ)

深入浅出消息队列 (RabbitMQ)

1. 前言

在分布式系统中,消息队列 (Message Queue, MQ) 经常被用来实现异步解耦、流量削峰等功能。它能够让系统在高并发环境下更具弹性,并且在各个模块之间实现“松耦合”的交互。

  • 什么是消息队列?
    消息队列是指在消息传输过程中保存消息的容器,可以帮助不同进程/应用之间异步通信。
  • 你可以把消息队列想象成一个“菜鸟驿站”。就像你网购时,快递先送到驿站,你再去取货一样,消息队列里先存储消息,等消费者来“取走”处理。这样,生产消息的部分(就像快递员)和处理消息的部分(就像取快递的人)就不会因为时间不匹配而互相拖累。简单来说,菜鸟驿站帮助解决快递和取件的时差问题,消息队列则解决了系统中生产和消费的速度差异问题。
  • image-20250314212026844
  • 为什么要使用消息队列?
    • 系统解耦:生产者和消费者不必直接调用。
    • 异步处理:生产者把任务丢到队列即可,后续由消费者异步执行。
    • 流量削峰:将突发的请求平滑化处理,防止服务被压垮。
    • 数据分发:在分布式环境中,能快速地把数据分发给不同的消费者。

2. MQ 的基本概念

  • 异步调用方式其实就是基于消息通知的方式,一般包含三个角色:

    • 消息发送者:投递消息的人,就是原来的调用方
    • 消息Broker:管理、暂存、转发消息,你可以把它理解成微信服务器
    • 消息接收者:接收和处理消息的人,就是原来的服务提供方

    image-20250408094739702

    在异步调用中,发送者不再直接同步调用接收者的业务接口,而是发送一条消息投递给消息Broker。然后接收者根据自己的需求从消息Broker那里订阅消息。每当发送方发送消息后,接受者都能获取消息并处理。

    这样,发送消息的人和接收消息的人就完全解耦了。

    消息Broker,目前常见的实现方案就是消息队列(MessageQueue),简称为MQ.


3. 常见的消息队列实现

RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApache阿里Apache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP,SMTP,STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义协议自定义协议
可用性一般
单机吞吐量一般非常高
消息延迟微秒级毫秒级毫秒级毫秒以内
消息可靠性一般一般

**追求可用性:**Kafka、 RocketMQ 、RabbitMQ

**追求可靠性:**RabbitMQ、RocketMQ

**追求吞吐能力:**RocketMQ、Kafka

**追求消息低延迟:**RabbitMQ、Kafka


4. 消息队列的使用场景

4.1异步处理

image-20250314223318585

如果没有引入MQ进行架构改造,每次支付成功后的大量同步接口调用,耗时大,通过引入MQ,当更新完订单状态和扣减成功库存后,就发一条“支付成功”消息到MQ中,然后立即返回。而信息、积分、优惠券系统会订阅MQ中的该类消息,它们收到通知后就会去异步处理。整个系统的响应时间可以大大缩短,

4.2 削峰/限流

先将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。

image-20250314222512088

像之前黑马点评项目中的异步秒杀下单,判断库存,检验一人一单,只要满足这两个条件就一定可以下单成功,不用等数据真的写进数据库中,可以直接告诉用户下单成功,只需要将订单id等信息引入异步队列记录,后台再开一个线程慢慢去执行队列中的消息就行,有效提高效率。

4.3 解耦

解耦和异步是同生同源的,我们经过上述的异步化改造后,自然而然的就已经将各个系统解耦出去了,可以在多个系统之间进行解耦,将原本通过网络之间的调用的方式改为使用MQ进行消息的异步通讯,只要该操作不是需要同步的,就可以改为使用MQ进行不同系统之间的联系,这样项目之间不会存在耦合,系统之间不会产生太大的影响,就算一个系统挂了,也只是消息挤压在MQ里面没人进行消费而已,不会对其他的系统产生影响 -site:*.youkuaiyun.com

5. 示例:如何简单使用 RabbitMQ

安装与配置

  1. RabbitMQ 官方下载
  2. 安装后可通过管理控制台查看队列状态。

image-20250408101153944

  • publisher:生产者,也就是发送消息的一方
  • consumer:消费者,也就是消费消息的一方
  • queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理
  • exchange:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。
  • virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue
WorkQueues模型

Work queues,任务模型。简单来说就是让多个消费者 绑定到一个队列,共同消费队列中的消息。

image-20250408102511471

当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。

此时就可以使用work 模型,多个消费者共同处理消息处理,消息处理的速度就能大大提高了。

交换机类型

没有交换机,生产者直接发送消息到队列。而一旦引入交换机,消息发送的模式会有很大变化:

image-20250408102756551

可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:

  • Publisher:生产者,不再发送消息到队列中,而是发给交换机
  • Exchange:交换机,一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
  • Queue:消息队列也与以前一样,接收消息、缓存消息。不过队列一定要与交换机绑定。
  • Consumer:消费者,与以前一样,订阅队列,没有变化

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

交换机的类型有四种:

  • Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是Fanout交换机
  • Direct:订阅,基于RoutingKey(路由key)发送给订阅了消息的队列
  • Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符
  • Headers:头匹配,基于MQ的消息头匹配,用的较少。

第一步,配置pom包。

创建Spring Boot项目并在pom.xml文件中添加spring-bootstarter-amqp等相关组件依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

在上面的示例中,引入Spring Boot自带的amqp组件spring-bootstarter-amqp。

SpringAMQP提供了三个功能:

  • 自动声明队列、交换机及其绑定关系
  • 基于注解的监听器模式,异步接收消息
  • 封装了RabbitTemplate工具,用于发送消

第二步,修改配置文件。

修改application.yml配置文件,配置rabbitmq的host地址、端口以及账户信息。

  spring:
    rabbitmq:
      host: localhost
      port: 5672
      username: guest
      password: guest

声明队列和交换机

基于API由程序启动时检查队列和交换机是否存在,如果不存在自动创建

创建一个类,声明队列和交换机

fanout示例
package com.itheima.consumer.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FanoutConfig {
    /**
     * 声明交换机
     * @return Fanout类型交换机
     */
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("hmall.fanout");
    }

    /**
     * 第1个队列
     */
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.queue1");
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    /**
     * 第2个队列
     */
    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }
}
direct示例

direct模式由于要绑定多个KEY,会非常麻烦,每一个Key都要编写一个binding:

package com.itheima.consumer.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DirectConfig {

    /**
     * 声明交换机
     * @return Direct类型交换机
     */
    @Bean
    public DirectExchange directExchange(){
        return ExchangeBuilder.directExchange("hmall.direct").build();
    }

    /**
     * 第1个队列
     */
    @Bean
    public Queue directQueue1(){
        return new Queue("direct.queue1");
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue1WithRed(Queue directQueue1, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
    }
    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue1WithBlue(Queue directQueue1, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
    }

    /**
     * 第2个队列
     */
    @Bean
    public Queue directQueue2(){
        return new Queue("direct.queue2");
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue2WithRed(Queue directQueue2, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
    }
    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding bindingQueue2WithYellow(Queue directQueue2, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
    }
}
基于注解声明

基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明。

例如,我们同样声明Direct模式的交换机和队列:

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "direct.queue1"),
    exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),
    key = {"red", "blue"}
))
public void listenDirectQueue1(String msg){
    System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "direct.queue2"),
    exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),
    key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg){
    System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
}

Topic模式:

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "topic.queue1"),
    exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
    key = "china.#"
))
public void listenTopicQueue1(String msg){
    System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "topic.queue2"),
    exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
    key = "#.news"
))
public void listenTopicQueue2(String msg){
    System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
}

消息转换器

默认转换器

image-20250408110042858

Spring的消息发送代码接收的消息体是一个Object:而在数据传输时,它会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。只不过,默认情况下Spring采用的序列化方式是JDK序列化。

JDK序列化存在下列问题:

  • 数据体积过大
  • 有安全漏洞
  • 可读性差
配置JSON转换器

显然,JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化

启动类中添加一个Bean即可

@Bean
public MessageConverter messageConverter(){
    // 1.定义消息转换器
    Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
    // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
    jackson2JsonMessageConverter.setCreateMessageIds(true);
    return jackson2JsonMessageConverter;
}
消费者接收Object

我们定义一个新的消费者,publisher是用Map发送,那么消费者也一定要用Map接收,格式如下:

@RabbitListener(queues = "object.queue")
public void listenSimpleQueueMessage(Map<String, Object> msg) throws InterruptedException {
    System.out.println("消费者接收到object.queue消息:【" + msg + "】");
}

6.如何在 RabbitMQ 中保证消息不丢失

需要从生产端、MQ 服务端、消费端三个环节分别做好保障,具体如下:

  1. 生产端保障
    • 开启消息确认机制(publisher confirm):生产者发送消息后,RabbitMQ 会在消息成功投递到交换机并路由到队列后,返回一个确认信号(ack);若投递失败(如交换机不存在、路由键错误),则返回否定确认(nack)。生产者可通过监听确认信号,对失败的消息进行重试,避免消息在发送环节丢失。
    • 避免消息在内存中积压:可通过设置合理的重试机制(如有限次数重试 + 死信队列),防止因网络波动等临时问题导致消息发送失败后无法恢复。
  2. MQ 服务端保障
    • 交换机持久化:创建交换机时指定durable=true,确保 MQ 重启后交换机不会丢失,否则消息可能因交换机不存在而投递失败。
    • 队列持久化:创建队列时同样指定durable=true,保证队列在 MQ 重启后依然存在,避免队列丢失导致消息无处存储。
    • 消息持久化:发送消息时设置deliveryMode=2,使消息被持久化到磁盘(默认是内存存储),即使 MQ 宕机,重启后可从磁盘恢复消息。
    • 合理配置刷盘策略:RabbitMQ 默认采用 “异步刷盘”(消息先存内存,定时批量刷盘),若需极致可靠性,可配置为 “同步刷盘”(消息写入即刷盘),但会牺牲部分性能。
  3. 消费端保障
    • 关闭自动 ACK,采用手动 ACK:消费端通过channel.basicConsume指定autoAck=false,在消息完全处理完成后(如业务逻辑执行成功、数据入库等),手动调用channel.basicAck发送确认信号。
    • 避免提前 ACK:若消费端拿到消息后未处理完就崩溃,手动 ACK 机制会让消息保留在队列中,等待其他消费者重新消费;若使用自动 ACK,消息会被立即标记为 “已消费” 并删除,导致未处理的消息丢失。
    • 处理消费异常:若消费过程中出现异常(如业务失败),可通过basicNackbasicReject拒绝消息,并根据需求决定是否将消息重新入队或转发到死信队列,避免消息被误删。

通过以上三个环节的配合,可最大程度确保消息在 RabbitMQ 中不丢失,核心是 “确认机制 + 持久化 + 手动控制消费确认”。

7.遇到 “消息积压” 的问题。比如,当消费者处理速度远低于生产者发送消息的速度时,队列中的消息会越积越多。

需要从 “紧急处理积压” 和 “长期避免积压” 两个层面入手,具体方案如下:

一、紧急处理:快速消化积压消息

  1. 临时扩容消费能力
    • 增加消费者实例:通过水平扩容(部署更多消费端服务),利用 RabbitMQ 的轮询分发机制,让多个消费者并行处理同一队列的消息,直接提升消费速率。
    • 调整消费者预取数:默认情况下,RabbitMQ 会将消息平均分配给消费者,若消息大小 / 处理耗时不均,可通过channel.basicQos(prefetchCount)设置预取数(如prefetchCount=1),确保一个消费者处理完当前消息后再获取下一个,避免 “忙的太忙、闲的太闲”。
    • 简化消费逻辑:临时去掉非核心业务(如日志、统计),只保留必要处理步骤,减少单条消息的处理耗时。
  2. 分流消息,优先处理新消息
    • 若积压严重且新消息持续涌入,可临时创建 “备用队列” 和对应的交换机,通过修改生产者路由策略,将新消息路由到备用队列,由正常消费者处理;原队列的积压消息由专门的 “清理消费者”(逻辑简化,甚至仅做数据归档)慢慢处理,避免新消息被积压阻塞。
    • 处理完积压后,再切换路由策略,让新消息回到原队列,恢复正常流程。
  3. 避免积压期间消息丢失
    • 确保队列和消息已开启持久化(durable=truedeliveryMode=2),即使 MQ 重启,积压消息也能从磁盘恢复。
    • 关闭消费端自动 ACK,采用手动 ACK,避免消费者崩溃导致消息被误删(确保消息处理完成后再发送 ACK)。

二、长期优化:避免再次积压

  1. 生产端限流
    • 若生产者发送速率远超消费能力,可在生产端添加限流机制(如基于令牌桶算法),控制消息发送频率,避免 “生产爆炸式增长”。
    • 结合 MQ 的channel.flow(false)机制,当 MQ 队列长度达到阈值时,通知生产者暂停发送,待队列消息减少后再恢复。
  2. 消费端性能优化
    • 优化业务逻辑:通过异步处理、批量操作(如批量写入数据库)减少单条消息的处理耗时。
    • 引入线程池:在消费端内部使用线程池并行处理消息(需注意线程安全),提升单实例的处理能力。
  3. 监控与告警
    • 实时监控队列长度、消息堆积速率,设置阈值告警(如队列消息数超过 10 万条时触发告警),及时发现并处理积压风险。
    • 定期压测,评估生产 / 消费的峰值能力,提前扩容资源(如在流量高峰期前增加消费者实例)。
  4. 死信队列兜底
    • 为原队列配置死信队列(DLX),若消息处理多次失败(如消费端持续异常),自动路由到死信队列,避免失败消息长期占用队列资源,后续可人工处理死信消息。

核心逻辑:短期通过 “扩容消费 + 分流消息” 快速减负,长期通过 “速率匹配 + 监控优化” 避免积压,同时全程依赖 MQ 自身的持久化和确认机制保障消息不丢失,减少对外部存储的依赖(除非特殊场景)。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KNeeg_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值