RabbitMQ-基础

1. 简介

MQ(Message Queue)消息队列,是基础数据结构中“FIFO(先进先出)”的一种数据结构。

一般用来解决应用解耦异步消息流量削峰等问题,实现高性能,高可用,可伸缩和最终一致性架构。

应用解耦

MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。

异步消息

将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。

流量削峰

如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定卡死了。

这时候我们可以使用MQ将消息保存起来,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会卡死数据库了。

但是使用了MQ之后,限制消费消息的速度为1000,但是这样一来,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000QPS,直到消费完积压的消息,这就叫做“填谷”。

2. RabbitMQ

RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列。

RabbitMQ 其实是一个消息代理:它接受和转发消息。可以将其视为邮局:当你把 要投递的邮件放入邮箱时,你可以确定邮递员最终会将邮件递送给你的收件人。在这个比喻中,RabbitMQ 是一个邮箱、一个邮局和一个信件载体。 RabbitMQ 和邮局之间的主要区别在于它不处理纸张,而是接受、存储和转发二进制数据块 - 消息。

3. 模式

官方网站

这里仅介绍了常用的模式,最近看官网又多个模式Publisher Confirms,完了有时间再补充上。

关于官网中提到的第六种模式RPC,由于RPC通信一般不使用RabbitMQ,所以这里也没有讲。

3.1 简单模式

如图所示:只有一个生产者(P)一个队列(红色块)和 一个消费者(C)。

应用场景:可以实现对应用程序的解耦,并且可以实现对业务的异步处理。事实上这是mq最基本的功能。

3.2 工作模式

如图所示:一个生产者对应多个消费者。多个消费者功能消费一个队列(负载均衡)。

每个消息只能被其中的一个消费者消费。

应用场景:对于 任务过重或任务较多情况使用工作队列可以提高任务处理的速度。

3.3 发布订阅模式

如图所示:在生产者和队列之间多了个交换机(X),此时的交换机类型为:扇形交换机(Fanout Exchange)

事实上,简单模式和工作模式也都有自己的Exchange,只不过不用显性的声明,因为默认使用default Exchange

即:一个发送到Exchange的消息都会被转发到与该交换机绑定的所有队列上。

每一个消息能被多个消费者都消费。

Fanout Exchange消息路由规则如图所示:

应用场景:顾名思义,一个消息想被多个订阅者消费。

3.4 路由模式

如图所示:相比发布订阅模式,Exchange和Queue之间多了个路由关系,此时的交换机类型为:直连交换机(Direct Exchange)

  • 队列和交换机不是任意绑定了,而是要指定一个Routingkey

  • 生产者在向Exchange发送消息时,也必须指定消息的RoutingKey

  • Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息。

Direct Exchange消息路由规则如图所示:

3.5 通配符模式/主题模式

如图所示:相比路由模式,Exchange和Queue之间不只是通过固定的RoutingKey进行绑定,还支持通配符的方式,此时的交换机类型为:主题交换机/通配符交换机(Topic Exchange)

Topic Exchange消息路由规则如图所示:

3. 安装RabbitMQ

version: '2'
services:
    rabbitmq:
       hostname: rabbitmq
       image: rabbitmq:3.8.3-management
       restart: always
       environment:
         # 默认的用户名
         - RABBITMQ_DEFAULT_USER=admin
         # 默认的密码
         - RABBITMQ_DEFAULT_PASS=admin123
       volumes:
         - ./data:/var/lib/rabbitmq
         - ./log:/var/log/rabbitmq/log
       ports:
         # rabbit ui 默认端口
         - "15672:15672"
         # Epmd 是 Erlang Port Mapper Daemon 的缩写,
         # 在 Erlang 集群中相当于 dns 的作用,绑定在4369端口上
         - "4369:4369"
         # rabbit 默认的端口
         - "5672:5672"
         # 25672端口用于节点间和CLI工具通信(Erlang分发服务器端口),
         # 并从动态范围分配(默认情况下仅限于单个端口,
         # 计算方式为AMQP 0-9-1和AMQP 1.0端口+20000),
         # 默认情况下通过 RABBITMQ_NODE_PORT 计算是25672
         - "25672:25672"

4. 各种模式的简单实现

4.1 项目搭建

4.1.1 引入依赖

我们这里使用spring-boot-starter-amqp操作RabbitMQ。

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ldx</groupId>
    <artifactId>rabbitmq</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rabbitmq</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

4.1.2 application.yaml

spring:
  rabbitmq:
    host: localhost
    port: 5672
    # rabbit 默认的虚拟主机
    virtual-host: /
    # rabbitmq 安装时指定的超管信息
    username: admin
    password: admin123

4.2 简单模式

4.2.1 声明一个简单队列

package com.ldx.rabbitmq.config;

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

/**
 * rabbit 快速开始
 *
 * @author ludangxin
 * @date 2021/8/23
 */
@Configuration
public class RabbitSimpleConfig {

    /**
     * 设置一个简单的队列
     */
    @Bean
    public Queue queue() {
        return new Queue("helloMQ");
    }
}

4.2.2 创建生产者

package com.ldx.rabbitmq.producer;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 生产者
 *
 * @author ludangxin
 * @date 2021/8/23
 */
@Component
public class SimpleProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send() {
        String context = "helloMQ " + System.currentTimeMillis();
        rabbitTemplate.convertAndSend("helloMQ", context);
    }
}

4.2.3 创建消费者

package com.ldx.rabbitmq.consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 消费者
 *
 * @author ludangxin
 * @date 2021/8/23
 */
@Slf4j
@Component
@RabbitListener(queues = {"helloMQ"})
public class SimpleConsumer {
  
    @RabbitHandler
    public void process(String hello) {
        log.info("Message:{} ", hello);
    }
  
}

4.2.4 创建测试类

package com.ldx.rabbitmq;

import com.ldx.rabbitmq.producer.SimpleProducer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class RabbitMQTest {

    @Autowired
    private SimpleProducer simpleSender;

    @Test
    public void hello() throws Exception {
        // 每秒发送一条消息
        for (int i = 0; i < 10; i++) {
            simpleSender.send();
            Thread.sleep(1000);
        }
    }
}

4.2.5 启动测试

启动测试类,输出内容如下:

每秒消费一条消息。

2021-09-08 23:58:01.837  INFO 29956 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631116681827 
2021-09-08 23:58:02.839  INFO 29956 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631116682833 
2021-09-08 23:58:03.842  INFO 29956 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631116683838 
2021-09-08 23:58:04.852  INFO 29956 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631116684843 
2021-09-08 23:58:05.853  INFO 29956 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631116685844 
2021-09-08 23:58:06.853  INFO 29956 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631116686847 
2021-09-08 23:58:07.857  INFO 29956 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631116687850 
2021-09-08 23:58:08.863  INFO 29956 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631116688855 
2021-09-08 23:58:09.868  INFO 29956 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631116689858 
2021-09-08 23:58:10.870  INFO 29956 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631116690862 

4.2.6 小节

简单模式,顾名思义,很简单,相当于Hello World程序。我们在编写的时候

  1. 指定了一个Queue并且名称为helloMQ
  2. 消息生产者通过SpringBoot 提供的RabbitTemplate发送消息,我们在发送时指定了QueuehelloMQ且发送了指定内容。
  3. 消息消费者通过@RabbitListener注解监听了指定QueuehelloMQ,且使用@RabbitHandler注解指定消费方法SimpleConsumer::process()
  4. 最后编写测试类循环调用生产者消息发送逻辑,实现了消息的生产与消费。

4.3 工作模式

首先分析:其实工作模式和简单模式相比,仅仅是由一个消费者变成了多个消费者。ok,很好办,我们通过代码再多加一个消费者即可。

4.3.1 添加消费者

package com.ldx.rabbitmq.consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 消费者
 *
 * @author ludangxin
 * @date 2021/8/23
 */
@Slf4j
@Component
@RabbitListener(queues = {"helloMQ"})
public class SimpleConsumer2 {

    @RabbitHandler
    public void process(String hello) {
        log.info("Message2:{} ", hello);
    }

}

4.3.2 启动测试

我们再次执行test方法,查看消息消费情况。

输出日志如下:

SimpleConsumerSimpleConsumer2交替消费队列中的消息(消费者之间消费消息是通过轮询的关系)。

2021-09-09 20:24:35.043  INFO 41927 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631190275019 
2021-09-09 20:24:36.038  INFO 41927 --- [ntContainer#1-1] c.ldx.rabbitmq.consumer.SimpleConsumer2  : Message2:helloMQ 1631190276029 
2021-09-09 20:24:37.036  INFO 41927 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631190277032 
2021-09-09 20:24:38.046  INFO 41927 --- [ntContainer#1-1] c.ldx.rabbitmq.consumer.SimpleConsumer2  : Message2:helloMQ 1631190278036 
2021-09-09 20:24:39.049  INFO 41927 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631190279041 
2021-09-09 20:24:40.049  INFO 41927 --- [ntContainer#1-1] c.ldx.rabbitmq.consumer.SimpleConsumer2  : Message2:helloMQ 1631190280042 
2021-09-09 20:24:41.054  INFO 41927 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631190281047 
2021-09-09 20:24:42.060  INFO 41927 --- [ntContainer#1-1] c.ldx.rabbitmq.consumer.SimpleConsumer2  : Message2:helloMQ 1631190282051 
2021-09-09 20:24:43.062  INFO 41927 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.SimpleConsumer   : Message:helloMQ 1631190283055 
2021-09-09 20:24:44.074  INFO 41927 --- [ntContainer#1-1] c.ldx.rabbitmq.consumer.SimpleConsumer2  : Message2:helloMQ 1631190284057 

4.3.3 小节

在一个队列中如果有多个消费者,那么消费者之间是轮询的关系。

4.4 发布订阅模式

首先分析:发布订阅模式其实是将消息先发送给扇形交换机,交换机再将消息转发给其绑定到此交换机的队列上。

这里,我们声明一个交换机,给交换机绑定两个队列,并且使用两个消费者分别绑定到两个队列上(其实就是为了和3.3保持一致)。

4.4.1 声明交换机和队列

package com.ldx.rabbitmq.config;

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

/**
 * 扇形交换机配置
 *
 * @author ludangxin
 * @date 2021/9/9
 */
@Configuration
public class RabbitFanoutConfig {

    public static final String EXCHANGE_NAME = "FANOUT_EXCHANGE";
    public static final String QUEUE_NAME = "FANOUT_QUEUE";
    public static final String QUEUE_NAME_1 = "FANOUT_QUEUE_1";

    /**
     * 1.交换机
     */
    @Bean(EXCHANGE_NAME)
    public Exchange fanoutExchange() {
        return ExchangeBuilder.fanoutExchange(EXCHANGE_NAME).durable(true).build();
    }

    /**
     * 2.Queue 队列
     */
    @Bean(QUEUE_NAME)
    public Queue fanoutQueue() {
        return QueueBuilder.durable(QUEUE_NAME).build();
    }

    /**
     * 2.1 Queue 队列
     */
    @Bean(QUEUE_NAME_1)
    public Queue fanoutQueue1() {
        return QueueBuilder.durable(QUEUE_NAME_1).build();
    }

    /**
     * 3. 队列和交互机绑定关系 Binding
     */
    @Bean
    public Binding bindFanoutExchange(@Qualifier(QUEUE_NAME) Queue queue, @Qualifier(EXCHANGE_NAME) Exchange exchange) {
        // fanout :routing key 默认为 "",指定了别的值也没用
        return BindingBuilder.bind(queue).to(exchange).with("").noargs();
    }

    /**
     * 3.1 队列和交互机绑定关系 Binding
     */
    @Bean
    public Binding bindFanoutExchange1(@Qualifier(QUEUE_NAME_1) Queue queue, @Qualifier(EXCHANGE_NAME) Exchange exchange) {
        // fanout :routing key 默认为 "",指定了别的值也没用,我们这里随便写个值,看会不会有影响
        return BindingBuilder.bind(queue).to(exchange).with("aaabbb").noargs();
    }
}

4.4.2 创建生产者

package com.ldx.rabbitmq.producer;

import com.ldx.rabbitmq.config.RabbitFanoutConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 扇形交换机消息生产者
 *
 * @author ludangxin
 * @date 2021/9/9
 */
@Component
public class FanoutProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

   public void sendWithFanout() {
        rabbitTemplate.convertAndSend(RabbitFanoutConfig.EXCHANGE_NAME, "", "fanout mq hello~~~");
        // 指定一个routingKey 看消费方能不能正常接收消息
        rabbitTemplate.convertAndSend(RabbitFanoutConfig.EXCHANGE_NAME, "abc", "fanout2 mq hello~~~");
    }
}

4.4.3 创建消费者

package com.ldx.rabbitmq.consumer;

import com.ldx.rabbitmq.config.RabbitFanoutConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 扇形交换机消息消费者
 *
 * @author ludangxin
 * @date 2021/9/9
 */
@Slf4j
@Component
public class FanoutConsumer {

    @RabbitListener(queues = {RabbitFanoutConfig.QUEUE_NAME})
    public void process(String message){
        log.info("queue === " + message);
    }

    @RabbitListener(queues = {RabbitFanoutConfig.QUEUE_NAME_1})
    public void process1(String message){
        log.info("queue1 === " + message);
    }
}

4.4.4 创建测试代码

    @Autowired
    private FanoutProducer producer;

    @Test
    @SneakyThrows
    public void sendWithFanout(){
        producer.sendWithFanout();
        // 为了阻塞进程,使消费者能正常消费。
        System.in.read();
    }

4.4.5 启动测试

执行测试方法,输出内容如下:

生产者发送的两条消息,被两个消费者共同消费了。实现了消息的广播。

2021-09-09 21:59:17.538  INFO 43749 --- [ntContainer#1-1] c.ldx.rabbitmq.consumer.FanoutConsumer   : queue1 === fanout mq hello~~~
2021-09-09 21:59:17.538  INFO 43749 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.FanoutConsumer   : queue === fanout mq hello~~~
2021-09-09 21:59:17.539  INFO 43749 --- [ntContainer#1-1] c.ldx.rabbitmq.consumer.FanoutConsumer   : queue1 === fanout2 mq hello~~~
2021-09-09 21:59:17.539  INFO 43749 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.FanoutConsumer   : queue === fanout2 mq hello~~~

4.4.6 小节

本节代码中我们创建了一个fanout Exchange,并且创建了两个队列与其绑定,其中一个队列进行绑定的时候还指定了routing key,但程序执行时消息正常被消费,说明fanout Exchange不用指定routing key

发布订阅模式与工作队列模式的区别

1、工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机。

2、发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)。

3、发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑定到默认的交换机 。

4.5 路由模式

首先分析:路由模式其实就是将 发布订阅模式中的 fanout Exchange 换成了 direct Exchange 从而指定相应的路由规则即可。

4.5.1 声明交换机和队列

package com.ldx.rabbitmq.config;

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

/**
 * 直连交换机配置
 *
 * @author ludangxin
 * @date 2021/9/9
 */
@Configuration
public class RabbitDirectConfig {

    public static final String EXCHANGE_NAME = "DIRECT_EXCHANGE";
    public static final String QUEUE_NAME_INSERT = "DIRECT_QUEUE_INSERT";
    public static final String QUEUE_NAME_UPDATE = "DIRECT_QUEUE_UPDATE";

    /**
     * 1.交换机
     */
    @Bean(EXCHANGE_NAME)
    public Exchange bootExchange() {
        return ExchangeBuilder.directExchange(EXCHANGE_NAME).durable(true).build();
    }

    /**
     * 2.Queue insert队列
     */
    @Bean(QUEUE_NAME_INSERT)
    public Queue bootQueueInsert() {
        return QueueBuilder.durable(QUEUE_NAME_INSERT).build();
    }

    /**
     * 2.Queue update队列
     */
    @Bean(QUEUE_NAME_UPDATE)
    public Queue bootQueueUpdate() {
        return QueueBuilder.durable(QUEUE_NAME_UPDATE).build();
    }

    /**
     * 3. 绑定insert 队列
     * 3. routing key: insert
     */
    @Bean
    public Binding bindInsertDirectExchange(@Qualifier(QUEUE_NAME_INSERT) Queue queue, @Qualifier(EXCHANGE_NAME) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("insert").noargs();
    }

    /**
     * 3. 绑定update 队列
     * 3. routing key: update
     */
    @Bean
    public Binding bindUpdateDirectExchange(@Qualifier(QUEUE_NAME_UPDATE) Queue queue, @Qualifier(EXCHANGE_NAME) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("update").noargs();
    }
}

4.5.2 创建生产者

package com.ldx.rabbitmq.producer;

import com.ldx.rabbitmq.config.RabbitDirectConfig;
import com.ldx.rabbitmq.config.RabbitFanoutConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 直连交换机消息生产者
 *
 * @author ludangxin
 * @date 2021/9/9
 */
@Component
public class DirectProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

   public void sendWithDirect() {
      rabbitTemplate.convertAndSend(RabbitDirectConfig.EXCHANGE_NAME, "insert", "diect insert mq hello~~~");
      rabbitTemplate.convertAndSend(RabbitDirectConfig.EXCHANGE_NAME, "update", "diect update mq hello~~~");
      // 指定一个没有配置的routingKey 看消费方能不能接收消息
      rabbitTemplate.convertAndSend(RabbitDirectConfig.EXCHANGE_NAME, "delete", "diect update mq hello~~~");
   }
}

4.5.3 创建消息者

package com.ldx.rabbitmq.consumer;

import com.ldx.rabbitmq.config.RabbitDirectConfig;
import com.ldx.rabbitmq.config.RabbitFanoutConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 直连交换机消息消费者
 *
 * @author ludangxin
 * @date 2021/9/9
 */
@Slf4j
@Component
public class DirectConsumer {
		/**
		 * @param message message 为springboot 封装的消息存储的实例对象,其对象中不仅封装了生产者发送的消息
		 *    而且也封装了很多消息的元数据,例如:headers contentType receivedRoutingKey ...
		 */
    @RabbitListener(queues = {RabbitDirectConfig.QUEUE_NAME_INSERT, RabbitDirectConfig.QUEUE_NAME_UPDATE})
    public void directQueue(Message message){
        log.info(message.toString());
        log.info(new String(message.getBody()));
    }

}

4.5.4 创建测试代码

    @Autowired
    private DirectProducer directProducer;

    @Test
    @SneakyThrows
    public void sendWithDirect() {
        directProducer.sendWithDirect();
        System.in.read();
    }

4.5.5 启动测试

执行测试代码,输出内容如下:

insert 和 update 对应的消息都被正常消费,其中值得注意的是指定routing key=delete的消息丢失了,因为队列与交换机绑定时根本没有此routing key,而交换机之所以叫交换机,因为其不存储消息,只是转发消息,其没有持久化消息的能力,所以消息还没有到queue,然后嗝屁。

2021-09-09 22:38:49.451  INFO 44436 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.DirectConsumer   : (Body:'diect insert mq hello~~~' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=DIRECT_EXCHANGE, receivedRoutingKey=insert, deliveryTag=1, consumerTag=amq.ctag-WJmYhQDljkKkM1pFeW99Yg, consumerQueue=DIRECT_QUEUE_INSERT])
2021-09-09 22:38:49.451  INFO 44436 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.DirectConsumer   : diect insert mq hello~~~
2021-09-09 22:38:49.452  INFO 44436 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.DirectConsumer   : (Body:'diect update mq hello~~~' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=DIRECT_EXCHANGE, receivedRoutingKey=update, deliveryTag=2, consumerTag=amq.ctag-guzpfaF0BdII70w85ywiCg, consumerQueue=DIRECT_QUEUE_UPDATE])
2021-09-09 22:38:49.452  INFO 44436 --- [ntContainer#0-1] c.ldx.rabbitmq.consumer.DirectConsumer   : diect update mq hello~~~

4.5.6 小节

路由模式特点:

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个rutingKey(路由key)。
  • 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 routingKey
  • Exchange不再把消息交给每一个绑定的队列,而是根据消息的routing Key进行判断,只有队列的routingkey与消息的 routing key完全一致,才会接收到消息。

4.6 主题模式

首先分析:通配符模式其实就是将 路由模式中的 direct Exchange 换成了 topic Exchange, 使其不仅可以将exchangequeuerouting key全匹配的方式进行绑定,而且还支持通配符

routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

通配符规则:

#:匹配一个或多个单词

*:匹配一个单词

举例:

item.#:能够匹配item.insert.abc 或者 item.insert

item.*:只能匹配item.insert

图解:

  • 红色Queue:绑定的是usa.# ,因此凡是以 usa.开头的routing key 都会被匹配到
  • 黄色Queue:绑定的是#.news ,因此凡是以 .news结尾的 routing key 都会被匹配

4.6.1 声明交换机和队列

package com.ldx.rabbitmq.config;

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

/**
 * 主题交换机配置
 *
 * @author ludangxin
 * @date 2021/9/9
 */
@Configuration
public class RabbitTopicConfig {

    public static final String EXCHANGE_NAME = "TOPIC_EXCHANGE";
    public static final String QUEUE_NAME1 = "TOPIC_QUEUE1";
    public static final String QUEUE_NAME2 = "TOPIC_QUEUE2";

    /**
     * 1.交换机
     * topicExchange:通配符,把消息交给符合routing pattern(路由模式) 的队列
     */
    @Bean(EXCHANGE_NAME)
    public Exchange bootExchange() {
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }

    /**
     * 2.Queue 队列
     */
    @Bean(QUEUE_NAME1)
    public Queue bootQueue1() {
        return QueueBuilder.durable(QUEUE_NAME1).build();
    }

    /**
     * 2.Queue 队列
     */
    @Bean(QUEUE_NAME2)
    public Queue bootQueue2() {
        return QueueBuilder.durable(QUEUE_NAME2).build();
    }

    /**
     * 3. 队列和交互机绑定关系 Binding
     * 匹配 routing key 以 insert 开头的 如 insert.user ; insert.user.log
     */
    @Bean
    public Binding bindTopicExchange1(@Qualifier(QUEUE_NAME1) Queue queue, @Qualifier(EXCHANGE_NAME) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("insert.#").noargs();
    }

    /**
     * 3. 队列和交互机绑定关系 Binding
     * routing key 中的 * 只能匹配单个单词
     * 匹配 routing key 以 update 开头的 如 update.user
     * 不能匹配 如 update.user.log
     */
    @Bean
    public Binding bindTopicExchange2(@Qualifier(QUEUE_NAME1) Queue queue, @Qualifier(EXCHANGE_NAME) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("update.*").noargs();
    }

    /**
     * 3. 队列和交互机绑定关系 Binding
     * routing key 中的 * 只能匹配单个单词
     * 匹配 routing key 以 . 分割的
     * 不能匹配 如 update.user.log
     */
    @Bean
    public Binding bindTopicExchange3(@Qualifier(QUEUE_NAME2) Queue queue, @Qualifier(EXCHANGE_NAME) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("*.*").noargs();
    }
}

4.6.2 创建生产者

package com.ldx.rabbitmq.producer;

import com.ldx.rabbitmq.config.RabbitTopicConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 主题交换机消息生产者
 *
 * @author ludangxin
 * @date 2021/9/9
 */
@Component
public class TopicProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

   public void sendWithTopic() {
      rabbitTemplate.convertAndSend(RabbitTopicConfig.EXCHANGE_NAME, "insert.user.log", "topic mq hello~~~ routing is insert.user.lo");
      rabbitTemplate.convertAndSend(RabbitTopicConfig.EXCHANGE_NAME, "update.user", "topic mq hello~~~ routing is update.user");
      rabbitTemplate.convertAndSend(RabbitTopicConfig.EXCHANGE_NAME, "update.user.log", "topic mq hello~~~ routing is update.user.log");
      rabbitTemplate.convertAndSend(RabbitTopicConfig.EXCHANGE_NAME, "delete.user", "topic mq hello~~~ routing is delete.user");
      rabbitTemplate.convertAndSend(RabbitTopicConfig.EXCHANGE_NAME, "delete.user.log", "topic mq hello~~~routing is delete.user.log");
   }
}

4.6.3 创建消费者

package com.ldx.rabbitmq.consumer;

import com.ldx.rabbitmq.config.RabbitTopicConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 主题交换机消息消费者
 *
 * @author ludangxin
 * @date 2021/9/9
 */
@Slf4j
@Component
public class TopicConsumer {

    @RabbitListener(queues = {RabbitTopicConfig.QUEUE_NAME1, RabbitTopicConfig.QUEUE_NAME2})
    public void topicQueue(Message message){
        log.info(message.toString());
        log.info(new String(message.getBody()));
    }
}

4.6.4 创建测试代码

@Autowired
private TopicProducer topicProducer;

@Test
@SneakyThrows
public void sendWithTopic() {
    topicProducer.sendWithTopic();
    System.in.read();
}

4.6.5 启动测试

执行测试代码,输入内容如下:

其中符合通配符条件的消息均已消费。

2021-09-09 23:02:57.131  INFO 44812 --- [ntContainer#5-1] com.ldx.rabbitmq.consumer.TopicConsumer  : (Body:'topic mq hello~~~ routing is insert.user.lo' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=TOPIC_EXCHANGE, receivedRoutingKey=insert.user.log, deliveryTag=1, consumerTag=amq.ctag-PeBOPjJFHMF3BMW1zDXCvw, consumerQueue=TOPIC_QUEUE1])
2021-09-09 23:02:57.132  INFO 44812 --- [ntContainer#5-1] com.ldx.rabbitmq.consumer.TopicConsumer  : topic mq hello~~~ routing is insert.user.lo
2021-09-09 23:02:57.132  INFO 44812 --- [ntContainer#5-1] com.ldx.rabbitmq.consumer.TopicConsumer  : (Body:'topic mq hello~~~ routing is update.user' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=TOPIC_EXCHANGE, receivedRoutingKey=update.user, deliveryTag=2, consumerTag=amq.ctag-PeBOPjJFHMF3BMW1zDXCvw, consumerQueue=TOPIC_QUEUE1])
2021-09-09 23:02:57.132  INFO 44812 --- [ntContainer#5-1] com.ldx.rabbitmq.consumer.TopicConsumer  : topic mq hello~~~ routing is update.user
2021-09-09 23:02:57.133  INFO 44812 --- [ntContainer#5-1] com.ldx.rabbitmq.consumer.TopicConsumer  : (Body:'topic mq hello~~~ routing is update.user' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=TOPIC_EXCHANGE, receivedRoutingKey=update.user, deliveryTag=3, consumerTag=amq.ctag-ocNDmCGDF8-aPxJ4lK1c8g, consumerQueue=TOPIC_QUEUE2])
2021-09-09 23:02:57.133  INFO 44812 --- [ntContainer#5-1] com.ldx.rabbitmq.consumer.TopicConsumer  : topic mq hello~~~ routing is update.user
2021-09-09 23:02:57.133  INFO 44812 --- [ntContainer#5-1] com.ldx.rabbitmq.consumer.TopicConsumer  : (Body:'topic mq hello~~~ routing is delete.user' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=TOPIC_EXCHANGE, receivedRoutingKey=delete.user, deliveryTag=4, consumerTag=amq.ctag-ocNDmCGDF8-aPxJ4lK1c8g, consumerQueue=TOPIC_QUEUE2])
2021-09-09 23:02:57.133  INFO 44812 --- [ntContainer#5-1] com.ldx.rabbitmq.consumer.TopicConsumer  : topic mq hello~~~ routing is delete.user

4.6.6 小节

Topic主题模式可以实现 Publish/Subscribe发布与订阅模式 Routing路由模式 的功能;只是Topic在配置routing key 的时候可以使用通配符,显得更加灵活。

5. 模式总结

RabbitMQ工作模式:
1、简单模式 HelloWorld
一个生产者、一个消费者,不需要设置交换机(使用default Exchange)。

2、工作队列模式 Work Queue
一个生产者、多个消费者(平均分配消息),不需要设置交换机(使用default Exchange)。

3、发布订阅模式 Publish/subscribe
需要设置交换机类型为fanout Exchange,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列。

4、路由模式 Routing
需要设置交换机类型为direct Exchange,交换机和队列进行绑定,并且指定routing key,发送消息时也要指定对应的routing key到交换机,交换机会根据routing key将消息发送到对应的队列。

5、主题模式 Topic
需要设置交换机类型为topic Exchange,,交换机和队列进行绑定,并且指定通配符方式的routing key,发送消息时指定routing key到交换机后,交换机会根据routing key规则将消息发送到对应的队列。主题模式比上面四类更灵活。

posted @ 2021-09-09 23:28  张铁牛  阅读(185)  评论(0编辑  收藏  举报