[笔记迁移][Spring Boot进阶]消息中间件[9]

15 篇文章 0 订阅

1. 消息队列概述

1.1 消息队列作用与应用场景
  • 异步通信(用户注册场景:信息入库与邮件收发、短信收发不需要同步)
  • 应用解耦(订单库存场景:连接两个微服务,发布-订阅)
  • 流量削峰(秒杀场景:请求在定长消息队列中“抢座位”,未能入队的请求被快速响应秒杀失败,入队的消息等待被其他业务获取处理 )
1.2 消息服务中两个重要的概念

当发送者发送消息后,将由消息代理接管,消息代理来保证消息传递到指定目的地。

  1. 消息代理(Message Broker):消息中间件所在的服务器

  2. 目的地(Destination)。主要有两种形式
    (1)队列(queue):用于点对点(point-to-point)的消息通信。

    消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从这个队列中获取消息内容。一旦消息被拿取后,它将被移出这个队列。
    消息只有唯一的发送者和接受者,但并不是说只能由一个接收者。也就是说,一个消息队列可以被多个实体接收,但一旦某个实体确认接受一条消息(消费),那其他实体就不能接受,这也就确保了消息被唯一处理。

    (2)主题(topic):用于发布-订阅(pub-sub)的消息通信。

    发送者(pub)发送消息到一个主题(topic),多个接收者(sub)监听这个主题,那么就会在一条消息到达的同时都收到这条消息,这条消息可以被多次消费,类似社交媒体的博主与粉丝的关系。

1.3 消息服务的两个规范
  1. JMS(Java Message Service)
    基于JVM的消息代理规范。ActiveMQ、HornetMQ是JMS的实现。
  2. AMQP(Advanced Message Queuing Protocol)
    高级消息队列协议,兼容JMS。RabbitMQ是AMQP的实现。
  3. 对比
    \JMSAMQP
    定义Java规定的api网络wire-level协议
    跨语言否,只能由Java语言编写
    跨平台否,只能运行在JVM上
    消息模型2种:“点对点”(Peer-2-Peer)、“发布-订阅”(Pub-Sub)5种:
    (1) direct exchange
    (2) fanout exchange
    (3) topic exchange
    (4) headers exchange
    (5) system exchange

    direct-exchange就是“点对点”模型,而后四种在本质上和JMS实现中的pub-sub模型没有太大区别,仅是在路由机制上做了更详细的划分
    支持的消息类型多种消息型:
    TextMessage
    MapMessage
    BytesMessage
    StreamMessage
    ObjectMessage
    Message(只有消息头和属性)
    byte[],通用的字节数组
    优劣JMS定义了Java API层面的标准,因此在Java生态中,多个应用客户端均可以通过面向JMS接口的方式进行信息交互,不需要应用修改代码,但其对跨平台的支持较差AKQP定义了wire-level层的协议标准,正因为这种概念抽象,天然具有跨平台、跨语言的特性。
  4. Spring对这两个规范都是支持的:
    (1)spring-jms提供了对JMS的支持(Spring Boot的spring-boot-starter-activemq/artemis等);
    (2)spring-rabbit提供了对AMQP(特别是RabbitMQ)的支持(Spring Boot的spring-boot-starter-amqp);
    (3)提供JmsTemplate、RabbitTemplate来发送消息;
    (4)@JmsListener、@RabbitListener标注在方法上监听消息代理发布的消息;
    (5)@EnableJms、@EnableRabbit开启注解支持
    (6)Spring Boot对它们的自动配置支持:JmsAutoConfiguration、RabbitAutoConfiguration。

2. RabbitMQ(AMQP的开源实现)

2.1 核心概念
  1. Message
    消息,消息是不具名的,它由消息头和消息体两部分组成。消息体即发送的内容,在传输过程中不可见;而消息头则是由一系列的可选属性组成,包括routing-key(路由键,发给谁?)、priority(相较于其他消息的优先权)、delivery-mode(该消息可能需要持久化存储)等。

  2. Publisher
    消息的生产者,是一个向交换器(Exchange)发布消息的客户端应用。

  3. Exchange
    交换器,用来接收生产者发送的消息并将这些消息路由(根据消息头中的routing-key)给服务器中的队列。有四种类型:direct(默认)、fanout、topic、headers。direct相当JMS中的“点对点”模型,而后面三种相当于JMS中的“发布-订阅”模型。

  4. Queue
    消息队列,是消息的容器,也是消息最终目的地。一个消息可以投入一个或多个队列。消息一直保持在队列里面,等待消费者连接到这个队列将其取走后会从这个队列中移除。

  5. Binding
    绑定,描述消息队列和绑定之间的关联。一个绑定就是基于rounting-key将交换器和消息队列连接起来的路由规则。交换器和消息队列的绑定是多对多的,也就是说,一个交换器可以关联多个消息队列,而一个消息队列也可以关联给多个交换器(不管消息从绑定的哪个交换器被转发,都会来到这个消息队列)

  6. Connection
    网络连接,比如一条TCP连接。

  7. Channel
    信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实TCP连接内的虚拟连接,不管是发送消息、订阅队列还是接收消息的行为,实际上都是通过信道完成。(因为对于操作系统来说,建立和销毁TCP连接都是非常耗费资源的,不可能每次与消息队列交互都新建TCP连接)。

  8. Consumer
    消息的消费者,表示一个从消息队列中拿取消息的客户端应用。

  9. Virtual Host(简称vhost)
    虚拟主机,是共享相同身份认证和加密环境的独立服务器域。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定(路由规则)和权限机制。必须在连接时指定,RabbitMQ默认的vhost是/(可以看出,RabbitMQ使用路径的方式划分vhost)。

  10. Broker
    表示消息队列服务器的实体主机。

RabbitMQ
上述概念交互的基本过程大致如下:
(1)生产者Publisher产出消息Message后发送给消息代理(RabbitMQ服务器)Broker里的一个虚拟主机vhost。
(2)虚拟主机把消息交给合适的交换器Exchange,交换器根据消息中的rounting-key判断将消息置入哪一个或哪几个绑定Binding的消息队列Queue中。
(3)消费者Consumer与对应的消息队列建立TCP连接Connection,为了省资源,在这条TCP连接里开辟多个信道Channel,从消息队列中拿出消息到消费者本地。

2.2 运行机制——消息路由
  1. 相较于JMS,AMQP在路由转发中增加了两个核心角色——Exchange和Binding。 Exchange的不同、Binding的不同都会导致消息路由转发的结果不同。
    AMQPProcess

  2. 根据Exchange类型的不同,消息被路由转发的策略不同,目前共有四种类型:direct、fanout、topic和headers。

    其中,headers Exchange与direct Exchange功能逻辑完全一致,但是根据消息的头部信息而不是routing-key进行路由转发,性能差很多,几乎使用不到了。

    (1)Direct Exchange
    DirectExchange
    如果消息中的routing-key和Binding时所规定的routing-key 完全匹配(一模一样),Direct Exchange就将消息置入对应的消息队列中。它是完全匹配、单播(点对点)的模式。

    例如一个“队列-Direct交换器”绑定要求rounting-key为“dog”,则Direct交换器只会转发routing key=“dog”的消息到这个关联的队列中,不会把这条消息转发到“dog.puppy”、“dog.guard”等关联的队列中(有一点对不上都不行)。

    (2)Fanout Exchange
    FanoutExchange
    每个发送到Fanout Exchange上的消息都会被路由转发到其绑定的所有队列中去,不管routing-key是什么。因此,Fanout Exchange转发消息是最快的。它是无条件的“广播-订阅”模式。

    (3)Topic Exchange
    TopicExchange
    Topic Exchange的消息队列需要绑定到一个模式(Binding时,routing-key指定为一个模式串)上,根据消息的routing-key和这些模式串进行匹配的结果,有选择地将这条消息路由转发给哪个或几个消息队列。消息的routing-key和绑定时的routing-key的字符串由多个单词组成,单词之间用“.”分隔。它是有条件的“广播-订阅”模式。

    模式匹配中可以识别的两个通配符:符号“#”表示匹配0个或多个单词;符号“*”表示匹配1个单词。(注意单位是单词)

    同一个Topic Exchange下,所关联队列的模式串可以相同,比如QueueA、QueueB可以使用同一模式串 #.news,那么routing-key为x.news,cctv.news,java.news的消息都会被转发到QueueA和QueueB中保存。

3. 整合RabbitMQ到项目中使用

3.1 搭建RabbitMQ环境
  1. 使用docker下载RabbitMQ镜像(安装Tag中以management结尾的版本,这些版本自带web管理界面)
    #下载镜像
    docker pull registry.docker-cn.com/library/rabbitmq:3-management
    #检查
    docker images
    
  2. 安装启动镜像
    #自带web管理界面的版本需要配置两个端口:
    #5672是客户端与RabbitMQ服务器的通信端口
    #15672是访问web管理界面的端口,登录的默认账号密码都是guest
    docker run -d -p 5672:5672 -p 15672:15672 --name myrabbitmq registry.docker-cn.com/library/rabbitmq:3-management
    
    #检查 
    docker ps
    
  3. 通过Web管理界面的方式创建交换器Exchange、队列Queue并绑定
    (1)创建实验的Exchanges(durable表示持久化,设为durable时,重启RabbitMQ服务器,该Exchange仍然存在)、Queues。
    (2)将Queues绑定到相应的Exchanges上:
    在Exchange选项卡内,点击某个Exchange的名称,来到其详细信息页,找到Bindings一节,注意绑定的routing-key设置。如果想要取消已绑定的某队列与该Exchange的绑定,点击对应的Unbind。
    (3)逐个给这些Exchanges发送一些消息,测试与其绑定的哪个队列能收到消息:
    在Exchange选项卡内,点击某个Exchange的名称,来到其详细信息页,找到Publish messages一节,还是注意routing-key要设置正确。输入消息点击Publish发送后,来到Queues选项卡中,点击某个Queue的名称,来到其详细页,找到Get messages一节获取之前所发消息(如果每次点击Get Message都是第一条消息,就把应答模式Ack Mode改为Ack message requeue false,之后再点击Get Message就可以从队列中依次取出消息直至为空)。
    【强调】在搭建环境时,Exchange和Queues的名字真的不重要(之后在客户端收发数据时需要使用到这些名字),能正确存取的关键是Exchange的类型,以及Queue绑定Exchange时指定的routing-key和实际消息的routing-key是否能满足该类型Exchange的策略。
3.2 整合RabbitMQ到Spring Boot中使用
  1. 使用Spring Initializer创建Spring Boot工程,选中Spring for RabbitMQ(Messaging)、Web模块(用来测试)。工程的pom.xml中自动引入了spring-boot-starter-amqp,由这个stater引入了spring-messaging(Spring与消息整合模块)和spring-rabbit(Spring对RabbitMQ操作模块)。

  2. 在主配置文件中添加spring.rabbitmq开头的相关配置

    #若不写,默认是localhost,认为RabbitMQ在本机
    spring.rabbitmq.host=xxx.xxx.xxx.xxx
    #spring.rabbitmq.port=5672 端口默认是5672,可以不写 
    spring.rabbitmq.username=guest
    spring.rabbitmq.password=guest
    #spring.rabbitmq.virtual-host=/ vhost默认是/,可以不写
    
  3. 在测试类中注入RabbitTemplate并使用它来发送消息。(不管何种模式,关键将交换器和队列绑定的routing-key设置好,之后只要发送消息给对应的交换器,由它根据规则把消息转发给队列即可,不需要复杂编码)

    /*
     * 【序列化消息默认使用jdk的序列化机制(content_type:application/x-java-serialized-object)】
     * 1. 单播(点对点模式)
     *  (1)rabbitTemplate.send(exchange,routingkey,message);
     *  	   message需要自己构造Message对象。消息体是序列化后的字节数组,消息头是MessageProperties的封装。
     *  	   public Message(byte[] body, MessageProperties messageProperties)
     *
     *   (2)日常使用rabbitTemplate.convertAndSend(exchange,routingkey,object);
     *   	   只需要传入要发送的数据对象object,它被自动序列化为字节数组,默认作为消息体。
     */
    //发送给direct类型的交换器(exchange.direct是之前搭建环境时,direct类型交换器的名字),它负责点对点模式的路由转发
    @Test
    public void p2pSend() {
    	Map<String,Object> map = new HashMap<>();
    	map.put("msg","This is the first message with direct exchange");
    	map.put("data", Arrays.asList("hello",123,false));
    	rabbitTemplate.convertAndSend("exchange.direct","me.news",map);
    
    }
    
    //2. 广播(发布-订阅模式)
    //routing-key根本用不到,直接不用指定
    @Test
    public void broadcastSend(){
    	rabbitTemplate.convertAndSend("exchange.fanout","",new Date());
    }
    
  4. 在测试类中注入RabbitTemplate并使用它从队列中取出消息。

    @Test
    public void receive(){
    	//rabbitTemplate.receive(queueName); 返回的是queueName指定队列中的一个Message对象,包含了消息体和消息头。
    	//rabbitTmplate.receiveAndConvert(queueName);返回的是queueName指定队列的一条消息中的被反序列化后的消息体。
    	Object o = rabbitTemplate.receiveAndConvert("me.news");
    	System.out.println(o.getClass());
    	System.out.println(o);
    
    }
    
  5. 如何将消息体自动序列化为JSON并发送?
    (1)默认使用jdk序列化机制的原因:RabbitMQ底层核心调用private volatile MessageConverter messageConverter = new SimpleMessageConverter(); 而它序列化消息体时,所用的就是字节流。
    (2)在配置类@Configuration中注入Jackson2JsonMessageConverter替换默认的MessageConverter(注意是org.springframework.amqp.support.converter.MessageConverter)。
    CandidateMessageConverter

    @Configuration
    public class MyAmqpConfig {
    
    	//在自动配置类注入RabbitTemplate时被设置进去
    	//【content_type:application/json】同时会记录封装消息体的JavaBean类型,用于正确的反序列化
        @Bean
        public MessageConverter messageConverter(){
            return new Jackson2JsonMessageConverter();
        }
        
    }
    
  6. 监听队列并自动获取数据的使用,参照第5小姐。

  7. 在工程中通过代码的方式创建交换器Exchange、队列Queue并绑定,参照第6小节。

4. RabbitMQ自动配置原理(RabbitAutoConfiguration+RabbitProperties)

  1. RabbitAutoConfiguration 自动配置了以下组件:
    (1)ConnectionFactory,通过它来获取与RabbitMQ的TCP连接,而连接信息由RabbitProperties提供。
    (2)RabbitTemplate,通过它向RabbitMQ发送和拿取消息。
    (3)AmqpAdmin,它是RabbitMQ系统管理功能组件,通过它可以创建和删除交换器Exchange、队列Queue以及它们间的绑定规则Binding。

       //1.ConnectionFactory
       @Bean
       public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties properties, ObjectProvider<ConnectionNameStrategy> connectionNameStrategy) throws Exception {
           PropertyMapper map = PropertyMapper.get();
           CachingConnectionFactory factory = new CachingConnectionFactory((com.rabbitmq.client.ConnectionFactory)this.getRabbitConnectionFactoryBean(properties).getObject());
           properties.getClass();
           map.from(properties::determineAddresses).to(factory::setAddresses);
           properties.getClass();
           map.from(properties::isPublisherReturns).to(factory::setPublisherReturns);
           properties.getClass();
           map.from(properties::getPublisherConfirmType).whenNonNull().to(factory::setPublisherConfirmType);
           org.springframework.boot.autoconfigure.amqp.RabbitProperties.Cache.Channel channel = properties.getCache().getChannel();
           channel.getClass();
           map.from(channel::getSize).whenNonNull().to(factory::setChannelCacheSize);
           channel.getClass();
           map.from(channel::getCheckoutTimeout).whenNonNull().as(Duration::toMillis).to(factory::setChannelCheckoutTimeout);
           Connection connection = properties.getCache().getConnection();
           connection.getClass();
           map.from(connection::getMode).whenNonNull().to(factory::setCacheMode);
           connection.getClass();
           map.from(connection::getSize).whenNonNull().to(factory::setConnectionCacheSize);
           connectionNameStrategy.getClass();
           map.from(connectionNameStrategy::getIfUnique).whenNonNull().to(factory::setConnectionNameStrategy);
           return factory;
       }
    
    	//2. RabbitTemplate
        @Bean
        @ConditionalOnSingleCandidate(ConnectionFactory.class)
        @ConditionalOnMissingBean({RabbitOperations.class})
        public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) {
            RabbitTemplate template = new RabbitTemplate();
            configurer.configure(template, connectionFactory);
            return template;
        }
    
    	//3. AmqpAdmin
    	@Bean
        @ConditionalOnSingleCandidate(ConnectionFactory.class)
        @ConditionalOnProperty(
            prefix = "spring.rabbitmq",
            name = {"dynamic"},
            matchIfMissing = true
        )
        @ConditionalOnMissingBean
        public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) {
            return new RabbitAdmin(connectionFactory);
        }
    }
    
  2. RabbitProperties,与主配置文件中spring.rabbitmq开头的配置映射绑定。

    @ConfigurationProperties(
        prefix = "spring.rabbitmq"
    )
    public class RabbitProperties {
        private static final int DEFAULT_PORT = 5672;
        private static final int DEFAULT_PORT_SECURE = 5671;
        private String host = "localhost";
        private Integer port;
        private String username = "guest";
        private String password = "guest";
        private final RabbitProperties.Ssl ssl = new RabbitProperties.Ssl();
        private String virtualHost;
        private String addresses;
        @DurationUnit(ChronoUnit.SECONDS)
        private Duration requestedHeartbeat;
        private int requestedChannelMax = 2047;
        private boolean publisherReturns;
        private ConfirmType publisherConfirmType;
        private Duration connectionTimeout;
        private final RabbitProperties.Cache cache = new RabbitProperties.Cache();
        private final RabbitProperties.Listener listener = new RabbitProperties.Listener();
        private final RabbitProperties.Template template = new RabbitProperties.Template();
        private List<RabbitProperties.Address> parsedAddresses;
        //... ...
    }
    

5. 监听队列,自动获取消息——@EnableRabbit+@RabbitListener

  1. 场景举例:订单服务与库存服务解耦,通过消息中间件进行交互。用户下单后发送单据消息给消息中间件中的某一个队列,库存服务实时监听同一个队列,一旦有新的单据消息被置入,库存服务就会取走处理。
  2. 通过注解实时监听RabbitMQ的某一队列
    (1)在主配置类上标注@EnableRabbit,表示启用RabbitMQ监听注解。
    (2)给处理监听的方法上标注@RabbitListener,反序列化绑定的参数有两种类型:消息的确切类型(如Employee/Book/Date等)或Message(amqp.core包下的,包含了消息头和消息体)。
    //me.news队列中已经放入了关于Book的消息
    @Service
    public class BookService {
    
        //监听me.news队列,获取其中的消息,封装绑定到book参数。只要消息在me.news队列,这个方法就会被调用。
        //属性queues是String[],可以指定多个监听的队列名字。
        @RabbitListener(queues = "me.news")
        public void receiveAcutalBook(Book book){
            System.out.println(book);
        }
        //监听me.news队列,获取其中的消息,封装绑定到book参数。只要消息在me.news队列,这个方法就会被调用。
        //拿到Message对象,可以获取消息体body和消息头messageProperties
        @RabbitListener(queues = "me.news")
        public void receiveOriginMessage(Message msg){
            System.out.println(msg.getBody());
            System.out.println(msg.getMessageProperties());
        }
    
    }
    

6. 通过代码的方式创建交换器Exchange、队列Queue并绑定——AmqpAdmin

  1. AmqpAdmin组件已经被RabbitAutoConfiguration注入在容器中,只要使用@Autowired注入便可使用。
  2. 在方法调用上有一个模式:所有declareXXX的方法都是用来创建xxx的,所有deleteXXX/removeXXX都是用来删除xxx的。(xxx可以是交换器Exchange,队列Queue,它们的绑定关系Binding。)
  3. 实例
    @Autowired
    private AmqpAdmin adqpAdmin;
    
    @Test
    public void amqpAdminTest(){
    	/*
    	 * 1. 创建Exchange,new一个实现类
    	 * Exchange是amqp.core包下的接口,它有几个实现类:
    	 * - DirectExchange
    	 * - FanoutExchange
    	 * - TopicExchange
    	 * - HeadersExchange
    	 * - CustomExchange
    	 **/	
    	amqpAdmin.declareExchange(new DirectExchange("amqpadmin.direct.exchange"));
    	/*
    	 * 2. 创建Queue
    	 * Queue是amqp.core包下的类,直接new Queue
    	 * 如果调用没有参数传入的declareQueue,RabbitMQ将会随机给队列起一个名字。
    	 **/
    	amqpAdmin.declareQueue(new Queue("amqpadmin.direct.queue"));
    	/*
    	 * 3. 创建Binding
    	 * Binding是amqp.core包下的类,直接new Binding(destination,destinationType,exchange,routingKey,arguments)
    	 * destination:目的地的名字,通常是绑定哪个queue的名字。
    	 * destinationType:目的地类型(队列还是交换器?),枚举类型。
    	 * exchange:绑定到的exchange名字。
    	 * routingkey:路由转发到这个队列的路由键。
    	 * arguments:绑定时使用的额外参数。
    	 **/
    	amqpAdmin.declareBinding(new Binding("amqpadmin.direct.queue", 
    			Binding.DestinationType.QUEUE,
    			"amqpadmin.direct.exchange",
    			"adqpadmin.abc",
    			null));
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值