消息队列基础

RabbitMQ入门

基本介绍

主要概念:

  • publisher:生产者,即为发送消息的程序
  • consumer:消费者,即为消费消息的程序
  • queue:队列,存储消息的缓存区。生产者发送消息暂存在队列中,等待消费者来消费处理
  • exchange:交换机,服务消息的录音。生产者发送的消息到交换机,并根据对应的key决定发送到哪个队列内
  • virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,根据对应的权限来决定可以操作的交换机和队列

image-20250406203121833

安装部署

通过docker来进行mq的安装

docker run \
 -e RABBITMQ_DEFAULT_USER=itheima \
 -e RABBITMQ_DEFAULT_PASS=123321 \
 -v mq-plugins:/plugins \
 --name mq \
 -p 15672:15672 \
 -p 5672:5672 \
 -d \
 rabbitmq:3.8-management

⭕

Tips:需要牢记的知识点

  • 15672:RabbitMQ提供的管理控制台的端口
  • 5672:RabbitMQ为程序内发送和处理消息的端口

依赖引入:由于RabbitMQ采用了AMQP(Advanced Message Queuing Protocol) 协议,官方文档内的Java实现代码较为复杂,在spring的环境中以及集成了一套模板工具SpringAMQP,使用较为方便

<!--AMQP依赖,包含RabbitMQ-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

添加配置文件内容

spring:
  rabbitmq:
    host: 192.168.101.68 # 你的虚拟机IP
    port: 5672 # 发送和处理消息端口
    virtual-host: / # 虚拟主机
    username: itheima # 用户名
    password: 123321 # 密码
接收模式介绍

消息消费者在RabbitMQ中主要有两种接收消息的模式:

方式一、推模式(push):默认采用该模式来接收消息,一旦消费者监听了一个队列后,只有队列内有消息接收到,就会自动推到消费者对应的监听程序内,消费者无需持续访问队列是否有新消息

方式二、拉模式(pull):即消费者主动请求队列来获取消息

在实际使用过程内,推模式居多,但是根据业务场景的不同也可能会使用拉模式来控制获取消息,消费消息的时机;

数据隔离

在实际的开发过程中,为了节约成本,可能会有多个项目同时使用一个RabbitMQ服务器的情况,并且RabbitMQ也是支持多租户的。这个时候就需要避免各个项目直接出现互相干扰的情况,因此对于不同的项目就会设置不同的虚拟主机来解决这种情况

实体步骤:

①、通过默认用户登录控制台,点击Admin选项卡,出现用户管理界面

image-20250406211052592

字段介绍

  • name:即为用户名
  • tags:为用户的角色,通过角色来实现权限的分配,administrator为超级管理员
  • can access virtual host:可以范围的主机范围,/ 是默认的主机

②、点击add a user 来添加新的用户,一般用户名设置为对应的项目名称,权限设置为超级管理员

③、默认创建好的用户的没有任何主机的访问权限的,需要登录新建的该用户进行授权,登录新建的用户点击Virtual hosts菜单,给该用户创建项目单独的virtual host一般为 / 项目名

④、完成授权后即可查看到对应的can access virtual hosts 目录,之后通过此用户创建的队列和交换机都会在对应的虚拟主机目录下,实现数据隔离


RabbitMQ工作模型

工作队列模型(WorkQueues)

工作队列模型是处理消息最简单的一种模型方式,就是将消费者之间绑定到对应的队列,并且为了能够处理大量的消息,可以绑定多个消费者到队列中,从而提高效率

image-20250406220028884

由于绑定了多个消费者,如果没有对消费者接收消息的限制,队列会把收到的消息平均分配给绑定了队列的消费者,但是这样会存在处理能力强的消费者在处理完成全部的消息,但是处理能力弱的消费者由于已经接收了消息了,不管快慢也需要全部处理,这样就无法充分利于每个消费者的能力,为了能够使每个人消费者的处理效率可以得到完全的发挥,就需要添加对消息获取的限制

可以在配置文件内添加如下的配置

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

添加了上述的配置后,由于必须处理完消息后才可以获取下一条消息,这样消费者就会根据自身的处理能力,来处理更多的消息。这样也可以避免处理能力慢导致的消息积压问题。

发布订阅模型

在工作队列模型内,由于消息直接发送到队列中,那么只能由一个消费者获取这个消息,处理完后这个消息就会在队列中消失,很多时候这样的工作模式无法满足业务的要求,比如发送消息方,在发送消息后,同时有两个业务需要接收这个消息同时执行不同的业务场景,这样单一的工作队列模式就无法实现,就需要采用发布订阅模型

image-20250406221438773

在发布订阅模型中,引入了交换机,生产者将消息发送到交换机后,由交换机将消息推送到队列中,由交换机进行消息的转发到队列中,具体的转发规则就需要根据不同的交换机类型来配置

交换机类型

fanout交换机

可以理解为广播类型,会将消息交给所以绑定到交换机的队列

控制台配置步骤

①、创建一个交换机,并将类型选择为fanout

②、创建两个队列,并帮到到上面创建的交换机内

测试

模拟生产者发送消息

@Test
public void testFanoutExchange() {
    // 交换机名称
    String exchangeName = "project.fanout";
    // 消息
    String message = "hello, everyone!";
    rabbitTemplate.convertAndSend(exchangeName, "", message);
}

查看控制台的队列消息接收

image-20250406223518139

模拟不同业务消费消息

@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交换机

在fanout交换机内,由于没有任何条件的限制,所有的绑定的队列上都会收到从交换机转发的消息,但是在部分业务场景下,消息可以指定某些队列来处理,而不是所有队列都处理,者就需要用到Direct交换机

image-20250406224338031

在Direct交换机下:

  • 队列在绑定交换机时,不能直接绑定需要指定一个RoutingKey,并且一个队列可以使用不同的RoutingKey来绑定同一个交换机
  • 消息的发送方在发送消息到交换机时,也需要指定对应的RoutingKey
  • 交换机不会再收到消息后广播所有的队列,而是根据对应的RoutingKey找到绑定了指定该key的队列,转发消息

如上图所示,来进行测试

①、创建一个交换机,指定类型为direct

②、创建两个队列,队列一通过red和blue作为RoutingKey绑定到上述交换机、队列二通过yellow和red作为RoutingKey绑定到上述交换机内

编写监听城西

@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 + "】");
}

场景一:发送key为red的消息到交换机

@Test
public void testSendDirectExchange() {
    // 交换机名称
    String exchangeName = "project.direct";
    // 消息
    String message = "红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "red", message);
}

查看控制台入住输出,可以看到由于两个队列都通过red绑定了交换机,都收到了消息

image-20250406225232275

场景二 :通过blue为key发送消息

@Test
public void testSendDirectExchange() {
    // 交换机名称
    String exchangeName = "project.direct";
    // 消息
    String message = "最新报道,哥斯拉是居民自治巨型气球,虚惊一场!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "blue", message);
}

查看日志只有通过blue为key绑定的队列获取到了消息

image-20250406225424366

场景三:通过yellow为key发送消息(略)


Topic交换机

Topic在Direct交换机的基础上进行了一定的增强,它可以允许在队列通过key绑定交换机的时候使用通配符;

具体的规则如下:

  • 一般key都是一个或者多个单词组成,多个单词可以使用.来进行分割。比如A.B.C
  • 通配符使用规则
    • #: 表示匹配零个或者多个词
    • * :匹配恰好一个词

比如:

  • china.#:表示能够匹配以china开头的所有key
  • china.*:表示只能匹配以china开头,后面只有一个词的key

image-20250406230414507


创建队列和交换机的方式

在实际开发的过程中,使用控制台来直接创建交换机和队列,并绑定队列到交换机,这样的场景是很少的,因为项目在上线后,如果交给运维来在控制台创建上述的内容,很容易出问题,那么久可以选择通过程序代码的方式,在项目启动后自动检查队列与交换机是否存在,如果不存在则会创建需要的队列与交换机,这种做法是强烈推荐的。

基于API的方式(了解即可)
创建队列

SpringAMQP提供了一个Queue类,可以来创建队列

image-20250407002600569

代码实现:首先需要定义一个配置类,在这个类里面来声明bean从而实现创建队列

 // 创建队列
@Bean
public Queue directQueue1() {
    return new Queue("direct.queue1");
}

@Bean
public Queue directQueue2() {
    return QueueBuilder.durable("direct.queue2").build();
}
创建交换机

Spring还提供了一个Exchange接口,可以表示不同类型的交换机,只需要找到对应类型交换机的实现类即可声明

image-20250407003119144

下面以Direct交换机为例子

// 创建交换机
@Bean
public DirectExchange directExchange() {
    // return new DirectExchange("direct.exchange");
    return ExchangeBuilder.directExchange("direct.exchange").build();
}
建立绑定关系

通过BindBuilder来创建Binding对象

image-20250407003452806

具体实现:以之前的Direct为例子

// 建立绑定关系
@Bean
public Binding directQueue1ToDirectExchangeRed(Queue directQueue1, DirectExchange directExchange) {
    return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
}

@Bean
public Binding directQueue1ToDirectExchangeBlue(Queue directQueue1, DirectExchange directExchange) {
    return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
}

@Bean
public Binding directQueue2ToDirectExchangeRed(Queue directQueue2, DirectExchange directExchange) {
    return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
}

@Bean
public Binding directQueue2ToDirectExchangeYellow(Queue directQueue2, DirectExchange directExchange) {
    return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
}

基于注解声明(熟练掌握)

在上述基于api通过@Bean的方式来声明交换机和队列可以发现,对应的代码的编写还是很麻烦的而且臃肿的,因此Spring还提供了基于注解的声明方式

使用的注解就是监听队列的注解@RabbitListener,但是这里声明需要定义对应的bindings属性

下面以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 + "】");
}

image-20250407004651126

查看对应的注解源码可以发现,

  • bindings的值为一个@QueueBinding的集合
  • QueueBinding也存在几个属性
    • value是一个@Queue注解,可以通过它的name属性来声明对应的队列名
    • exchange是一个@Exchange注解,可以通过name属性来声明交换机名称,type属性类声明对应交换机的类型
    • key则是一个数组,来声明交换机和队列之间的RoutingKey

其他类型的声明方式也可以以此类推

posted @ 2025-04-07 00:54  zhangfff  阅读(64)  评论(0)    收藏  举报