文章目录

  • 1. 消息队列
  • 1.1 引入
  • 1.2 应用场景
  • 1.3 核心概念
  • 1.4 JMS VS AMQP
  • 2. RabbitMQ
  • 2.1 核心概念
  • 2.2 运行机制
  • 2.3 Exchange
  • 2.3.1 direct
  • 2.3.2 fanout
  • 2.3.3 topic
  • 2.4 云服务器安装
  • 2.4.1. docker安装rabbitmq
  • 2.4.2 安全规则配置
  • 2.4.3 登录
  • 2.4.4. 简单实验
  • 3. Spring Boot整合RabbitMQ
  • 3.1 环境搭建
  • 3.2 实验



1. 消息队列


1.1 引入

消息队列已经逐渐成为企业IT系统内部通信的核心手段。它具有低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能,成为异步RPC的主要手段之一。当今市面上有很多主流的消息中间件,如老牌的ActiveMQ、RabbitMQ,炙手可热的Kafka,阿里巴巴自主开发RocketMQ等。


1.2 应用场景
  • 异步通信
  • 应用解耦
  • 流量削峰

1.3 核心概念

消息服务中两个重要概念:消息代理(message broker)和目的地(destination)。当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。消息队列主要有两种形式的目的地:

  • 队列(queue):针对于点对点消息通信(point-to-point)
  • 主题(topic):针对于发布(publish)/订阅(subscribe)消息通信

其他的重要概念有:

  • 点对点式:消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列

消息只有唯一的发送者和接受者,但并不是说只能有一个接收者。即消息有可能被多个接收者接收,但只能有一个用户使用到消息。

  • 发布订阅式:发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息
  • JMS(Java Message Service):Java消息服务,它是基于JVM消息代理的规范,ActiveMQ、HornetMQ都是JMS的实现
  • AMQP(Advanced Message Queuing Protocol):高级消息队列协议,也是一个消息代理的规范,兼容JMS。RabbitMQ是AMQP的实现

1.4 JMS VS AMQP

JMS

AMQP

定义

Java API

网络线级协议

跨语言



跨平台



Model

Peer-2-Peer、Pub/sub

direct exchange、fanout exchange、topic change、headers exchange、system exchange

支持消息类型

多种消息类型:TextMessage、MapMessage、BytesMessage、StreamMessage、ObjectMessag、eMessage (只有消息头和属性)

byte[],当实际应用时,有复杂的消息,可以将消息序列化后发送。

综合评价

JMS 定义了JAVA API层面的标准;在java体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差

AMQP定义了wire-level层的协议标准;天然具有跨平台、跨语言特性



2. RabbitMQ


2.1 核心概念

RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。它的核心概念有:

  • Message:消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
  • Publisher:消息的生产者,也是一个向交换器发布消息的客户端应用程序
  • Exchange:交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。Exchange有4种类型:
  • direct(默认)
  • fanout
  • topic
  • headers
    不同类型的Exchange转发消息的策略有所区别
  • Queue:消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
  • Binding:绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和Queue的绑定可以是多对多的关系。
  • Connection:网络连接,比如一个TCP连接
  • Channel:信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP 连接
  • Consumer:消息的消费者,表示一个从消息队列中取得消息的客户端应用程序
  • Virtual Host:虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost 本质上就是一个mini 版的RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的vhost 是/
  • Broker:表示消息队列服务器实体

springboot 获取队列消费者 springboot内置消息队列_springboot 获取队列消费者


2.2 运行机制

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


2.3 Exchange

Exchange共有direct、fanout、topic和headers四种类型,下面重点看一下前三种:

2.3.1 direct

springboot 获取队列消费者 springboot内置消息队列_springboot 获取队列消费者_02

direct是一种完全匹配且单播的模式,当消息中的路由键和Binding中的binding key完全一致时,交换器才将消息发到对应的消息队列中。

2.3.2 fanout

springboot 获取队列消费者 springboot内置消息队列_消息队列_03

fanout是一种广播的模式,它不处理路由键,只是简单的将队列绑定到交换器上,每个发到fanout类型的交换器的消息都会被发到所有绑定的队列上。因此,fanout类型转发速度是最快的。

2.3.3 topic

springboot 获取队列消费者 springboot内置消息队列_springboot 获取队列消费者_04

topic交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配。此时队列需要绑定到一个模式上。它将路由键和绑定键的字符切分为单词,这些单词之间用点隔开。它同样会识别两个通配符:

  • #:匹配0个或是多个单词
  • *:匹配一个单词

2.4 云服务器安装
2.4.1. docker安装rabbitmq
  • 查询rabbitmq相关镜像文件
[root@izbp15ffbqqbe97j9dcf5dz ~]# docker search rabbitmq
  • 拉取镜像,这里选择带管理平台的*-management版本
[root@izbp15ffbqqbe97j9dcf5dz ~]# docker pull rabbitmq:3.8.5-management
Trying to pull repository docker.io/library/rabbitmq ... 
3.8.5-management: Pulling from docker.io/library/rabbitmq
d7c3167c320d: Pull complete 
131f805ec7fd: Pull complete 
322ed380e680: Pull complete 
6ac240b13098: Pull complete 
58ab633708c7: Pull complete 
4ef7b4c52e3f: Pull complete 
0bcc8241708b: Pull complete 
4bbf89f47f34: Pull complete 
2dcee968b577: Pull complete 
b7f34b3a6ae9: Pull complete 
57f11a70a594: Pull complete 
6a2415c21710: Pull complete 
Digest: sha256:4739d28990182895e27e726414801408c85db5c7af285eb689f2b67cd45c1b29
Status: Downloaded newer image for docker.io/rabbitmq:3.8.5-management
[root@izbp15ffbqqbe97j9dcf5dz ~]# docker pull rabbitmq:3.8.5-management
  • 查看docker中的镜像列表
[root@izbp15ffbqqbe97j9dcf5dz ~]# docker images;
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
docker.io/rabbitmq   3.8.5-management    95bc78c8d15d        3 days ago          187 MB
docker.io/redis      latest              235592615444        10 days ago         104 MB
docker.io/mysql      latest              be0dbf01a0f3        11 days ago         541 MB
  • 运行rabbitmq并进行端口映射
[root@izbp15ffbqqbe97j9dcf5dz ~]# docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq 95bc78c8d15d
  • 查看是否运行成功
[root@izbp15ffbqqbe97j9dcf5dz ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                                                                        NAMES
afc7a1ab33ce        95bc78c8d15d        "docker-entrypoint..."   3 seconds ago       Up 3 seconds        4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, 15671/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp   rabbitmq
2.4.2 安全规则配置

登录云服务器,选择左侧的安全组中的配置规则,手动添加访问规则

springboot 获取队列消费者 springboot内置消息队列_f5_05

2.4.3 登录

使用ip地址:端口号的方式在浏览器中登录rabbitmq,如果看到如下界面,表示rabbitmq安装成功



springboot 获取队列消费者 springboot内置消息队列_springboot 获取队列消费者_06


使用默认的用户名guest和密码guest登录,进行管理界面,然后就可以使用啦~



springboot 获取队列消费者 springboot内置消息队列_docker_07



2.4.4. 简单实验
  • 登录RabbitMQ控制台,添加三种Exchange
  • 添加4个队列
  • Exchange绑定队列

  • 发送消息
  • 查看队列,接收消息
  • 查看具体接受到的信息


3. Spring Boot整合RabbitMQ


3.1 环境搭建

Spring Boot中整合RabbitMQ首先需要导入依赖:

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

Spring Boot中提供了和RabbitMQ相关的自动配置类RabbitAutoConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnMissingBean(ConnectionFactory.class)
	protected static class RabbitConnectionFactoryCreator {

		@Bean
		public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties properties,
				ObjectProvider<ConnectionNameStrategy> connectionNameStrategy) throws Exception {
			PropertyMapper map = PropertyMapper.get();
			CachingConnectionFactory factory = new CachingConnectionFactory(
					getRabbitConnectionFactoryBean(properties).getObject());
			map.from(properties::determineAddresses).to(factory::setAddresses);
			map.from(properties::isPublisherReturns).to(factory::setPublisherReturns);
			map.from(properties::getPublisherConfirmType).whenNonNull().to(factory::setPublisherConfirmType);
			RabbitProperties.Cache.Channel channel = properties.getCache().getChannel();
			map.from(channel::getSize).whenNonNull().to(factory::setChannelCacheSize);
			map.from(channel::getCheckoutTimeout).whenNonNull().as(Duration::toMillis)
					.to(factory::setChannelCheckoutTimeout);
			RabbitProperties.Cache.Connection connection = properties.getCache().getConnection();
			map.from(connection::getMode).whenNonNull().to(factory::setCacheMode);
			map.from(connection::getSize).whenNonNull().to(factory::setConnectionCacheSize);
			map.from(connectionNameStrategy::getIfUnique).whenNonNull().to(factory::setConnectionNameStrategy);
			return factory;
		}

		private RabbitConnectionFactoryBean getRabbitConnectionFactoryBean(RabbitProperties properties)
				throws Exception {
			PropertyMapper map = PropertyMapper.get();
			RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean();
			map.from(properties::determineHost).whenNonNull().to(factory::setHost);
			map.from(properties::determinePort).to(factory::setPort);
			map.from(properties::determineUsername).whenNonNull().to(factory::setUsername);
			map.from(properties::determinePassword).whenNonNull().to(factory::setPassword);
			map.from(properties::determineVirtualHost).whenNonNull().to(factory::setVirtualHost);
			map.from(properties::getRequestedHeartbeat).whenNonNull().asInt(Duration::getSeconds)
					.to(factory::setRequestedHeartbeat);
			map.from(properties::getRequestedChannelMax).to(factory::setRequestedChannelMax);
			RabbitProperties.Ssl ssl = properties.getSsl();
			if (ssl.determineEnabled()) {
				factory.setUseSSL(true);
				map.from(ssl::getAlgorithm).whenNonNull().to(factory::setSslAlgorithm);
				map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType);
				map.from(ssl::getKeyStore).to(factory::setKeyStore);
				map.from(ssl::getKeyStorePassword).to(factory::setKeyStorePassphrase);
				map.from(ssl::getTrustStoreType).to(factory::setTrustStoreType);
				map.from(ssl::getTrustStore).to(factory::setTrustStore);
				map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase);
				map.from(ssl::isValidateServerCertificate)
						.to((validate) -> factory.setSkipServerCertificateValidation(!validate));
				map.from(ssl::getVerifyHostname).to(factory::setEnableHostnameVerification);
			}
			map.from(properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis)
					.to(factory::setConnectionTimeout);
			factory.afterPropertiesSet();
			return factory;
		}

	}

	@Configuration(proxyBeanMethods = false)
	@Import(RabbitConnectionFactoryCreator.class)
	protected static class RabbitTemplateConfiguration {

		@Bean
		@ConditionalOnMissingBean
		public RabbitTemplateConfigurer rabbitTemplateConfigurer(RabbitProperties properties,
				ObjectProvider<MessageConverter> messageConverter,
				ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers) {
			RabbitTemplateConfigurer configurer = new RabbitTemplateConfigurer();
			configurer.setMessageConverter(messageConverter.getIfUnique());
			configurer
					.setRetryTemplateCustomizers(retryTemplateCustomizers.orderedStream().collect(Collectors.toList()));
			configurer.setRabbitProperties(properties);
			return configurer;
		}

		@Bean
		@ConditionalOnSingleCandidate(ConnectionFactory.class)
		@ConditionalOnMissingBean(RabbitOperations.class)
		public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) {
			RabbitTemplate template = new RabbitTemplate();
			configurer.configure(template, connectionFactory);
			return template;
		}

		@Bean
		@ConditionalOnSingleCandidate(ConnectionFactory.class)
		@ConditionalOnProperty(prefix = "spring.rabbitmq", name = "dynamic", matchIfMissing = true)
		@ConditionalOnMissingBean
		public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) {
			return new RabbitAdmin(connectionFactory);
		}

	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(RabbitMessagingTemplate.class)
	@ConditionalOnMissingBean(RabbitMessagingTemplate.class)
	@Import(RabbitTemplateConfiguration.class)
	protected static class MessagingTemplateConfiguration {

		@Bean
		@ConditionalOnSingleCandidate(RabbitTemplate.class)
		public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) {
			return new RabbitMessagingTemplate(rabbitTemplate);
		}

	}

}

RabbitAutoConfiguration中有和自动配置相关的连接工厂ConnectionFactory,并通过RabbitProperties 封装了RabbitMQ的配置。因此,可以在application.properties中添加和RabbitMQ相关的配置

spring.rabbitmq.addresses=xxxx
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

此外,自动配置类还添加了如下的两个组件:

  • RabbitTemplate :给RabbitMQ发送和接受消息
  • AmqpAdmin : RabbitMQ系统管理功能组件,它可以用于创建和删除 Queue、Exchange、Binding

此外还提供了和RabbitMQ相关的注解:

  • @EnableRabbit:开启RabbitMQ的注解支持
  • @RabbitListener :设置监听器,监听消息队列的内容

3.2 实验

RabbitAutoConfiguration提供了RabbitTemplate 和AmqpAdmin 两个组件用于操作消息队列,下面我们通过单元测试的方式来看一下如何使用。首先设置它们各自的对象,并通过@Autowired实现自动装配

@Autowired
RabbitTemplate rabbitTemplate;

@Autowired
AmqpAdmin amqpAdmin;

测试方法

@Test
public void createExchange(){
    amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
    System.out.println("创建完成");

    amqpAdmin.declareQueue(new Queue("amqpadmin.queue", true));

    amqpAdmin.declareBinding(new Binding("amqpadmin.queue",
                                         Binding.DestinationType.QUEUE,
                                         "amqpadmin.exchange",
                                         "amqp.#",
                                         null));

}

使用amqpAdmin.declareExchange()可以添加一个交换器,方法的参数是如下所示的某一个Exchange接口实现类对象

springboot 获取队列消费者 springboot内置消息队列_docker_08

DirectExchange()构造方法中需传入交换器的name。



springboot 获取队列消费者 springboot内置消息队列_消息队列_09


类似,declareQueue()用于声明一个队列



springboot 获取队列消费者 springboot内置消息队列_消息队列_10


declareBinding()用于实现交换器和队列之间的绑定。



springboot 获取队列消费者 springboot内置消息队列_docker_11


rabbitTemplate中的convertAndSend(exchage,routeKey,object)可用于向指定的交换器中传递数据,并设置路由键。其中object默认当成消息体,只需要传入要发送的对象,自动序列化发送给rabbitmq

@Test
public void testPutMessage() {
    Map<String,Object> map = new HashMap<>();
    map.put("msg","first message");
    map.put("data", Arrays.asList("hello world",123,true));
    //对象被默认序列化以后发送出去
    rabbitTemplate.convertAndSend("amqpadmin.exchange","amqp.#",new Book(1,"西游记"));

}

执行单元测试,可以从消息队列中获取到传入的message



springboot 获取队列消费者 springboot内置消息队列_docker_12


另外receiveAndConvert()可用于从指定的队列中获取元素

@Test
public void receive(){
    Object o = rabbitTemplate.receiveAndConvert("amqpadmin.queue");
    System.out.println(o.getClass());
    System.out.println(o);
}

执行单元测试,控制台输出

class dyliang.domain.Book
dyliang.domain.Book@3d7fb838

其他更多的方法可以根据RabbitTemplate 和AmqpAdmin类中定义的方法选择使用。