2020:0705--16--SpringBoot与消息

315 阅读13分钟

主要内容

    JMS规范:JAVA消息服务
    基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现
    
    AMQP规范:高级消息队列协议
    高级消息队列协议,也是一个消息代理的规范,兼容JMS
    RabbitMQ是AMQP的实现
    
    RabbitMQ:是AMQP的实现。
    
    1.两者对比:

    2.Spring底层对两者都是支持的。
    
        spring-jms提供了对JMS的支持
        spring-rabbit提供了对AMQP的支持
        需要ConnectionFactory的实现来连接消息代理
        提供JmsTemplate、RabbitTemplate来发送消息
        @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息
        @EnableJms、@EnableRabbit开启支持
        
    3.Spring Boot自动配置
        
        JmsAutoConfiguration
        RabbitAutoConfiguration

1. 概述

1.  大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力

2.  消息服务中两个重要概念:

    消息代理(message broker):
        消息中间件的服务器,要想向消息队列中发送内容,就要连接消息代理(消息中间件服务器)
        消息发送至是将消息发送给消息中间价的服务器,再由消息中间价的服务器发送到指定的目的地。
        
    目的地(destination)
        队列:点对点
        主题:发布/订阅
    
    关系:
    当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
    
3.  消息队列主要有两种形式的目的地
    
    1.队列(queue):点对点消息通信(point-to-point)
    
        消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,
        消息读取后被移出队列。
        
        消息只有唯一的发送者和接受者,但并不是说只能有一个接收者。
        多个接收者时:谁抢到,就是谁的,抢到后队列中就没了。
        
    2.主题(topic):发布(publish)/订阅(subscribe)消息通信
    
        发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么
        就会在消息到达时同时收到消息。


4.  应用场景    
    应用场景:提升异步通信能力。
    关键字:异步读取。

    应用场景:解耦
    引用消息队列,来传递数据。达到解耦两个微服务的目的。

    应用场景:秒杀/流量削峰
    将消息队列设定为10000,那么前10000名能进消息队列,秒杀成功。其他的直接抛弃请求。

2. 安装RabbiMQ

2.1 RabbitMQ简介

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

2.  核心概念

    1.  Message
    消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,
    这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、
    delivery-mode(指出该消息可能需要持久性存储)等。
    
    2.  Publisher
    消息的生产者,也是一个向交换器发布消息的客户端应用程序。
    
    3.  Exchange
    是消息中间件服务器的一个组件,其上可能绑定这许多消息队列,消息分发给哪个队列,由路由键决定的。
    
    交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。

    Exchange有4种类型:direct(默认),fanout, topic, 和headers,
    不同类型的Exchange转发消息的策略有所区别。
    
    4.  Queue
    Queue消息队列,用来保存消息直到发送给消费者。
    它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,
    等待消费者连接到这个队列将其取走。
    
    5.  Binding
    绑定。
    消息发给消息服务器后,由其中交换器派分给消息队列。那么就意味着,消息队列和交换器存在着
    一种绑定规则。
    每个路由键就对应着一个这种规则,即基于路由键将交换器和消息队列连接起来的路由规则,这种规则也可以称为一个绑定。
    
    所以可以将交换器理解成一个由绑定构成的路由表。
    
    Exchange 和Queue的绑定可以是多对多的关系。
    
    
    6.  Connection
    网络连接,比如一个TCP连接。
    
    7.  Channel
    如果每次去消息队列中取一个消息,都建立一条TCP连接的话。这是非常耗费资源的。
    所以只建立一条连接,从消息队列取数据时再建立多个信道,以便复用这个TCP连接。
    
    信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,
    AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。
    因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP 连接。
    
    8.  Consumer
    消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
    
    9.  Virtual Host:vhost
    
    可以将消息中间件服务器划分成若干个虚拟主机,每个虚拟主机可以理解成是一个mini的RabbitMQ服务器。
    他们是可以独立运行的,而且隔离。
    虚拟主机A: /A
    虚拟主机B: /B
    虚拟主机C: /C
    
    虚拟主机,表示一批交换器、消息队列和相关对象。
    虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost 本质上就是一个mini 版的RabbitMQ 
    服务器,拥有自己的队列、交换器、绑定和权限机制。
    
    vhost 是AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的vhost 是/ 。
    
    10.     Broker
    表示消息队列服务器实体

2.2 RabbitMQ的运行机制

    一个消息是如何从生产者到达消费者呢?
    
    AMQP 中消息的路由过程和Java 开发者熟悉的JMS 存在一些差别,AMQP 中增加了Exchange和Binding的角色。
    生产者把消息发布到Exchange 上,消息最终到达队列并被消费者接收,
    而Binding 决定交换器的消息应该发送到那个队列。
    
    核心是交换器和绑定规则:有不同的分发策略。
    
    1.  Exchange类型
        Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:
        direct、fanout、topic、headers。
        
        headers 匹配AMQP 消息的header 而不是路由键,headers 交换器和direct 交换器完全一致,
        但性能差很多,目前几乎用不到了,所以直接看另外三种类型:
        
        1.  Direct Exchange:单播模式
            消息中的路由键(routing key)和Binding 中的binding key 一致时。
            交换器就将消息发到对应的队列中。
            
            是否可以理解为:
            消息里面有一个键:routing key
            消息队列一个键:binding key
            如果是Direct Exchange:那么就只要一对一的分发。

        2.  Fanout Exchange:类似广播模式
        
            每个发到fanout 类型交换器的消息都会分到所有绑定的队列上去,fanout 交换器不处理路由键。
            fanout 类型转发消息是最快的。

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

2.3 RabbitMQ镜像

    1.  docker pull rabbitmq:3-management
    
        pull带management标签的RabbitMQ镜像,有带web的管理界面。
        
    2.  启动镜像
    
        docker run -d -p 5672:5672 -p15672:15672 --name myRabbitMQ 43f79d83563f

        注意:代管理界面的RabbitMQ,有两个端口。
        
            把本地主机的5672映射到docker容器的5672:客户端和RabbitMQ进行通信的端口。
            
            把本地主机的15672映射到docker容器的15672:管理界面,访问web页面的端口。
            
    
    3.  访问测试一下

        默认登录账号密码:
            guest
            guest

3. 整合RabbitMQ

3.1 创建交换器,消息队列和绑定消息。

    1.  创建交换器

    1.1.  exchange.direct
    
        持久化durable:下一次重启RabbitMQ该交换器还在。

    1.2.  exchange.fanout

    1.3.  exchange.topic

    2.  添加消息队列

        atguigu
        atguigu.news
        atguigu.emps
        gulixueyuan.news

    3.  将消息队列和exchange.direct交换器进行绑定
    
        3.1 进入绑定操作页面   

        3.2 绑定第一个队列:路由键atguigu

        3.3 绑定第二个队列:路由键atguigu.news

        3.4 绑定第三个队列:路由键atguigu.emps

        3.5 绑定第四个队列:路由键gulixueyuan.news

    4.  将消息队列和exchange.fanout交换器进行绑定

    5.  将消息队列和exchange.topic交换器进行绑定
        
        #:匹配0或多个单词
        *:匹配一个单词
        
        atguigu队列的路由键:atguigu.#
        atguigu.news队列的路由键:atguigu.#
        atguigu.emps队列的路由键:atguigu.#
        
        atguigu.news队列的路由键:*.news
        gulixueyuan.news队列的路由键:*.news

    6.  解绑

3.2 测试发送一个消息

    1.  给exchange.direct发送消息

    2.  查看发送的消息

    3.  给exchange.fanout发送消息

    4.  查看发送的消息
        
        所有的消息队列都收到消息。

    5.  给exchange.topic发送消息
    
    给exchange.topic发送消息:路由键是atguigu.news 

    能匹配路由键是:atguigu.#  *.news的队列

    给exchange.topic发送消息:路由键是给hello.news 
    
    能匹配路由键是:*.news的队列

    查看一下:
    选择Ack message requeue false:获取一个删除一个。不然只会显示一个消息。

4. 整合RabbitMQ

4.1 RabbitMQzi自动配置原理

    1.  创建一个项目

    2.  看一下RabbitMQ给我们自动配置了哪些组件?
    
        打开RabbitMQ的自动配置类:RabbitAutoConfiguration
        
        1.  连接工厂:能获取和RabbitMQ的连接
        
            CachingConnectionFactory
            
        2.  连接工厂的连接信息从RabbitProperties获得

        3.  点进RabbitProperties
            
            RabbitProperties封装了RabbitMQ的配置。
            其中的属性和主配置文件中前缀spring.rabbitmq的配置绑定。

        4.  配置连接属性
            spring:
              rabbitmq:
                host: 192.168.92.130
                username: guest
                password: guest
                #port: 5672 连接rabbitmq的端口默认是5672,管理界面访问web页面的端口15672。
                #virtual-host: "/" 虚拟主机不写的话默认是 "/"
        5.  RabbitMQ给我们自动配置RabbitTemplate
            
            给RabbitMQ发送和接收消息的。
            
        6.  RabbitMQ给我们自动配置AmqpAdmin
            
            是RabbitMQ的系统管理功能组件

4.2 测试:向exchange.direct发送一个消息(代码实现)

    1.  application.yum配置    
        spring:
          rabbitmq:
            host: 192.168.92.130
            username: guest
            password: guest
            #port: 5672 连接rabbitmq的端口默认是5672,管理界面访问web页面的端口15672。
            #virtual-host: "/" 虚拟主机不写的话默认是 "/"
    2.  测试发送消息方法
@SpringBootTest
class SpringbootAdvanced02AmqpApplicationTests {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() {
        /**
         *     rabbitTemplate.send(exchange, routeKey, message);
         *
         *     message需要自己构造:
         *     可以定制消息体和消息头
         *
         *     public Message(byte[] body, MessageProperties messageProperties) {
         *         this.body = body;
         *         this.messageProperties = messageProperties;
         *     }
         */

        //object默认被当做消息体,只需要传入要发送的消息对象,会自动序列化发送给rabbitMQ
        //rabbitTemplate.convertAndSend(exchange, routeKey, object);

        /**
         * 使用我们创建的exchange.direct
         * 使用atguigu.news路由键
         * 要发送的内容:map
         */
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("msg", "这是第一个消息");
        map.put("data", Arrays.asList("hello", 123, true));
        rabbitTemplate.convertAndSend("exchange.direct", "atguigu.news", map);
    }

}
    3.  查看结果
        
        测试成功,用JDK的序列化方式将map发送过来。

4.3 测试:从atguigu.news队列中接受/获取一个消息(代码实现)

    测试接收/获取数据:消息队列中的数据一被接收就没有了
    /**
     * 测试接收/获取数据:消息队列中的数据一被接收就没有了
     */
    @Test
    public void receive(){
        Object o = rabbitTemplate.receiveAndConvert("atguigu.news");
        System.out.println(o.getClass());
        System.out.println(o);
    }

    消息队列中的数据一被接收就不存在了。

4.4 如何将数据转成JSON再发出去呢?

    为什么之前是按照JDK的序列化转换对象?
    
    RabbitTemplate中有一个消息转换器MessageConverter
    MessageConverter是一个org.springframework.amqp.support.converter接口
    
    这个转换器是一个SimpleMessageConverter,他默认是用JDK的序列化工具,进行序列化。
    private MessageConverter messageConverter = new SimpleMessageConverter();
    
    所以我们可以自己写一个配置

    分析一下:
        
        自动配置类在注入RabbitTemplate时,调用了configure方法,其中进行了messageConverter的配置。
        
        如果没有messageConverter就用它默认的,如果有就用自定义的。
        @Configuration
        public class MyAMQPConfig {
            
            @Bean
            public MessageConverter messageConver(){
                return new Jackson2JsonMessageConverter();
            }
        }
    所以不用在进行其他配置,就可以使用该messageConver。
    
    
    1. 结果成功的序列化成了JSON。

4.5. 测试广播

    /**
     * 测试广播
     */
    @Test
    public void sendMsg(){
        rabbitTemplate.convertAndSend("exchange.fanout", "", new Book("三国演义","罗贯中"));
    }

5. RabbitMQ监听方法

    0.  注意,获取到的消息要封装book实体类,实体类必须有空参
        public class Book {
        
            private String name;
            private String author;
        
            public Book() {
            }
        
            public Book(String name, String author) {
                this.name = name;
                this.author = author;
            }
        
            public String getName() {
                return name;
            }
        
            public void setName(String name) {
                this.name = name;
            }
        
            public String getAuthor() {
                return author;
            }
        
            public void setAuthor(String author) {
                this.author = author;
            }
        
            @Override
            public String toString() {
                return "Book{" +
                        "name='" + name + '\'' +
                        ", author='" + author + '\'' +
                        '}';
            }
        }
    1.  这个监听的方法,会将消息队列中的消息取走。
    /**
     *  注意类上加上@Service注解
     * 监听来自消息队列中book相关的内容
     */
    @Service
    public class BookService {
    
        /**
         * 监听消息队列以后调用的。
         * 
         * 要让这个@RabbitListener起作用,要开启基于注解的RabbitMQ模式。
         * @param book
         */
        @RabbitListener(queues = "atguigu.news")
        
        //会将消息中的内容封装成一个book对象,可能都是有注解实现的
        public void receive(Book book){
            //这要这个消息队列中有内容,这个方法就会被调用。
            System.out.println("收到消息: "+book);
            
        }
    }
    1.1 要让这个@RabbitListener起作用,要开启基于注解的RabbitMQ模式。
    //要让这个@RabbitListener注解起作用,要开启基于注解的RabbitMQ模式。
    
    @SpringBootApplication
    @EnableRabbit //开启基于注解的RabbitMQ
    public class SpringbootAdvanced02AmqpApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringbootAdvanced02AmqpApplication.class, args);
        }
    
    }

    2.  监听消息头,消息体
        @RabbitListener(queues = "atguigu")
        public void receive02(Message message){
    
            System.out.println(message.getBody());
            //消息头
            System.out.println(message.getMessageProperties());
        }
    3.  测试一下
        启动主入口方法
        
        在启动测试方法:用一个关播交换器帮我们给所有消息队列发送消息。
        
        看RabbitListener能不能监听到queues = "atguigu.news"中的添加消息
        /**
         * 测试广播
         */
        @Test
        public void sendMsg(){
            rabbitTemplate.convertAndSend("exchange.fanout", "", new Book("红楼梦","曹雪芹"));
        }

6. AmqpAdmin管理组件的使用

    我们前面在RabbitMQ的web页面已经创建好了Exchange和Queue。
    
    如果我们需要在程序中创建怎么做呢?
    
    我们用程序临时的创建一些Exchange,Quere,Binding等,由AmqpAdmin管理来实现。
    他帮我们创建和删除Exchange,Quere,Binding。
    
    RabbitAutoConfiguration自动给我们配置了这个类,所以我们只要注入使用即可。

1.  测试一下创建Exchange
    Exchange是一个接口:有direct/fanout/topic等实现

    @Autowired
    AmqpAdmin amqpAdmin;

    @Test
    public void creatExchange(){
        //Exchange是一个接口:有direct/fanout/topic等实现
        amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
        System.out.println("创建完成");
    }

2.  创建Queue
    @Test
    public void creatQueue(){
        //Queue是一个对象
        amqpAdmin.declareQueue(new Queue("amqpadmin.queue2", true));
    }

3.  测试用amqpadmin进行绑定
    /**
     * 测试用amqpadmin进行绑定
     */
    @Test
    public void binding(){
        amqpAdmin.declareBinding(new Binding("amqpadmin.queue2", Binding.DestinationType.QUEUE,
                "amqpadmin.exchange", "amqp.haha", null));
    }

测试成功。