消息队列MQ

初识MQ

同步通讯

业务之间以串行方式工作。

同步调用的优点

时效性强,可以立即得到结果

同步调用的问题

微服务间基于Feign的调用就是属于同步方式,存在一些问题。

  1. 耦合度高:每次加入新的需求,都要修改原来的代码。
  2. 性能下降,吞吐量下降:调用者需要等待服务提供者响应,如果调用链过长则响应事件等于每次调用的时间之和。
  3. 资源浪费:调用链中的每个服务都在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源。
  4. 级联失败:如果服务提供者出现问题,所有调用方都会跟着出现问题,导致整个微服务群故障。

 


异步通讯

业务之间以并行方式工作。

异步调用方案

异步调用常见实现就是事件驱动模式

异步通讯优点

  1. 服务解耦。
  2. 性能提升,吞吐量高。
  3. 服务没有强依赖关系,不担心级联失败问题。
  4. 流量削峰:高并发的时候,Broker会做一个缓存,微服务可以基于自己的能力从Broker中获取事件并处理事件。

 

异步通讯的缺点

  1. 依赖于Broker的可靠性,安全性,吞吐能力。
  2. 架构复杂了,业务没有明显的流程线,不好追踪管理。

什么是MQ

MQ(MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker,类似于计算机体系中的缓存,缓冲消息发送和消息处理之间速度不匹配的问题。


RabbitMQ

RabbitMQ概述

RabbitMQ是基于Erlang语言开发的开源消息通信中间件。官网地址为:https://www.rabbitmq.com/

 

RabbitMQ安装

单机部署

在Centos7虚拟机中使用Docker安装。

方式一:在线拉取

docker pull rabbitmq:3-management

方式二:从本地加载

将RabbitMQ镜像包上传得到虚拟机,使用docker load -i RabbitMQ.tar即可

 

运行MQ容器

docker run \
-e RABBITMQ_DEFAULT_USER=bo \
-e RABBITMQ_DEFAULT_PASS=123456789 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management

-e是设置环境变量

hostname是主机名,便于以后集群工作

15672是rabbitmq管理界面

5672是rabbitmq通信的端口

 

访问localhost:15672即可进入管理界面

Overview:总览,结点的信息

Connection:连接,消息的消费者和消息的发布者都要与MQ建立连接

Channel:通道,建立连接后一定要创建通道,消费者和发布者都要基于通道完成消息的发送和接受

Exchanges:交换机,消息的路由器。

Queues:队列,用于队列存储。

Admin:管理用户信息。

virtual host:虚拟主机,是对queue,exchange等资源的逻辑分组

RabbitMQ的结构和概念

 

消息模型介绍

  • 基本消息队列(BasicQueue)
  • 工作消息队列(WorkQueue)
  • 发布订阅(Publish,Subscribe),有根据交换机类型不同分为三种:
    • Fanout Exchange:广播
    • Direct Exchange:路由
    • Topic Exchange:主题

 

HelloWorld案例

publisher:消息的发布者,将消息发送到队列queue。

queue:消息队列,负责接收并缓存消息。

consumer:订阅队列,处理队列中的消息。

 

下载mq-demo

https://pan.baidu.com/s/169SFtYEvel44hRJhmFTRTQ

密码1234

消费者代码

public class ConsumerTest {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("bo");
        factory.setPassword("123456789");
        // 1.2.建立连接
        Connection connection = factory.newConnection();
        // 2.创建通道Channel
        Channel channel = connection.createChannel();
        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);
        // 4.订阅消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 5.处理消息
                String message = new String(body);
                System.out.println("接收到消息:【" + message + "】");
            }
        });
        System.out.println("等待接收消息。。。。");
    }
}

Connection connection = factory.newConnection();这段代码执行完就建立起连接了

Channel channel = connection.createChannel();  这段代码执行完就创建通道了

 

发布者代码

public class PublisherTest {
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.144.137");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("bo");
        factory.setPassword("123456789");
        // 1.2.建立连接
        Connection connection = factory.newConnection();
        // 2.创建通道Channel
        Channel channel = connection.createChannel();
        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);
        // 4.发送消息
        String message = "hello, rabbitmq!";
        channel.basicPublish("", queueName, null, message.getBytes());
        System.out.println("发送消息成功:【" + message + "】");
        // 5.关闭通道和连接
        channel.close();
        connection.close();
    }
}

 

 基本消息队列的消息发送流程:

  1. 建立connection
  2. 创建channel
  3. 利用channel声明队列
  4. 利用channel向队列发送消息

基本消息队列的消息接收流程:

  1. 建立connection
  2. 创建channel
  3. 利用channel声明队列
  4. 定义consumer的消费行为handleDelivery()
  5. 利用channel将消费者与队列绑定

SpringAMQP

什么是SpringAMQP

AMQP(Advance Message Queuing Protocol),是用于在应用程序,或之间传递业务消息的开放标准,该协议与语言和平台无关,更符合微服务中独立性的要求。(应用间消息通信的一种协议,与语言和平台无关。)

Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息,包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现。

 

特征

  1. 侦听容器,用于一步处理入栈消息
  2. 用于发送和接受消息的RabbitTemplate
  3. RabbitAdmin用于自动声明队列,交换和绑定

 

 

案例:利用SpringAMQP实现HelloWorld中的基础消息队列功能

流程:

1.在父工程中引入spring-amqp的依赖

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

2.启动虚拟机中的RabbitMQ。

3.在publisher服务中配置yaml文件,并利用RabbitTemplate发送消息到simple.queue这个队列。

spring:
  rabbitmq:
    host: 192.168.144.128 #rabbitMQ的ip地址
    port: 5672 #端口
    username: bo
    password: 123456789
    virtual-host: /
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage2SimpleQueue(){
        String queueName = "simple.queue"; //消息队列的名称
        String message="hello,spring amqp!";  //消息内容
        rabbitTemplate.convertAndSend(queueName,message);  //发送
    }
}

4.在consumer服务中编写配置,并编写消费逻辑,绑定simple.queue这个队列。

spring:
  rabbitmq:
    host: 192.168.144.128 #rabbitMQ的ip地址
    port: 5672 #端口
    username: bo
    password: 123456789
    virtual-host: /
@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg){
        System.out.println("消费者接受到simple.queue的消息:"+msg);
    }
}

 

 

Work Queue工作队列

作用

可以提高消息处理速度,避免队列消息堆积。

案例:模拟WorkQueue,实现一个队列绑定对个消费者

1.在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue。

@Test
public void testSendMessage2WorkQueue() throws InterruptedException {
    String queueName = "simple.queue";
    String message="hello,message__!";
    for(int i=1;i<=50;i++){
        rabbitTemplate.convertAndSend(queueName,message+i);
        Thread.sleep(20);
    }
}

 

2.在consumer服务中定义两个消息监听这,都监听simple.queue队列。

消费者1每秒处理50条消息,消费者2每秒处理10条消息。

@Component
public class SpringRabbitListener {
    @RabbitListener(queues = "simple.queue")
    public void listenWorkQueue1(String msg) throws InterruptedException {
        System.out.println("消费者1接受到的消息:"+msg);
        Thread.sleep(20);
    }
    @RabbitListener(queues = "simple.queue")
    public void listenWorkQueue2(String msg) throws InterruptedException {
        System.err.println("消费者2接受到的消息:"+msg);
        Thread.sleep(200);
    }
}

可以发现消费者1处理的都是奇数条,消费者2处理的是偶数条。

 

消费预取限制

修改application.yml文件,设置preFetch这个值,可以控制预取消息的上限

spring:
  rabbitmq:
    host: 192.168.144.128 #rabbitMQ的ip地址
    port: 5672 #端口
    username: bo
    password: 123456789
    virtual-host: /
    listener:
      simple:
        prefetch: 1 #预取消息的上限

 


发布订阅模式

发布(Publish),订阅(Subscribe)

发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机)。

 

常见的exchange类型包括:

Fanout:广播

Direct:路由

Topic:话题

注意:exchange负责消息路由,而不是存储,路由失败则消息丢失。

 


FanoutExchange

Fanout Exchange会将接收到哦消息路由到每一个跟其绑定额queue

案例:利用SpringAMQP演示FanoutExchange的使用

1.在consumer服务中,利用代码声明队列,交换机,并将两者绑定

@Configuration
public class FanoutConfig {
    //itcast.fanout
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("itcast.fanout");
    }
    //itcast.fanout1
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.queue1");
    }
    //绑定队列1到交换机
    @Bean
    public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }
    //itcast.fanout2
    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }
    //绑定队列2到交换机
    @Bean
    public Binding fanoutBinding2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }
}

2.在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2

@Component
public class SpringRabbitListener {
  @RabbitListener(queues = "fanout.queue1")
  public void listenFanoutQueue1(String msg)  {
      System.out.println("消费者接受到fanout.queue1的消息:" + msg);
  }
    @RabbitListener(queues = "fanout.queue2")
    public void listenFanoutQueue2(String msg)  {
        System.out.println("消费者接受到fanout.queue2的消息:" + msg);
    }
}

3.在publisher中编写测试方法,向itcast.fanout发送消息。

@Test
public void testSendFanoutExchange(){
    String exchangeName = "itcast.fanout";  //发送目标为交换机
    String message="hello,everyone!";
    rabbitTemplate.convertAndSend(exchangeName,"",message);//向交换机发送
}

 交换机的作用

  • 接收publisher发送的消息
  • 将消息按照规则路由到与之绑定的队列
  • 不能缓存消息,路由失败,消息丢失
  • FanoutExchange的会将消息路由到每个绑定的队列

声明队列,交换机,绑定关系的Bean是什么

  • Queue
  • FanoutExchange
  • Binding

 

DirectExchange

DirectExchange会将收到的消息根据路由规则路由到指定的Queue,因此称为路由模式(routes)。

  • 每一个Queue都与Exchange设置一个BindingKey
  • 发布者发送消息时,指定消息的RoutingKey
  • Exchange将消息路由到BindingKey与消息RoutingKey一致的队列

 

案例:利用SpringAMQO演示DirectExchange的使用

实现

1.利用RabbitListener声明Exchange,Queue,RoutingKey。 

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

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

2.在控制台可以看见绑定的Routing key

3.编写消息发送代码

@Test
public void testSendDirectExchange(){
    String exchangeName = "itcast.direct";
    String message="hello,blue!";
    rabbitTemplate.convertAndSend(exchangeName,"blue",message);
}

只有绑定了blue的才会收到。

 

Direct交换机与Fanout交换机的差异

  • Fanout交换机将消息路由到每一个与之绑定的队列。
  • Direct交换机根据RoutingKey判断路由给哪个队列。
  • 如果对个队列具有相同的RoutingKey,则与Fanout功能类似。

 

基于@RabbitListener注解声明队列和交换机有哪些常见注解?

  • @Queue
  • @Exchange

TopicExchange

TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以“.”分割

Queue与Exchange指定BindingKey时可以使用通配符:

#:代指0个或多个单词。

*:代指一个单词。

 

案例:利用SpringAMQP演示TopicExchange的使用

实现

1.利用@RabbitListener声明Exchange,Queue,RoutingKey,在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2

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

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

2.在后台中可以看到已经注册成功

在publisher中编写测试方法,向itcast.topic发送消息

测试1

key为“china.#”和“#.news”都可以收的到

@Test
public void testSendTopicExchange(){
    String exchangeName = "itcast.topic";
    String message="黄剑波被中国科学院大学电子信息专业录取!!牛逼!!";
    rabbitTemplate.convertAndSend(exchangeName,"china.news",message);
}

 

测试2

@Test
public void testSendTopicExchange(){
    String exchangeName = "itcast.topic";
    String message="今天天气不错!";
    rabbitTemplate.convertAndSend(exchangeName,"china.weathcer",message);
}

只有key为“china.#”的才能收到,

  Direct交换机与Topic交换机的差异?

Topic交换机支持通配符。


消息转换器

 

案例:测试发送Object类型消息

说明:在SpringAMQP的发送方法中,接受消息的类型是Object,也就是说我们可以发哦是那个任意对象类型的消息,SpringAMQP会帮我们序列化字节后发送。

测试如果不使用序列化直接发送Object会如何:

1.注册一个消息队列object.queue

@Bean
public Queue objectQueue(){
    return new Queue("object.queue");
}

2.向object.queue中发送Map对象

@Test
public void testSendobjectExchange(){
    Map<String,Object> msg = new HashMap<>();
    msg.put("name","Bo");
    msg.put("age",21);
    rabbitTemplate.convertAndSend("object.queue",msg);
}

3.在控制台查看发现与预期不符

 

所以消息发送时候是需要序列化的

Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。

如果要修改只需定义一个MessageConverter类型的Bean即可。推荐使用JSON方式序列化,步骤如下:

1.首先在publisher中引入依赖

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

2.在publisher启动类中中声明MessageConverter(这里要注意引入的依赖都是amqp的)

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class PublisherApplication {
    public static void main(String[] args) {
        SpringApplication.run(PublisherApplication.class);
    }

    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

3.在控制台可以看到已经转换成功

 

消息接收时候也需要序列化

1.在consumer服务中引入jackson依赖

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

2.在consumer启动类中定义MessageConverter

@Bean
public MessageConverter messageConverter(){
    return new Jackson2JsonMessageConverter();
}

3.然后定义一个消费者,监听object.queue队列并消费消息

@RabbitListener(queues ="object.queue")
public void listenObjectQueue(Map<String,Object> msg){
  System.out.println("接收得到object.queue的消息:"+msg);
}

 

SpringAMQP中消息的序列化和反序列化是怎么实现的?

  • 利用MessageConverter实现的,默认是JDK的序列化
  • 发送方与接收方必须使用相同的MessageConverter
posted @   Laplace蒜子  阅读(114)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示