文章目录
- 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:表示消息队列服务器实体
2.2 运行机制
AMQP 中增加了Exchange和Binding的角色。生产者把消息发布到Exchange 上,消息最终到达队列并被消费者接收,而Binding 决定交换器的消息应该发送到那个队列。
2.3 Exchange
Exchange共有direct、fanout、topic和headers四种类型,下面重点看一下前三种:
2.3.1 direct
direct是一种完全匹配且单播的模式,当消息中的路由键和Binding中的binding key完全一致时,交换器才将消息发到对应的消息队列中。
2.3.2 fanout
fanout是一种广播的模式,它不处理路由键,只是简单的将队列绑定到交换器上,每个发到fanout类型的交换器的消息都会被发到所有绑定的队列上。因此,fanout类型转发速度是最快的。
2.3.3 topic
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 安全规则配置
登录云服务器,选择左侧的安全组中的配置规则,手动添加访问规则
2.4.3 登录
使用ip地址:端口号的方式在浏览器中登录rabbitmq,如果看到如下界面,表示rabbitmq安装成功
使用默认的用户名guest和密码guest登录,进行管理界面,然后就可以使用啦~
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接口实现类对象
DirectExchange()
构造方法中需传入交换器的name。
类似,declareQueue()
用于声明一个队列
declareBinding()
用于实现交换器和队列之间的绑定。
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
另外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类中定义的方法选择使用。