RabbitMQ

112 阅读14分钟

RabbitMQ

引入

普通的业务场景,咱们客户端发送一个请求,后端需要经过Controller-->Service-->Dao等一系列流水作业,是一个同步的过程,咱们需要一步步进行,当然,每一步需要耗费时间,让用户等待过多的时间一种不好的体验。

image-20220626130725269

那么我们优化一下:

image-20220626144244393

这样,让不相干的业务异步执行一下,可以适当缩短时间,从之前的150ms缩短到了100ms,但好像还是挺久的,有没有什么办法可以解决呢?

有一些业务,比如发送验证码,邮件等,用户不需要等待后端完全完成发送后才知道,而是发送请求的那一刻,后端接收到请求后便可以直接返回给客户端一个信号(发送成功了),接下来的事,留给后端自己去做就行了,从而减少了客户的等待时间,那么,消息队列就应运而生了,咱们可以把消息写到消息队列中,其他服务主动的去消费这个队列中的信息来执行业务,客户端生产消息,服务端监听并且消费消息的过程。

image-20220626131531987

消息队列的优点

应用解耦

传统的业务中,一个系统跟另一个系统之间通过调用函数等方式来互相交互,耦合度很高。

举个例子:服务A调用了服务B的一个方法,但是,某一天,服务B的这个方法发生了迭代,传参变化了,咱们是不是还得下线一下服务A把对应的调用方法参数变化一下再重新上线呢?损失了时间和维护成本。

消息队列就可以很好的解决这个问题,服务A可以把调用的消息发送到消息队列,也就只是发送一下"我需要调用服务B的方法",并不需要像之前那样严格的调用方法,服务B的特定方法监听到了消息,就会主动调用自己的方法了,那么服务B的迭代就跟A没有强耦合了,因为添加了消息中间件。这就是解耦合的思想。

削峰

在一些高峰期,服务器某时刻可能突然承受很大的并发导致服务器崩溃甚至宕机,那么消息队列可以很好的缓解高并发带来的服务器压力过大。

高并发量到来时,这些请求会优先储存到消息队列中,然后服务器会根据自己的能力来在队列中读取这些请求并处理而不是一口气接收了所有请求,起到了一个哼好的缓冲作用。

两个重要概念

  • 消息代理(message broker)消息生产者发送消息后,优先发送到消息代理,消息代理再通过交换机(Exchange)发送到对应绑定(Binding)的队列(Queue)。

  • 目的地(destination) :消息传递到指定目的地。(目的地分为两种形式)

    • 队列点对点消息通信:

      • 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获

        取消息内容,消息读取后被移出队列。

        消息只有唯一的发送者和接受者,但并不是说只能有一个接收者

    • 主题发布(subscribe)/订阅(subscribe)消息通信:

      • 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个

        主题,那么就会在消息到达时同时收到消息。

JMS和AMQP

  • JMS(Java Message Service):JAVA消息服务: 基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现.
  • AMQP(Advanced Message Queuing Protocol) 高级消息队列协议,也是一个消息代理的规范兼容JMS,RabbitMQ是AMQP的实现.

两者的对比如下表:

image-20220626133526175

Spring提供了对JMS和AMQP的支持,很方便,SpringBoot也包含了对两者的自动配置。

RabbitMQ

简介

RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。

核心概念

  • Message消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,

    这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可

    能需要持久性存储)等。

  • Publisher消息的生产者,就是向消息代理发送消息的客户端应用程序。
  • Exchange交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。 Exchange有4种类型:direct(默认),fanout, topic, 和headers,不同类型的Exchange转发消息的策略有所区别。
  • Queue消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直 在队列里面,等待消费者连接到这个队列将其取走。
  • Binding绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键(routing-key)将交换器和消息队列连接起来的路由规则,所以可以将交 换器理解成一个由绑定构成的路由表。 Exchange 和Queue的绑定可以是多对多的关系。
  • Connection:网络连接,比如一个TCP连接。
  • Channel信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道 发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都 是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接

注意:一条连接中开辟多条信道来交互消息。

  • Consumer:消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
  • Virtual Host虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加 密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥 有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时 指定,RabbitMQ 默认的 vhost 是 / 。

  • Broker:表示消息队列服务器实体。(消息代理)

    关系图:

image-20220626134630894

Docker启动RabbitMQ

直接在Docker Hub上找到对应的RabbitMQ镜像拉取,十分方便。

docker run -d 
--name rabbitmq 
-p 5671:5671 
-p 5672:5672 
-p 4369:4369 
-p 25672:25672 
-p 15671:15671 
-p 15672:15672 
rabbitmq:management

端口说明:

  • 4369, 25672 (Erlang发现&集群端口)
  • 5672, 5671 (AMQP端口)
  • 15672 (web管理后台端口)
  • 61613, 61614 (STOMP协议端口)
  • 1883, 8883 (MQTT协议端口)

拉去后直接访问15672端口,也就是后台管理端口,就可以看到RabbitMQ的后台管理页面了。

AMQP的消息路由

AMQP中增加了ExchangeBinding的角色。生产者把消息发布到Exchange上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到哪个队列。

image-20220626135226999

Exchange类型

Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,headers交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了。

下面由四个图,就可以很清楚看出来区别了:

  • Direct:消息中的路由键(routing key)如果和Binding中的binding key一致, 交换器 就将消息发到对应的队列中。路由键与队 列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则转发routing key标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。

image-20220626135515829

  • Fanout:每个发到 fanout 类型交换器的消息都 会分到所有绑定的队列上去。fanout 交 换器不处理路由键,只是简单的将队列 绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内 的主机都获得了一份复制的消息。

    注意:因为Fanout实际上不需要处理路由键匹配的问题,那么它转发消息是最快的。

    image-20220626135717458

  • Topic:topic 交换器通过模式匹配分配消息的 路由键属性,将路由键和某个模式进行 匹配,此时队列需要绑定到一个模式上。 它将路由键和绑定键的字符串切分成单词,这些单词之间用隔开。它同样也会识别两个通配符:符号“#”和符号“”。#匹配0个或多个单词,“*”只匹配一个单词。

image-20220626135906324

SpringBoot测试RabbitMQ

导入依赖

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

配置文件

spring:
  rabbitmq:
    host: 自己的主机ip地址
    port: 5672
    virtual-host: /

自定义配置

因为发送的消息如果是对象,会使用默认JDK序列化机制,也就是发送的类需要实现serelizable,序列化成流数据,存储起来读取不太容易,我们就需要自己配置一个序列化器,让他转化成Json类型数据传输。

@Configuration
public class MyRabbitConfig {
    /**
     * 向容器中添加一个消息转换器,就会使用咱们的
     * 配置自己的消息转换器(object->json)
     * @return
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}
​

@EnableRabbit

开启RabbitMQ的作用。

重要的组件

  • AmqpAdmin管理组件(包括创建删除交换机,绑定,队列等)。
  • RabbitTemplate消息发送处理组件。

测试

创建交换机
@Test
void createExchange() {
    //1.使用AmqpAdmin创建一个Direct类型的交换机
    DirectExchange directExchange = new DirectExchange("hello-java-exchange", true, false);
    amqpAdmin.declareExchange(directExchange);
    log.info("exchange[{}]创建成功","hello-java-exchange");
}

image-20220626141216050

创建队列
@Test
public void createQueue() {
    Queue queue = new Queue("hello-java-queue", true, false, false);
    amqpAdmin.declareQueue(queue);
    log.info("exchange[{}]创建成功","hello-java-queue");
}

image-20220626141249016

创建绑定
@Test
public void createBinding() {
    Binding binding = new Binding("hello-java-queue",  //绑定的队列名字
            Binding.DestinationType.QUEUE,             //被绑定的对象:可以是队列或者交换机
            "hello-java-exchange", //绑定的交换机名字
            "hello.java",          //绑定的路由键
            null);                 //队列中的初始参数
    amqpAdmin.declareBinding(binding);
    log.info("binding[{}]创建成功","hello-java-binding");
}

image-20220626141313366

发送消息
@Test
void sendMessageTest() {
    //1. 发送消息,如果发送的是对象,会采用序列化机制,需是实现serelizable
    for(int i=0; i<10; i++) {
        String msg = "Hello world--"+i;
        rabbitTemplate.convertAndSend("hello-java-exchange", //发送给的交换机
                                      "hello.java",   //使用的路由键
                                      msg);
        log.info("消息发送完成{}",msg);
    }
}

发送后就可以在队列中看到发送到的消息。

消费者监听消息
@RabbitListener

可以用在类+方法上,但一般用在类上,说明要监听哪个队列的消息。

@RabbitHandler

只能用在方法上重载接收不同的消息。

@Service("orderItemService")
@RabbitListener(queues = {"hello-java-queue"})
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
    /**
     * queues:声明要监听的queue
     * 参数可以写成一下类型
     * 1.Message message:原生消息信息:头+体
     * 2.消息体:String content:泛型
     * 3.Channel channel: 当前传输数据的通道
     * Queue: 可以很多人都来监听,但最后只能有一个人收到,收到后Queue消息删除
     * 场景:
     *    1) 订单服务启动多个,同一个消息只能被一个客户端收到
     *    2) 只能有一个消息接收处理完才能处理下一个
     */
    @RabbitHandler
    public void receiveMessage(Message message, String content, Channel channel) {
        System.out.println("接收到消息:"+message+"--->内容:"+content);
        //获取消息体
        byte[] body = message.getBody();
        //获取消息头
        MessageProperties messageProperties = message.getMessageProperties();
        System.out.println("消息处理完成-->"+content);
    }
}

消息确认机制-可靠抵达

生产者产生消息并发送给消息队列后,消息先到达Broker中的交换机,交换机再通过绑定传递个队列,队列再交互到相应的服务来处理,那么如何确认消息在这一系列传递中是顺利进行而没有丢失呢?我们就引入了消息确认机制

生产者端:我们有两个回调:

  • confirmCallback:确认模式,就是消息顺利到达交换机后的回调。
  • returnBack:退回模式,就是消息未能顺利到达queue的回调。(顺利就不会回调)

消费者端:

  • ack机制:就是消费者是否接收到消息并顺利处理的回调。

注意:RabbitMQ中默认的ack模式是当消息被消费者接收后,相应队列中的消息会从Ready状态转化为Unacked状态,当消息处理成功后,队列中的消息会被自动清除。 但是自动的ack有一个弊端:当接收消息的消费者端在处理消息的过程中途宕机,队列中处理的和未处理的所有消息都会被一并清除。

image-20220626142232136

配置文件
spring:
  rabbitmq:
    host: 192.168.226.130
    port: 5672
    virtual-host: /
    publisher-confirm-type: correlated   #开启消息抵达broker确认
    publisher-returns: true  #开启消息抵达队列确认
    template:
      mandatory: true  #只要消息抵达队列,以异步方式优先回调这returnConFirm
    listener:
      simple:
        acknowledge-mode: manual #手动ack消息,而不是我们之前说的自动ack
自定义配置
@Configuration
public class MyRabbitConfig {
​
    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 配置自己的消息传唤器(object->json)
     * @return
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
​
    /**
     * 定制RabbitTemplate
     * 1.服务器收到消息就回调ConfirmCallback
     *   1)publisher-confirm-type: correlated
     *   2)设置确认回调
     * 2.消息抵达队列的回调  ReturnCallback
     *   1)    publisher-returns: true  #开启消息抵达队列确认
     *         template.mandatory: true #只要消息抵达队列,以异步方式优先回调这returnConFirm
     *   2)配置
     * 3.消费端确认(保证每个消息被正常消费,此时才可以broker删除消息)  acknowledge-mode: manual #手动ack消息
     *   1)默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除消息
     *      问题:收到了对应queue中的消息,自动回复给服务器ack,但只有一个消息处理成功宕机,其他消息都会被删除丢失
     *      解决办法:手动确认(消费者货物不丢):只要我们没有明确告诉MQ货物被签收,我们就没有ack,消息一直是unacked状态,即使Consumer宕机,
     *      消息也不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他
     *   2)如何签收:channel.basicAck(deliveryTag, false);
     */
    @PostConstruct //注解解释:对象MyRabbitConfig初始化对象完成后调用这个方法,定制我们的template
    public void initRabbitTemplate(){
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * 只要消息抵达broker,就返回true
             * @param correlationData: 当前消息的唯一关联数据(消息的唯一id)
             * @param ack 消息是否成功收到
             * @param cause 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("confirm...correlationData["+correlationData+"]==>ack["+ack+"]==>cause["+cause+"]");
            }
        });
​
        //设置消息抵达队列的确认回调
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            /**
             *只有抵达失败:才回调
             * @param returnedMessage 回复的消息封装
             */
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                System.out.println(returnedMessage.toString());
            }
        });
    }
}
手动ACK

我们需要使用channel.basicAck来手动"收货"。

    @RabbitListener(queues = {"hello-java-queue"})   //监听hello-java-queue队列中的消息
    public void receiveMessage(Message message, String content, Channel channel) {
        System.out.println("接收到消息:"+message+"--->内容:"+content);
        //获取消息体
        byte[] body = message.getBody();
        //获取消息头
        MessageProperties messageProperties = message.getMessageProperties();
​
        System.out.println("消息处理完成-->"+content);
​
        //channel内自增
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
​
        //签收获取,非批量模式,手动ack
        try {
            if(deliveryTag %2 == 0) {
                //收货 
                channel.basicAck(deliveryTag, false);
                System.out.println("==签收了货物:"+deliveryTag);
            }else {
                //退货
                /**
                 * b: 是否批量退货
                 * b1: 是否返回服务器重新入队,即时生效
                 */
                channel.basicNack(deliveryTag, false, false);
                System.out.println("==没有签收货物:"+deliveryTag);
            }
​
        } catch (IOException e) {
            //网络中断
            log.error("网络中断了...");
        }
    }

其实咱们的消息队列机制,很像淘宝购物,物品经过商家传递到指定的物流公司,物流公司再通过快递员发放,快递员执行发放成功,咱们收到了货物后,就可以选择确认收货。

总结

消息队列的引入,能够在以后开发的业务中使用,以提高服务的吞吐量,提高处理性能。