1. 消息队列概述
1.1 消息队列作用与应用场景
- 异步通信(用户注册场景:信息入库与邮件收发、短信收发不需要同步)
- 应用解耦(订单库存场景:连接两个微服务,发布-订阅)
- 流量削峰(秒杀场景:请求在定长消息队列中“抢座位”,未能入队的请求被快速响应秒杀失败,入队的消息等待被其他业务获取处理 )
1.2 消息服务中两个重要的概念
当发送者发送消息后,将由消息代理接管,消息代理来保证消息传递到指定目的地。
-
消息代理(Message Broker):消息中间件所在的服务器
-
目的地(Destination)。主要有两种形式
(1)队列(queue):用于点对点(point-to-point)的消息通信。消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从这个队列中获取消息内容。一旦消息被拿取后,它将被移出这个队列。
消息只有唯一的发送者和接受者,但并不是说只能由一个接收者。也就是说,一个消息队列可以被多个实体接收,但一旦某个实体确认接受一条消息(消费),那其他实体就不能接受,这也就确保了消息被唯一处理。(2)主题(topic):用于发布-订阅(pub-sub)的消息通信。
发送者(pub)发送消息到一个主题(topic),多个接收者(sub)监听这个主题,那么就会在一条消息到达的同时都收到这条消息,这条消息可以被多次消费,类似社交媒体的博主与粉丝的关系。
1.3 消息服务的两个规范
- JMS(Java Message Service)
基于JVM的消息代理规范。ActiveMQ、HornetMQ是JMS的实现。 - AMQP(Advanced Message Queuing Protocol)
高级消息队列协议,兼容JMS。RabbitMQ是AMQP的实现。 - 对比
\ JMS AMQP 定义 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层的协议标准,正因为这种概念抽象,天然具有跨平台、跨语言的特性。 - 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 核心概念
-
Message
消息,消息是不具名的,它由消息头和消息体两部分组成。消息体即发送的内容,在传输过程中不可见;而消息头则是由一系列的可选属性组成,包括routing-key(路由键,发给谁?)、priority(相较于其他消息的优先权)、delivery-mode(该消息可能需要持久化存储)等。 -
Publisher
消息的生产者,是一个向交换器(Exchange)发布消息的客户端应用。 -
Exchange
交换器,用来接收生产者发送的消息并将这些消息路由(根据消息头中的routing-key)给服务器中的队列。有四种类型:direct(默认)、fanout、topic、headers。direct相当JMS中的“点对点”模型,而后面三种相当于JMS中的“发布-订阅”模型。 -
Queue
消息队列,是消息的容器,也是消息最终目的地。一个消息可以投入一个或多个队列。消息一直保持在队列里面,等待消费者连接到这个队列将其取走后会从这个队列中移除。 -
Binding
绑定,描述消息队列和绑定之间的关联。一个绑定就是基于rounting-key将交换器和消息队列连接起来的路由规则。交换器和消息队列的绑定是多对多的,也就是说,一个交换器可以关联多个消息队列,而一个消息队列也可以关联给多个交换器(不管消息从绑定的哪个交换器被转发,都会来到这个消息队列)。 -
Connection
网络连接,比如一条TCP连接。 -
Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实TCP连接内的虚拟连接,不管是发送消息、订阅队列还是接收消息的行为,实际上都是通过信道完成。(因为对于操作系统来说,建立和销毁TCP连接都是非常耗费资源的,不可能每次与消息队列交互都新建TCP连接)。 -
Consumer
消息的消费者,表示一个从消息队列中拿取消息的客户端应用。 -
Virtual Host(简称vhost)
虚拟主机,是共享相同身份认证和加密环境的独立服务器域。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定(路由规则)和权限机制。必须在连接时指定,RabbitMQ默认的vhost是/(可以看出,RabbitMQ使用路径的方式划分vhost)。 -
Broker
表示消息队列服务器的实体主机。
上述概念交互的基本过程大致如下:
(1)生产者Publisher产出消息Message后发送给消息代理(RabbitMQ服务器)Broker里的一个虚拟主机vhost。
(2)虚拟主机把消息交给合适的交换器Exchange,交换器根据消息中的rounting-key判断将消息置入哪一个或哪几个绑定Binding的消息队列Queue中。
(3)消费者Consumer与对应的消息队列建立TCP连接Connection,为了省资源,在这条TCP连接里开辟多个信道Channel,从消息队列中拿出消息到消费者本地。
2.2 运行机制——消息路由
-
相较于JMS,AMQP在路由转发中增加了两个核心角色——Exchange和Binding。 Exchange的不同、Binding的不同都会导致消息路由转发的结果不同。
-
根据Exchange类型的不同,消息被路由转发的策略不同,目前共有四种类型:direct、fanout、topic和headers。
其中,headers Exchange与direct Exchange功能逻辑完全一致,但是根据消息的头部信息而不是routing-key进行路由转发,性能差很多,几乎使用不到了。
(1)Direct Exchange
如果消息中的routing-key和Binding时所规定的routing-key 完全匹配(一模一样),Direct Exchange就将消息置入对应的消息队列中。它是完全匹配、单播(点对点)的模式。例如一个“队列-Direct交换器”绑定要求rounting-key为“dog”,则Direct交换器只会转发routing key=“dog”的消息到这个关联的队列中,不会把这条消息转发到“dog.puppy”、“dog.guard”等关联的队列中(有一点对不上都不行)。
(2)Fanout Exchange
每个发送到Fanout Exchange上的消息都会被路由转发到其绑定的所有队列中去,不管routing-key是什么。因此,Fanout Exchange转发消息是最快的。它是无条件的“广播-订阅”模式。(3)Topic Exchange
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环境
- 使用docker下载RabbitMQ镜像(安装Tag中以management结尾的版本,这些版本自带web管理界面)
#下载镜像 docker pull registry.docker-cn.com/library/rabbitmq:3-management #检查 docker images
- 安装启动镜像
#自带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
- 通过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中使用
-
使用Spring Initializer创建Spring Boot工程,选中Spring for RabbitMQ(Messaging)、Web模块(用来测试)。工程的pom.xml中自动引入了spring-boot-starter-amqp,由这个stater引入了spring-messaging(Spring与消息整合模块)和spring-rabbit(Spring对RabbitMQ操作模块)。
-
在主配置文件中添加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默认是/,可以不写
-
在测试类中注入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()); }
-
在测试类中注入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); }
-
如何将消息体自动序列化为JSON并发送?
(1)默认使用jdk序列化机制的原因:RabbitMQ底层核心调用private volatile MessageConverter messageConverter = new SimpleMessageConverter(); 而它序列化消息体时,所用的就是字节流。
(2)在配置类@Configuration中注入Jackson2JsonMessageConverter替换默认的MessageConverter(注意是org.springframework.amqp.support.converter.MessageConverter)。
@Configuration public class MyAmqpConfig { //在自动配置类注入RabbitTemplate时被设置进去 //【content_type:application/json】同时会记录封装消息体的JavaBean类型,用于正确的反序列化 @Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); } }
-
监听队列并自动获取数据的使用,参照第5小姐。
-
在工程中通过代码的方式创建交换器Exchange、队列Queue并绑定,参照第6小节。
4. RabbitMQ自动配置原理(RabbitAutoConfiguration+RabbitProperties)
-
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); } }
-
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
- 场景举例:订单服务与库存服务解耦,通过消息中间件进行交互。用户下单后发送单据消息给消息中间件中的某一个队列,库存服务实时监听同一个队列,一旦有新的单据消息被置入,库存服务就会取走处理。
- 通过注解实时监听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
- AmqpAdmin组件已经被RabbitAutoConfiguration注入在容器中,只要使用@Autowired注入便可使用。
- 在方法调用上有一个模式:所有declareXXX的方法都是用来创建xxx的,所有deleteXXX/removeXXX都是用来删除xxx的。(xxx可以是交换器Exchange,队列Queue,它们的绑定关系Binding。)
- 实例
@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)); }