【2025版】2小时候学会MQ消息队列,全是干货

认识RabbitMQ

RabbitMQ的架构如图
在这里插入图片描述
重要概念:

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

1. 安装

我们使用docker来安装RabbitMQ,如果对docker还不熟悉,可以先看这篇文章:
https://blog.youkuaiyun.com/fim77/article/details/143844100(推荐快速入门,后续学习其他知识也会方便很多)

1.1 拉取镜像

docker pull rabbitmq:management
在这里插入图片描述

1.2 运行容器

docker run -d --hostname my-rabbit --name rabbit-mq -p 5672:5672 -p 15672:15672 -e RABBITMQ_DEFAULT_USER=paran -e RABBITMQ_DEFAULT_PASS=123 rabbitmq:management
在这里插入图片描述

1.3 访问RabbitMQ管理端页面

输入上一步创建容器时设置的用户名和密码,登录(如果没设置,默认账号和密码是guest)
在这里插入图片描述

2. 收发消息

2.1 交换机(exchange)

tip:交换机只转发消息,不会保存消息
RabbitMQ默认会初始化一些交换机,交换机(Exchange)是消息队列系统中的一个核心概念,它决定了消息将被发送到哪个或哪些队列。当生产者(Producer)发送消息时,并不是直接将消息发送到队列中,而是先发送给交换机。交换机会根据特定的规则和绑定(Bindings)决定如何处理这些消息,比如是否丢弃、路由到一个或多个队列等。
在这里插入图片描述
交换机的类型有四种:

  1. Fanout:广播,将消息交给所有绑定到交换机的队列。
  2. Direct:订阅,基于RoutingKey(路由key)发送给订阅了消息的队列
  3. Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符
  4. Headers:头匹配,基于MQ的消息头匹配,用的较少。

1) Fanout 交换机

特点:Fanout 交换机会将接收到的消息广播到所有绑定到该交换机的队列。

工作原理:当消息到达 fanout 交换机时,它会直接将该消息复制并发送到所有绑定在这个交换机上的队列,而不管消息的内容、路由键(routing key)是什么。

用途:适用于广播模式,比如广播通知给多个消费者,所有队列都会收到消息。

2) Direct 交换机

特点:Direct 交换机基于精确匹配的路由键将消息发送到队列。

工作原理:当消息到达 direct 交换机时,交换机会根据消息的路由键(routing key)找到与之完全匹配的队列,并将消息传递给该队列。

用途:适用于一对一的路由模式,比如基于某些条件决定将消息发送到特定队列的场景。

示例:你有一个 direct 交换机,并且有多个队列,每个队列绑定一个特定的路由键。比如队列A绑定的路由键是"error",队列B绑定的路由键是"info"。如果你发送一个路由键为"error"的消息,只有队列A会收到该消息。

3) Topic 交换机

特点:Topic 交换机允许更复杂的路由规则,基于路由键的模式匹配来决定消息的传递。

工作原理:Topic 交换机会根据消息的路由键和队列绑定时指定的模式进行匹配。路由键通常由多个单词组成,用点(.)分隔。可以使用通配符(*和#)来匹配路由键中的部分内容。

*(星号):匹配一个词。

#(井号):匹配零个或多个词。

用途:适用于更复杂的消息路由需求,比如基于多个条件选择性地发送消息。

示例

你有一个 topic 交换机和多个队列,每个队列都绑定了不同的模式。比如:

队列A绑定模式"*.error",这意味着它将接收所有路由键包含error的消息(例如"system.error")。

队列B绑定模式"*.info",它将接收所有路由键包含info的消息(例如"system.info")。

队列C绑定模式"system.#",它将接收所有以"system."开头的消息。

当发送一个路由键为"system.error"的消息时,队列A和队列C都会接收到这条消息。

2.2 队列

基本上不需要改什么设置,填个队列名字就可以创建队列了
在这里插入图片描述
创建完之后就可以看到队列的信息总览了
在这里插入图片描述

2.3 绑定关系

这里我们用fanout类型的交换机作为演示,绑定交换机到hello.q1队列
在这里插入图片描述

绑定完之后能查看绑定关系
在这里插入图片描述
在这里插入图片描述

2.4 发消息

在交换机这里可以模拟发消息
在这里插入图片描述
在队列这里可以看到消息已经发过来了,因为这里是管理端,能看到消息,并不是把消息消费了,看了之后消息仍然在队列里面
在这里插入图片描述

3. 数据隔离

假设有两个用户,他们的交换机、队列等数据需要相互隔离,互不影响,这里创建一个新账户做演示
在这里插入图片描述
可以看到新账户是没有virtual hosts的,对于默认账号,是有一个名字为/virtual hosts
在这里插入图片描述
切换刚创建的账号登录,添加一个virtual hosts
在这里插入图片描述
创建好之后可以看到新创建的virtual hosts已经归属到当前账号了
在这里插入图片描述
但是我们仍然能看到其他虚拟主机的信息
在这里插入图片描述
可以在右上角切换虚拟主机,可以只查看指定虚拟主机的内容
在这里插入图片描述

4. SpringAMQP

将来开发编程肯定不是在控制台这样手动操作的,是需要写在代码里面的,手动操作繁琐,容易出错,而且也不方便自动化执行,如果从研发环境转到生产环境,这些配置肯定不可能再手动点一遍配出来,如果能通过执行代码的方式把这些交换机、队列重新自动化的配置好,那肯定是更准确,更方便的。

由于RabbitMQ采用了AMQP协议,因此它具备跨语言的特性。任何语言只要遵循AMQP协议收发消息,都可以与RabbitMQ交互。并且RabbitMQ官方也提供了各种不同语言的客户端。

但是,RabbitMQ官方提供的Java客户端编码相对复杂,一般生产环境下我们更多会结合Spring来使用。而Spring的官方刚好基于RabbitMQ提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配,使用起来非常方便。

4.1 环境配置

创建一个父工程,俩子工程,父工程只有一个pom文件,只用来做依赖管理
子工程一个用来发消息,一个用来接消息
父pom如下


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
         
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.paran.test</groupId>
    <artifactId>parent-project</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version> <!-- 版本根据你的需求进行修改 -->
    </parent>
    
    <name>parent-project</name>
    <url>http://maven.apache.org</url>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

	    <dependency>
	      <groupId>com.fasterxml.jackson.dataformat</groupId>
	      <artifactId>jackson-dataformat-xml</artifactId>
	      <version>2.9.10</version>
	    </dependency>
    
    </dependencies>
    <modules>
        <module>consumer</module>
        <module>publisher</module>
    </modules>
</project>

子工程继承父工程,pom如下,另外一个类似

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.paran.test</groupId>
    <artifactId>parent-project</artifactId>
    <version>1.0-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath> <!-- 指向父POM的相对路径 -->
  </parent>
  <artifactId>consumer</artifactId>

  <packaging>jar</packaging>

  <name>consumer</name>
  <url>http://maven.apache.org</url>
</project>

4.2 消息收发初体验

1) 消息发送

hello.q1是之前演示的时候创建的队列,可以测试一下往这个队列发消息
publisher模块(子工程之一)的配置文件:

spring:
  rabbitmq:
    host: localhost # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: paran # 用户名
    password: 123 # 密码

测试代码

package com.paran.test;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class SpringAmpqTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testPublish() {
        String queueName = "hello.q1";
        String message = "Hello World!";
        rabbitTemplate.convertAndSend(queueName, message);
    }
}

发完之后在RabbitMQ的后台可以看到消息的内容
在这里插入图片描述

2)消息接收


import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class Listener {
    @RabbitListener(queues = "hello.q1")
    public void listenQueueMessage(String msg){
        System.out.println("spring 消费者接收到消息:【" + msg + "】");
    }
}
2025-04-19 15:17:58.762  INFO 5780 --- [           main] com.paran.App                            : Starting App using Java 1.8.0_151 on c42038b38c1bfcd with PID 5780 (E:\projects\java_projects\RabbitMQ\consumer\target\classes started by Paran in E:\projects\java_projects\RabbitMQ)
2025-04-19 15:17:58.763  INFO 5780 --- [           main] com.paran.App                            : No active profile set, falling back to 1 default profile: "default"
2025-04-19 15:17:59.546  INFO 5780 --- [           main] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
2025-04-19 15:17:59.562  INFO 5780 --- [           main] o.s.a.r.c.CachingConnectionFactory       : Created new connection: rabbitConnectionFactory#3e8f7922:0/SimpleConnection@4b4dd216 [delegate=amqp://paran@127.0.0.1:5672/, localPort= 58179]
spring 消费者接收到消息:【hello,778899】
spring 消费者接收到消息:【Hello World!2025-04-19 15:17:59.588  INFO 5780 --- [           main] com.paran.App   

4.3 声明队列和交换机

之前的队列和交换机,以及他们的绑定关系都是在控制台操作的,但是在实际开发时,队列和交换机是程序员定义的,将来项目上线,又要交给运维去创建。那么程序员就需要把程序中运行的所有队列和交换机都写下来,交给运维。在这个过程中是很容易出现错误的。因此推荐的做法是由程序启动时检查队列和交换机是否存在,如果不存在自动创建。

1) 基于@Bean注解

@Configuration
public class FanoutConfig {

    @Bean
    public FanoutExchange fanoutExchange(){
        //return ExchangeBuilder.fanoutExchange("new.fanout").build();
        return new FanoutExchange("new.fanout");
    }

    @Bean
    public Queue fanoutQueue1(){
        //QueueBuilder.durable("fanout.queue1").build();
        return new Queue("fanout.queue1");
    }
    @Bean
    public Binding bindingQueue1(Queue queue,FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);

    }
}

项目启动起来自动创建交换机和队列,项目关闭之后交换机和队列仍然会保留
在这里插入图片描述

在这里插入图片描述
演示一下direct类型的交换机如何绑定队列

public class DirectConfig {
    @Bean
    public DirectExchange directExchange(){
        return ExchangeBuilder.directExchange("new.direct").build();
    }
    
    @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");
    }
}

这里使用.with()来绑定key,每个队列都要绑定一次,甚至一个队列要绑定多个key,这样做太麻烦了,下面介绍另外一种办法

2) 基于@RabbitListener注解(推荐)

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

这样也会自动创建交换机和队列,在单元测试中可以发个消息试一下能不能收到
如果key不是指定的red或者blue是收不到消息的
rabbitTemplate.convertAndSend一般有两个常用的重载
两个参数的:参数一是队列名,参数二是消息
三个参数的:参数一是交换机名,参数二是路由key,参数三是消息
参数二可以写""或者null

@SpringBootTest
public class AppTest
{
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSimpleQueue() {
        String exchangeName= "new.direct";
        String message = "Hello 1234567";
        // 发送消息
        //rabbitTemplate.convertAndSend(queueName, message);
        rabbitTemplate.convertAndSend(exchangeName,"blue",message);
    }
}
2025-04-19 16:27:04.064  INFO 11324 --- [           main] com.paran.App                            : Starting App using Java 1.8.0_151 on c42038b38c1bfcd with PID 11324 (E:\projects\java_projects\RabbitMQ\consumer\target\classes started by Paran in E:\projects\java_projects\RabbitMQ)
2025-04-19 16:27:04.065  INFO 11324 --- [           main] com.paran.App                            : No active profile set, falling back to 1 default profile: "default"
2025-04-19 16:27:04.829  INFO 11324 --- [           main] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
2025-04-19 16:27:04.844  INFO 11324 --- [           main] o.s.a.r.c.CachingConnectionFactory       : Created new connection: rabbitConnectionFactory#2c104774:0/SimpleConnection@2e8ab815 [delegate=amqp://paran@127.0.0.1:5672/, localPort= 50415]
2025-04-19 16:27:04.910  INFO 11324 --- [           main] com.paran.App                            : Started App in 1.032 seconds (JVM running for 1.604)
消费者1接收到direct.queue1的消息:【Hello 123】
消费者1接收到direct.queue1的消息:【Hello 1234567

5. 配置JSON转换器

消息不止可以发送String类型的,也可以发送任意类型的Java对象,在数据传输时,它会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。只不过,默认情况下Spring采用的序列化方式是JDK序列化。JDK的序列化存在很多弊端,所以这里要换一换。

默认情况发送object,在控制台看到的消息格式非常不友好
在这里插入图片描述
publisherconsumer中引入依赖

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.10</version>
</dependency>
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

配置好消息转换器之后,我们就可以直接发个自定义的对象试试了

  @Test
    public void testSimpleQueue() {
        String exchangeName= "new.direct";
        rabbitTemplate.convertAndSend(exchangeName,"blue",Person.builder().age(18).name("paran").build());
    }

在这里插入图片描述
@RabbitListener修饰的方法那里,入参可以写rabbitTemplate.convertAndSend里发送的类Person,也可以用Message(org.springframework.amqp.core.Message)来接,实际上在发送的时候也是先转成Message后发的

如果用Message来接的话,可以用getMessageProperties().getMessageId()来获取MessageId(如果在配置消息转换器的时候设置了jackson2JsonMessageConverter.setCreateMessageIds(true);

如果获取消息体,可以用getBody(),这时候就是我们的Person对象了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值