RabbitMQ

1.消息中间件

消息中间件可以用于异步处理,在将消息存储到消息队列后即可返回
消息中间件可以用于应用解耦,避免服务升级时接口的改变,通过消息可以避免服务之间由于升级等所导致的影响
消息中间件可以用于流量控制,大量请求可以先存储到消息队列中,将响应结果返回给客户,后台后续对消息进行处理

2.概述

在大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力
消息服务中两个重要概念:
(1)消息代理(message broker)
(2)目的地(destination)
当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息能够传递到指定目的地
消息队列主要有两种形式的目的地:
(1)队列(queue):点对点消息通信(point-to-point)
(2)主体(topic):发布(publish)/订阅(subscribe)消息通信

  • 点对点式:
    • 消息发送者发出消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移除队列
    • 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者(即很多接收者可以接受这个队列的消息,但是只能有一个接收者收到本次发来的这条消息)
  • 发布订阅式:
    • 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息
  • JMS:(JAVA Message Server)JAVA消息服务
    • 基于JVM消息代理的规范,ActiveMQ、HornetMQ是JMS实现
  • AMQP:(Advanced Message Queuing Protocol)
    • 高级消息队列协议,也是一个消息代理的规范,兼容JMS
    • RabbitMQ是AMQP的实现

      AMQP是协议,而JMS是一个接口规范,只适用于java平台
      在Java体系中建议使用JMS,因为他是一个接口,升级无需改动代码
      而对于跨平台来说,建议使用AMQP,如同时使用C++、Java

2.1.Spring支持:

(1)spring-rabbit提供对AMQP的支持
(2)需要ConnenctionFactory的实现来连接消息代理
(3)提供JMSTemplate、RabbitTemplate来发送消息
(4)@JmsTemplate@RabbitListener在方法上监听消息代理发布的消息
(5)@EnableJms@EnableRabbit开启支持
SpringBoot自动配置:JmsAutoConfigurationRabbitAutoConfiguration开启支持

3.RabbitMQ概念

核心概念:
(1)Message:消息由消息头和消息体组成,消息体不透明,而消息头由一系列可选属性组成,如routing-key(路由键)、priority(相对于其他消息的优先级)、delivery-mode(指出该消息可能需要持久性存储)
(2)Publisher:向交换器发布消息的客户端应用程序
(3)Exchange:交换器用于接收生产者发送的消息并将这些消息路由给服务器中队列
Exchange有四种类型:direct(默认)、fanout、topic以及headers,不同类型的Exchange转发消息的策略有所区别

(4)Queue:消息队列,用来保存消息直到发给消费者,它是消息的容器,也是消息的终点,一个消息可投入一个或者多个队列,消息一直在队列里面,等待消费者连接到这个队列将其取走
(5)Binding:绑定,用于消息队列,用于消息队列和交换器之间的关联,一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成由绑定构成的路由表
Exchange和Queue的绑定可以是多对多的关系
(6)Connection:网络连接,可以一个TCP连接
(7)Channel:信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息,订阅消息,还是接收消息,这些动作都是通过信道完成,复用的好处是减少了TCP连接创建和销毁所导致的开销
(8)consumer:表示一个从消息队列中取得消息的客户端应用程序
(9)vhost:虚拟主机,表示一批交换器、消息队列和相关对象,其是共享相同的身份认证和加密环境的独立服务器域。每个vhost本质上就是一个mini版的RabbitMQ服务器,必须在连接时指定vhost,RabbitMQ默认的vhost是/

4.安装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

使用端口即可访问rabbitmq管理界面,默认账户密码均为guest

5.exchange类型

exchange分发消息时根据类型的不同,目前共四种类型:direct、fanout、topic、headers(不用)

  • direct:路由键与交换机完全匹配
  • fanout:消息会分配到所有绑定的队列上,类似于广播,不处理路由键
  • topic:将路由键和绑定键的字符串切分为单词,这些单词之间用.分割,可写两个通配符(#——表示匹配一个或者多个单词,*———表示匹配一个单词)

6.使用

导入依赖:

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

引入AMQP场景后,RabbitAutoConfiguration就会自动生效
为容器自动配置RabbitTemplateAmqpAdminCachingConnectionFactoryRabbitMessagingTemplate
所有的属性在spring.rabbitmq内配置

spring:
  rabbitmq:
    host: 192.168.88.132
    port: 5672
    virtual-host: /

(1)如何创建Exchange、Queue、Binding
使用AmqpAdmin创建

按照下面的方式可创建交换器:


按照下面的方式可创建队列:

将交换机与队列进行绑定:

(2)RabbitTemplate使用

如果发送的消息是个对象,则会使用java的序列化机制,将其写出,必须实现Serializable
也可以将发送的对象类型的消息,转换为json:


(3)监听消息:使用@RabbitListener,同时要在配置类上标注@EnableRabbit

    // queues声明需要监听的队列
    @RabbitListener(queues = {"hello-java-queue"})
    public void receive(Object message) {
        System.out.println("接收到消息...内容" + message + "==>类型" + message);
    }

也可以用下面的类型接收:

    // queues声明需要监听的队列
    @RabbitListener(queues = {"hello-java-queue"})
    public void receive(Message message, OrderReturnReasonEntity entity) {
        // message有原生消息对象(头+体),后面的参数entity为消息体的真正内容(发送写什么类型就用什么类型)
        byte[] body = message.getBody();
        // 获取消息头信息
        MessageProperties messageProperties = message.getMessageProperties();
        System.out.println("接收到消息...内容" + message + "==>类型" + entity);
    }

queue可以有很多接受者监听,只要收到消息,队列便会删除消息,而且只能有一个接收者收到消息

  • 同一个消息,只能有一个客户端收到
  • 只有一个消息处理完,方法运行结束后,才可以接收下一个消息
    @RabbitListener可以标在类或者方法上,而@RabbitHandler只能标在方法上
    @RabbitHandler可用于重载,接收不同类型的消息,如多个方法只是接收数据的参数类型不同,则可以使用该注解,而@RabbitListener主要用于标记监听哪个队列

7.RabbitMQ消息确认机制-可靠抵达


publisher: confirmCallback确认模式
publisher: returnCallback 未投递到queue退回模式
consumer: ack机制

7.1.ConfirmCallback

消息代理收到消息就会回调此方法

需要定制RabbitTemplate以使用ConfirmCallback
新版rabbitMQ配置:

spring:
  rabbitmq:
    publisher-confirm-type: CORRELATED

同时在配置类中进行定制

@Configuration
public class MyRabbitConfig {

    @Autowired
    RabbitTemplate rabbitTemplate;

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

    // PostConstruct表示MyRabbitConfig对象创建完成后再执行此方法
    @PostConstruct
    public void initRabbitTemplate() {
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            //correlationData为当前消息的唯一关联数据(消息的唯一Id)
            //ack表示下次是否成功收到,只要消息抵达代理,就返回true
            //cause表示失败原因
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("...confirm::" + correlationData + "---" + ack + "===" + cause);
            }
        });
    }
}

7.2.ReturnCallback


配置文件:

spring:
  rabbitmq:
    publisher-returns: true
    template:
      mandatory: true

publisher-returns用于开启发送端消息抵达队列的确认
template.mandatory表示为匹配的消息是否删除,为true则不会删除,并使用return
配置类:

@Configuration
public class MyRabbitConfig {

    @Autowired
    RabbitTemplate rabbitTemplate;

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

    // PostConstruct表示MyRabbitConfig对象创建完成后再执行此方法
    @PostConstruct
    public void initRabbitTemplate() {
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            // 只要消息没有正确投递到指定队列,就会触发此回调
            @Override
            public void returnedMessage(ReturnedMessage returned) {
            }
        });
    }
}

7.3.ack


消费端确认,默认自动确认,只要接收到消息,客户端会自动确认,并移除这个消息
存在问题:收到很多消息,自动回复给服务器ack,中途消费服务宕机,导致只有部分消息得到处理,但是所有消息全部丢失
未解决上述问题,可以选择手动确认的方式
配置文件:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual

此种方式,如果不进行其他处理,即使消息已经被消费,但是由于没有给与确认信息,消息一直处于unacked状态,因此队列中消息不会被删除,消息状态重新变为ready

    @RabbitListener(queues = {"hello-java-queue"})
    public void receive(Message message, OrderReturnReasonEntity entity, Channel channel) {
        // message有原生消息对象(头+体),后面的参数entity为消息体的真正内容(发送写什么类型就用什么类型)
        byte[] body = message.getBody();
        // 获取消息头信息
        MessageProperties messageProperties = message.getMessageProperties();
        // deliveryTag是channel内按顺序自增的
        long deliveryTag = messageProperties.getDeliveryTag();
        // 签收消息,第二个参数为是否批量签收,为false表示只签收当前消息
        try {
            if (deliveryTag % 2 == 0) {
                channel.basicAck(deliveryTag, false);
            } else {
                // 拒绝接受消息,使用basicNack或者basicReject均可
                // 此方法有三个参数,long deliveryTag, boolean multiple, boolean requeue
                // 第三个参数表示是否重新入队,true表示重新加入队列,false表示丢弃此消息
                channel.basicNack(deliveryTag, false, true);
                // 此方法有两个参数,long deliveryTag, boolean requeue
//                channel.basicReject(deliveryTag, true);
            }
        } catch (IOException e) {
            // 网络中断异常
            e.printStackTrace();
        }
        System.out.println("接收到消息...内容" + message + "==>类型" + entity);
    }
posted @ 2021-08-20 16:18  kanaliya  阅读(72)  评论(0)    收藏  举报