MQ:RabbitMQ

同步和异步通讯

同步通讯:

需要实时响应,时效性强

耦合度高

每次增加功能都要修改两边的代码

性能下降

需要等待服务提供者的响应,如果调用链过长则每次响应时间需要等待所有调用完成

资源浪费

调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源

级联失败

如果一个服务的提供者出现了问题,所有调用方都会出问题,出现雪崩问题

异步通讯:

在发布方和接收方之间存在中间人(Broker)

发布方只需将消息发布到中间人

接受方只需要从中间人订阅消息

实现解耦

吞吐量提升

无需等待订阅者处理完成,响应快速

故障隔离

服务不存在直接调用,不存在级联失败问题

资源占用问题

调用之间不会阻塞,不会产生无效资源占用问题

解耦

每个服务之间灵活插拔,实现解耦

流量削峰

所有发布事件由Broker直接接受,接受者按照自己的速度从Broker处理事件,实现缓冲

MQ

MessageQueue消息队列

即上述过程中的Broker

几种常见的MQ对比

MQ的基本结构

publisher:发布者

consumer:消费者

exchange:交换机,负责消息路由

queue:队列,存储消息,消息的缓冲区

队列绑定交换机

virtualHost:虚拟主机

channel:表示通道,操作MQ的工具,连接消息发布者和交换机,连接消息接受者和队列

RabbitMQ整体工作流程

发布者发布消息给交换机

交换机将消息路由到与其绑定的队列

消费者监听与其对应的队列获取消息

RabbitMQ消息模型

生产者->(交换机)->队列->消费者
基本消息队列BasicQueue
工作消息队列WorkQueue

多个消费者并发消费队列,消费者之间是竞争关系

发布订阅(Publish,Subscribe)

根据交换机类型不同分为三种

广播

消费者各自拥有

生产者将消息发送到交换机,具体发给哪个队列,生产者无法决定,由交换机决定.

交换机把消息发送给绑定过的所有队列,队列的所有消费者都能拿到消息.实现一条消息被多个消费者消费

生产者->所有消费者

路由

需求不同的消息被不同队列消费,就需要用到Direct类型的Exchange.在Direct模型下,需要指定RoutingKey(路由key).在消息发送方向交换机发送消息时,必须指定消息的路由key

交换机在接受到生产者消息后,将消息递交给routingkey完全匹配的队列

主题

topic类型相当于可以使用通配符匹配routingkey的路由类型

通配符规则

#:匹配一个或者多个词

*:匹配恰好一个词

SpringAMQP

Spring官方基于RabbitMQ提供的一套消息收发的模版工具:SpringAMQP

提供了三个功能:

自动声明队列,交换机以及绑定关系

基于注解的监听器模式,异步接收消息

封装RabbitTemplate工具用于发送消息

引入依赖
xml 复制代码
 <!--AMQP依赖,包含RabbitMQ-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

实现通信

新建队列->publisher发送->consumer接收

基于rabbitTemplate API进行发送

简单队列通信

发送:

java 复制代码
    @GetMapping("/simple")
    public void publishMessage(){
        String queueName = "simplequeue";
        String message = "simplequeuemessage";
        rabbitTemplate.convertAndSend(queueName,message);
        System.out.println("simple_success");
    }

接收:

java 复制代码
@Component
public class MqListener {
    @RabbitListener(queues = "simplequeue")
    public void simpleListener(String msg){
        System.out.printf("%s 简单队列收到消息:%s", Convert.toStr(LocalDateTime.now()),msg);
    }
}

工作队列通信

多个消费者监听一个队列,不同消费者因为自身的能力不同对消息处理的时间也不同

如果不进行额外设置的话,会将队列中的消息平均分配给所有消费者

造成处理能力浪费的情况

所以我们可以通过配置

yaml 复制代码
    listener:
      simple:
        prefetch: 1#每个消费者每次只能取一条

来限制每个消费者预取的数量,实现能者多劳的工作场景

发送:

java 复制代码
    @GetMapping("/work")
    public void PublishWorkMessage(){
        String queueName = this.WORK;
        for (int i = 0; i < 100; i++) {
            rabbitTemplate.convertAndSend(queueName,System.currentTimeMillis());
        }
    }

接收:

java 复制代码
    @RabbitListener(queues = "workqueue")
    public void workListener1(String msg){
        System.out.printf("%s 工作队列1收到消息:%s\n", Convert.toStr(LocalDateTime.now()),msg);
    }

    @RabbitListener(queues = "workqueue")
    public void workListener2(String msg){
        System.out.printf("%s 工作队列2收到消息:%s\n", Convert.toStr(LocalDateTime.now()),msg);
    }

交换机

上述两种工作方式都是不包含交换机,消息直接发送到队列的通信方式

我们可以通过引入交换机来实现消息的路由,决定具体要发送到哪个队列

消息通信流程 发布者->交换机->交换机决定的队列->消费者

交换机类型

包含以下四种:

FanOut:广播,将消息传递给所有绑定交换机的队列
Direct:基于RoutingKey发送消息给对应的队列
Topic:通配符订阅,基于通配符RoutingKey发送消息给对应的队列
Headers:头匹配,基于MQ的消息头匹配

FanOut

创建多个队列->创建交换机进行绑定->发布者发布->消费者接收

发布:

java 复制代码
    @GetMapping("/fanout")
    public void publishFanoutMessage(){
        String exchangeName = this.FANOUT;
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend(exchangeName,"",System.currentTimeMillis() + "from:fanout");
        }
    }

接收:

java 复制代码
    @RabbitListener(queues = "cfjg_queue1")
    public void queue1Listener(String msg){
        System.out.printf("%s 队列1收到消息:%s\n", Convert.toStr(LocalDateTime.now()),msg);
    }
    @RabbitListener(queues = "cfjg_queue2")
    public void queue2Listener(String msg){
        System.out.printf("%s 队列2收到消息:%s\n", Convert.toStr(LocalDateTime.now()),msg);
    }

Direct

创建多个队列->创建交换机进行路由key绑定->发布者发布->消费者接收

发布:

java 复制代码
    @GetMapping("/direct")
    public void publishDirectMessage(){
        String exchangeName = this.DIRECT;
        for (int i = 0; i < 10; i++) {
            if(i%2 == 0){
                rabbitTemplate.convertAndSend(exchangeName,this.Queue1,"directTo:queue1--" + i);
            }else if(i%2 !=  0){
                rabbitTemplate.convertAndSend(exchangeName,this.Queue2,"directTo:queue2--" + i);
            }
            if(i%5 == 0){
                rabbitTemplate.convertAndSend(exchangeName,this.ALL,"directTo:all--" + i);
            }
        }
    }

接收:

java 复制代码
    @RabbitListener(queues = "cfjg_queue1")
    public void queue1Listener(String msg){
        System.out.printf("%s 队列1收到消息:%s\n", Convert.toStr(LocalDateTime.now()),msg);
    }
    @RabbitListener(queues = "cfjg_queue2")
    public void queue2Listener(String msg){
        System.out.printf("%s 队列2收到消息:%s\n", Convert.toStr(LocalDateTime.now()),msg);
    }

Topic

创建多个队列->创建交换机进行通配符路由key绑定->发布者发布->消费者接收

路由key由多个由.分割的单词组成

绑定时使用#或*通配符进行绑定

#:代表任意多个单词

*.代表任意一个单词

eg:cfjg.test

可以由cfjg.#或者*.test进行匹配

发布:

java 复制代码
    @GetMapping("/topic")
    public void publishTopicMessage(){
        for (int i = 0; i < 100; i++) {
            String tmp = i % 2 == 0 ? "two" : "one";
            String routingKey = "cfjg." + tmp;
            System.out.println(routingKey);
            rabbitTemplate.convertAndSend(this.TOPIC,routingKey,"topicTo:queue--" + i);
        }
    }

接收:

java 复制代码
    @RabbitListener(queues = "cfjg_queue1")
    public void queue1Listener(String msg){
        System.out.printf("%s 队列1收到消息:%s\n", Convert.toStr(LocalDateTime.now()),msg);
    }
    @RabbitListener(queues = "cfjg_queue2")
    public void queue2Listener(String msg){
        System.out.printf("%s 队列2收到消息:%s\n", Convert.toStr(LocalDateTime.now()),msg);
    }

声明队列和交换机

SpringAMQP提供了一个Queue类用来创建队列

java 复制代码
public class Queue extends AbstractDeclarable implements Cloneable {}

提供了一个Exchange接口用来表示不同类型的交换机

SpringAMQP提供了ExchangeBuilder和BindingBuilder来简化创建和绑定队列和交换机的过程

我们可以在消费者中编写一个配置类来对队列和交换机进行声明

java 复制代码
@Configuration
public class MqConfig {
    //声明交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("cfjg.fanout");
    }
	
    //声明队列
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("test_queue");
    }
	
    //声明绑定
    @Bean
    public Binding bingingQueue1(){
        return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
    }
}
direct和topic模式需要每个key都进行一次绑定(同控制台操作)

基于注解声明

Spring提供了基于注解方式进行声明的途径

通过注解可以声明

绑定
队列
交换机
路由key
java 复制代码
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(name = "cfjg_queue2"),
                    exchange = @Exchange(name = "cfjg.fanout" , type = ExchangeTypes.FANOUT),
                    key = {"red","blue"}
            )
    )
    public void fanoutQueue(String msg){
        System.out.println(msg);
    }

消息转换器

在MQ的消息传输中,会先将对象序列化为字节,接收消息时将字节反序列化为Java对象

但是默认的JDK序列化存在以下问题

数据体积过大,

存在安全漏洞,

可读性差

可以使用JSON进行序列化和反序列化

引入JackSon依赖来进行JSON序列化

xml 复制代码
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.10</version>
</dependency>

通过在配置类中注册转换器Bean来实现消息发送时的自动序列化

java 复制代码
	@Bean
    public MessageConverter messageConverter(){
        return new jackson2JsonMessageConverter();
    }
相关推荐
逸风扬1 分钟前
被裁了(9年)
java·程序人生
DKPT8 分钟前
如何确定MySQL中哪些列适合做索引
数据库·spring boot·spring·spring cloud·缓存
极客先躯20 分钟前
中级java每日一道面试题-2024年7月3日
java·开发语言·java每日一道面试题
Song22 分钟前
C++:求梯形面积
开发语言·c++
wilsonzane26 分钟前
Mongodb集群中的分布式读写
数据库·分布式·mongodb
关中雪35 分钟前
【应届应知应会】SQL常用知识点50道
数据库·mysql·nosql·秋招·校招·春招
AskHarries37 分钟前
Spring Boot集成geode快速入门Demo
java·spring boot·后端·geode
zxrhhm1 小时前
MySQL中使用PROFILING来查看SQL执行流程
数据库·mysql
醇氧1 小时前
【postgresql】表操作
数据库·sql·postgresql