likes
comments
collection
share

Rabbit Mq 异步通信学习记录

作者站长头像
站长
· 阅读数 2

初始 MQ

同步调用存在的问题

耦合度高

每次加入新的需求,都要修改原来的代码

性能下降

调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和。

资源浪费

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

级联失败

如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务群故障

同步调用的优点:

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

同步调用的问题:

  • 耦合度高
  • 性能和吞吐能力下降
  • 有额外的资源消耗
  • 有级联失败问题

异步调用方案

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

当有新的需要订阅事件

优势一:服务解耦

Rabbit Mq 异步通信学习记录

优势二:性能提升,吞吐量提高

Rabbit Mq 异步通信学习记录

优势三:服务没有强依赖,不担心级联失败问题,不浪费资源

Rabbit Mq 异步通信学习记录

优势四:流量削峰

Rabbit Mq 异步通信学习记录

异步通信的优点:

  • 耦合度低
  • 吞吐量提升
  • 故障隔离
  • 流量削峰

异步通信的缺点:

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

什么是 MQ

MQ (MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。

几种常见MQ的对比:

RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApache阿里Apache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP,SMTP,STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义协议自定义协议
可用性一般
单机吞吐量一般非常高
消息延迟微秒级毫秒级毫秒级毫秒以内
消息可靠性一般一般

快速上手 / 安装

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

使用 docker pull rabbitmq镜像,创建容器并运行

运行容器

  • --name my-rabbitmq: 这个参数设置容器的名称为 my-rabbitmq
  • -p 5672:5672 5672 端口用于 AMQP 协议通信
  • -p 15672:15672: 15672 端口用于 RabbitMQ 管理界面
  • -e RABBITMQ_DEFAULT_USER=myuser 设置了 RabbitMQ 的默认用户名和密码
  • -e RABBITMQ_DEFAULT_PASS=mypassword
  • --hostname my-rabbitmq-host: 设置了容器的主机名为 my-rabbitmq-host。主机名是容器在网络中的标识符
  • rabbitmq:3-management: 这是要运行的 Docker 镜像的名称和标签。
docker run \
--name my-rabbitmq \
--hostname my-rabbitmq \
-e rabbitmq_default_user=admin \
-e rabbitmq_default_pass=1234 \
-p 5672:5672 \
-p 15672:15672 \
-d rabbitmq

开启管理平台

docker exec -it my-rabbitmq bash
rabbitmq-plugins enable rabbitmq_management

做完这个,但是无法打开通道,需要修改配置文件

docker exec -it 容器ID bash
cd /etc/rabbitmq/conf.d/
echo management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf
exit
docker restart 容器ID

如果部署到服务器上,那么密码设置的 admin 和1234是不可以访问的

默认是 guest 密码也是 guest

设置访问密码

# 添加账户
rabbitmqctl add_user admin 1234
# 设置角色
rabbitmqctl set_user_tags admin administrator
# 设置权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"

管理平台介绍

这是总览页面,包含节点,以及连接信息

Rabbit Mq 异步通信学习记录

连接页面

Rabbit Mq 异步通信学习记录

这里做消息的接收-发送等

Rabbit Mq 异步通信学习记录

在这里做管理。

Rabbit Mq 异步通信学习记录

这里是添加用户,配置角色和密码

Rabbit Mq 异步通信学习记录

但是添加之后,是没有任何的访问权限的,这里有一个虚拟主机的概念,避免两个用户访问时,出现信息错乱。

Rabbit Mq 异步通信学习记录

添加虚拟主机

Rabbit Mq 异步通信学习记录

在用户界面,点击进去,配置虚拟主机。

Rabbit Mq 异步通信学习记录

每一个用户应该具备自己的虚拟主机。将数据隔离开。

消息发送者将消息发送到路由队列,路由队列负责将消息存入到队列中,消费者可以从队列中读取数据。虚拟主机是对路由和队列进行逻辑分组,隔离数据。

Rabbit Mq 异步通信学习记录

RabbitMQ中的几个概念:

  • channel:操作MQ的工具
  • exchange:路由消息到队列中
  • queue:缓存消息
  • virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组

常见消息模型

MQ的官方文档中给出了5个MQ的Demo示例,对应了几种不同的用法:

  • 基本消息队列(BasicQueue)
  • 工作消息队列(WorkQueue)

发布订阅(Publish、Subscribe),又根据交换机类型不同分为三种:

  • Fanout Exchange:广播
  • Direct Exchange:路由
  • Topic Exchange:主题

官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:

  • publisher:消息发布者,将消息发送到队列queue
  • queue:消息队列,负责接受并缓存消息
  • consumer:订阅队列,处理队列中的消息

Rabbit Mq 异步通信学习记录

简单的实现队列

依赖

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

发送者、生产者

建立连接

// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("localhost");
factory.setPort(5672);
factory.setVirtualHost("/"); // 虚拟主机
factory.setUsername("admin");
factory.setPassword("123456");
// 1.2.建立连接
Connection connection = factory.newConnection();

连接之后查看管理界面,发现有一个连接了

Rabbit Mq 异步通信学习记录

创建通道,走完可以看到控制界面有通道了

// 2.创建通道Channel
Channel channel = connection.createChannel();

Rabbit Mq 异步通信学习记录

接下来创建队列

// 3.创建队列--队列名称
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);

Rabbit Mq 异步通信学习记录

发送消息,并关闭通道

// 4.发送消息
String message = "hello, rabbitmq!";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:【" + message + "】");

// 5.关闭通道和连接
channel.close();
connection.close();

Rabbit Mq 异步通信学习记录

接收者、消费者

第一步还是建立连接,创建通道、创建队列

接下来我们订阅消息

使用 channel.basicConsume(queueName, true, consumer) 方法订阅指定名称的队列 queueName 中的消息,其中第二个参数 true 表示自动确认消息,即在接收到消息后,消息将被认为已经被消费,因此不需要手动确认。

consumer 是一个实现了 Consumer 接口的对象,用于处理接收到的消息。

在 handleDelivery 方法中,重写了 DefaultConsumer 类的 handleDelivery 方法,用于处理接收到的消息。

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

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

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

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

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

SpringAMQP

依赖

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

SpringAmqp的官方地址:spring.io/projects/sp…

AMQP(协议)

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

Spring AMQP

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

SpringBoot 发送消息,接收消息

配置参数

spring:
  rabbitmq:
    host: localhost # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: admin # 用户名
    password: 123456 # 密码

发送消息

@SpringBootTest
public class PublisherTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void name() {
        String queueName = "simple.queue";
        rabbitTemplate.convertAndSend(queueName, "消息已发送!");
    }
}

接收消息

@Component
public class SpringRabbitMqListener {

    // 监听名为 "simple.queue" 的 RabbitMQ 队列
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg) {
        // 当队列中有消息到达时,该方法将被调用,并打印消息内容
        System.out.println("msg = " + msg);
    }
}

SpringAMQP如何接收消息?

  1. 引入amqp的starter依赖
  2. 配置RabbitMQ地址
  3. 定义类,添加@Component注解
  4. 类中声明方法,添加@RabbitListener注解,方法参数就时消息

注意:消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能

Work Queue 工作队列

Work queue,工作队列,可以提高消息处理速度,避免队列消息堆积

多个消费者同时处理一个通道的消息

Rabbit Mq 异步通信学习记录

但是如果直接这么设置啊,会出现消费预取限制的问题。他们会提前把消息拿出来,存进去,再处理。也就是平分消息的。这种方式没有考虑到消费者的能力哈,有的消费快,有的消费慢

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

spring:
  rabbitmq:
    # RabbitMQ 监听器配置
    listener:
      # 简单消息监听器配置
      simple:
        # 每个消费者最多预取的消息数量,即消费者在确认之前可以接收的未确认消息的最大数量
        prefetch: 1 # 设置为1,表示每个消费者一次只接收一条未确认消息

Work模型的使用:

多个消费者绑定到一个队列,同一条消息只会被一个消费者处理

通过设置prefetch来控制消费者预取的消息数量

发布( Publish )、订阅( Subscribe )

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

常见exchange类型包括:

  1. Fanout:广播
  2. Direct:路由
  3. Topic:话题

Rabbit Mq 异步通信学习记录

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

FanoutExchange

SpringAMQP提供了声明交换机、队列、绑定关系的API,例如:

Rabbit Mq 异步通信学习记录

队列绑定交换机

@Configuration
public class FanoutConfig {

    // 定义一个 FanoutExchange的交换机,名字是kunkun.fanout
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange("kunkun.fanout");
    }

    // 定义一个队列 fanoutQueue1,名字是kun.queue1
    @Bean
    public Queue fanoutQueue1() {
        return new Queue("kun.queue1");
    }

    // 定义一个绑定,将 kun.queue1 绑定到 kunkun.fanout 的交换机
    @Bean
    public Binding fanoutBind1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
        return BindingBuilder
                .bind(fanoutQueue1) // 绑定 fanoutQueue1
                .to(fanoutExchange); // 绑定到 fanoutExchange
    }
    
    // 绑定多个到交换机......
    @Bean
    public Queue fanoutQueue2() {
        return new Queue("kun.queue2"); 
    }

    @Bean
    public Binding fanoutBind2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
        return BindingBuilder
                .bind(fanoutQueue2) 
                .to(fanoutExchange);
    }
}

程序运行,查看控制台

已经有交换机了

Rabbit Mq 异步通信学习记录

查看队列及绑定关系

Rabbit Mq 异步通信学习记录

Rabbit Mq 异步通信学习记录

接下来我们准备程序:

分别是监听名为 "kun.queue1" 和 "kun.queue2" 的队列,接收消息并处理。

@Component
public class SpringRabbitMqListener {

    // 监听名为 "kun.queue1" 的队列,接收消息并处理
    @RabbitListener(queues = "kun.queue1")
    public void listenWorkSimpleQueue1(String msg) throws InterruptedException {
        System.out.println("消息者1:" + msg); // 打印接收到的消息
    }

    // 监听名为 "kun.queue2" 的队列,接收消息并处理
    @RabbitListener(queues = "kun.queue2")
    public void listenWorkSimpleQueue2(String msg) throws InterruptedException {
        System.err.println("消息者2:" + msg); // 打印接收到的消息
    }
}

发送信息,参数一从队列改成交换机,实现一条消息多个消费者接收。

// 发送消息到名为 "kunkun.fanout" 的 FanoutExchange,这会将消息广播到所有绑定到该 Exchange 的队列
@Test
void name() {
    rabbitTemplate.convertAndSend("kunkun.fanout", "", "hello ever one!"); // 发送消息
}

交换机的作用是什么?

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

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

  • Queue
  • FanoutExchange
  • Binding

发布订阅-DirectExchange

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

每一个Queue都与Exchange设置一个BindingKey

发布者发送消息时,指定消息的RoutingKey

Exchange将消息路由到BindingKey与消息RoutingKey一致的队列

Rabbit Mq 异步通信学习记录

创建队列及交换机

@Component
public class SpringRabbitMqListener {

    // 对列名:direct.queue1
    // 交换机为:kunkun.direct,类型为路由模式,并将队列帮到到此交换机
    // 使用路由键 "red" 和 "yellow" 绑定到队列 "direct.queue1",接收消息并处理
    @RabbitListener(bindings =
    @QueueBinding(value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "kunkun.direct", type = ExchangeTypes.DIRECT),
            key = {"red", "yellow"}
    ))
    public void listenWorkSimpleQueue1(String msg) {
        System.out.println("消息者接收到路由模式的消息:" + msg); // 打印接收到的消息
    }

}

发送信息

在参数二中指定key,匹配相同的key的消息队列

@SpringBootTest
public class PublisherTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void name() throws InterruptedException {
        rabbitTemplate.convertAndSend("kunkun.direct", "blue", "hello ever one!");
    }
}

描述下Direct交换机与Fanout交换机的差异?

Fanout交换机将消息路由给每一个与之绑定的队列

Direct交换机根据RoutingKey判断路由给哪个队列

如果多个队列具有相同的RoutingKey,则与Fanout功能类似

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

@Queue

@Exchange

发布订阅-TopicExchange

跟之前的代码是一样的

区别的是概念不一样,再指定key的时候

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

注意:需要将类型改为 ExchangeTypes.TOPIC

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

  • #:代指0个或多个单词
  • *:代指一个单词

Rabbit Mq 异步通信学习记录

@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("消费者1接收到Topic消息:【" + msg + "】");
}

消息转换器

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

当我们把数据发送之后,我们看一下控制台

@Test
void name() throws InterruptedException {
    HashMap<String, Object> map = new HashMap<>();
    map.put("name", "刘诗诗");
    map.put("age", 21);
    rabbitTemplate.convertAndSend("object.queue", map);
}

我们能看到,数据是java序列化之后的。

Rabbit Mq 异步通信学习记录

修改为 JSON 序列化方式

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

如果要修改只需要定义一个MessageConverter 类型的Bean即可。推荐用JSON方式序列化,步骤如下: 我们在publisher服务引入依赖

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

我们在publisher服务声明MessageConverter:

// org.springframework.amqp.support.converter;
@Bean
public MessageConverter messageConverter() {
    return new Jackson2JsonMessageConverter();
}

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

利用MessageConverter实现的,默认是JDK的序列化

注意发送方与接收方必须使用相同的MessageConverter