springboot03_RabbitMQ
1.1 RabbitMQ
1.1.1 springBoot与消息
大多应用中,都可以通过消息服务中间件来提升系统异步通信、扩展解耦能力,下面将对几个应用场景进行介绍。
传统项目的注册功能是采用同步调用的方式依次完成信息写入数据库、发送邮件、发送信息,如下图:
很显然,在信息写入数据库之后,邮件和信息的发送并不用保证一定的先后顺序,此时我们可以使用多线程同步执行邮件和信息的发送。,如下图:
即便使用了多线程,对用户的响应也依旧是在邮件和信息发送完成之后进行的,这依旧是同步的,此时我们可以使用消息队列,利用异步消息来优化系统,给用户更快的响应。
我们还可以使用消息队列来进行应用解耦。在传统项目中,下定单功能需要调用库存接口,此时订单系统和库存系统直接就存在着耦合。
我们可以使用消息队列来进行解耦,如下图:
我们还可以通过消息队列实现流量削峰,比如在秒杀系统中,我们可以给消息队列定一个长度,用户请求按照请求的先后顺序进入队列,当请求占满队列之后就给之后的请求直接响应秒杀失败,而在队列中的请求便是秒杀成功的用户请求,此时可以根据队列中的信息来处理这些秒杀成功的请求。
1.1.2 概述
消息服务中有两个重要概念:
1. 消息代理(message broker):消息中间件的服务器
2. 目的地(destination):当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
消息队列主要有两种形式的目的地:
1. 队列(queue):点对点消息通信(point-to-point)
2. 主题(topic):发布(publish)/订阅(subscribe)消息通信
点对点式通讯机制:
– 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列。
– 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者,也就是说消息只能有一个发送者,可以有多个接收者但是当其中一个接收着收到消息后,该消息就会被销毁,其它接收者就无法再接受该消息。
发布订阅式通讯机制:
– 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息,也就是说消息有一个发送者,有多个接收者,接收者都可以接收该消息。
两个消息服务的规范:
1. JMS(Java Message Service):JAVA消息服务
基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现。
2. AMQP(Advanced Message Queuing Protocol):高级消息队列协议
也是一个消息代理的规范,兼容JMS。RabbitMQ是AMQP的实现。
JMS | AMQP | |
---|---|---|
定义 | Java api | 网络线级协议 |
跨语言 | 否 | 是 |
跨平台 | 否 | 是 |
Model 提供两种消息模型: | 1. Peer-2-Peer 2. Pub/sub |
提供了五种消息模型: 1. direct exchange 2. fanout exchange 3. topic change 4. headers exchange 5. system exchange 本质来讲,后四种和JMS的pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分. |
支持消息类型 | 多种消息类型:TextMessage MapMessage BytesMessage StreamMessage ObjectMessageMessage (只有消息头和属性) |
byte[],当实际应用时,有复杂的消息,可以将消息序列化后发送。 |
综合评价 | JMS 定义了JAVA API层面的标准,在java体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差。 | AMQP定义了wire-level层的协议标准,天然具有跨平台、跨语言特性。 |
1.1.3 RabbitMQ简介
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 | 表示消息队列服务器实体。 |
- 交互流程
消息生产者(Publisher)将消息(Message)发送到消息代理服务器(Broker)中的虚拟主机(Virtual Host),虚拟主机中有很多自己的交换器(Exchange)和消息队列(Queue),当虚拟主机接收到消息之后会将该消息交给指定的交换器,交换器会根据消息的路由键(routing-key)判断将消息路由到那个队列中,路由规则由绑定关系(Binding)来表示。当消息到达消息队列之后,消费者就可以从消息队列中取得消息。消费者想要取得消息需要和队列建立网络连接(Connection),而为了节省资源在一个网络连接中开辟了很多的信道(Channel),从消息队列拿到的数据就通过这些信道交给消费者。
2.1 原理机制
2.1.1 RabbitMQ运行机制
AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了Exchange和 Binding的角色。生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列。
2.1.2 Exchange 类型
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键, headers 交换器和direct 交换器完全一致,但性能差很多,目前几乎用不到了,因此这里将不再进行介绍。
- Direct Exchange
消息中的路由键(routing key)如果和 Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routing key 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。
- Fanout Exchange
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。
- Topic Exchange
topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“”。#匹配0个或多个单词,匹配一个单词。
3.1 RabbirMQ安装测试
- 使用Docker下载镜像
docker pull rabbitmq:3-management
tag带management的镜像是带有Web管理界面的。
- 运行RabbitMQ镜像
docker images #查看本地镜像信息
docker run -d -p 5672:5672 -p 15672:15672 --name myrabbitmq df6f26ea3e53 #根据镜像ID运行镜像
docker ps #查看运行中的镜像
带Web管理界面的RabbitMQ需要向外暴露两个端口,其中5672是客户端与RabbitMQ进行通信的端口,15672是访问Web管理界面的端口。
启动容器之后在浏览器中访问192.168.1.16:15672,可以看到如下界面。
之后我们可以使用默认的guest账号密码来登录(账号和密码都是guest),登录之后将进入管理主页。
- 测试RabbitMQ的消息路由机制
- 创建交换器
在RabbitMQ管理界面选中Exchange标签。
之后找到Add a new exchange选项,点击,之后根据提示创建交换器。
同上创建另外两种类型的交换器,总共创建三个,如下:
- 创建队列
在RabbitMQ管理界面选中Queues标签。
之后找到Add a new queue选项,之后根据提示创建队列。
按照上述步骤创建如下的队列:
- 绑定交换器和队列
点击目标交换器。
绑定队列。
按照上述步骤将每一个交换器都和四个队列进行绑定。
exchange.direct的绑定:
exchange.fanout的绑定
exchange.topic的绑定
完成绑定后我们就可以发消息进行测试。
之后找到Publish message选项
之后我们就可以在队列中查看发送的消息,由于direct机制是完全匹配,所以只有sk队列能够收到该消息。
3.2 RabbitMQ整合
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在spring-boot-starter-amqp中引入了如下的依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
</dependency>
</dependencies>
- RabbitMQ的自动配置原理
在SpringBoot中只要引入了相关场景依赖,它就会进行自动配置。根据自动配置原理,我们可以查找RabbitAutoConfiguration类来查看相关配置。
1. 自动配置了连接工厂CachingConnectionFactory,部分源码如下:
@Bean
public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties config)
throws Exception {
RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean();
if (config.determineHost() != null) {
factory.setHost(config.determineHost());
}
factory.setPort(config.determinePort());
if (config.determineUsername() != null) {
factory.setUsername(config.determineUsername());
}
if (config.determinePassword() != null) {
factory.setPassword(config.determinePassword());
}
if (config.determineVirtualHost() != null) {
factory.setVirtualHost(config.determineVirtualHost());
}
相关的配置信息都是从RabbitProperties中获取的,其封装了RabbitMQ的所有相关配置,部分源码如下:
@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitProperties {
/**
* RabbitMQ host.
*/
private String host = "localhost";
/**
* RabbitMQ port.
*/
private int port = 5672;
/**
* Login user to authenticate to the broker.
*/
private String username;
/**
* Login to authenticate against the broker.
*/
private String password;
/**
* SSL configuration.
*/
private final Ssl ssl = new Ssl();
/**
* Virtual host to use when connecting to the broker.
*/
private String virtualHost;
/**
* Comma-separated list of addresses to which the client should connect.
*/
private String addresses;
/**
* Requested heartbeat timeout, in seconds; zero for none.
*/
private Integer requestedHeartbeat;
/**
* Enable publisher confirms.
*/
private boolean publisherConfirms;
/**
* Enable publisher returns.
*/
private boolean publisherReturns;
/**
* Connection timeout, in milliseconds; zero for infinite.
*/
private Integer connectionTimeout;
/**
* Cache configuration.
*/
private final Cache cache = new Cache();
/**
* Listener container configuration.
*/
private final Listener listener = new Listener();
private final Template template = new Template();
private List<Address> parsedAddresses;
我们可以在application.properties配置文件中对RabbitMQ进行简单的配置,内容如下:
#RabbitMQ的主机地址
spring.rabbitmq.host=192.168.1.16
#RabbitMQ的端口,默认就是5672,所以可以不配置
#spring.rabbitmq.port=5672
#RabbitMQ的登录用户名
spring.rabbitmq.username=guest
#RabbitMQ的登录密码
spring.rabbitmq.password=guest
#RabbitMQ的访问路径,默认值是/
#spring.rabbitmq.virtual-host=/
从RabbitAutoConfiguration的源码我们可以看到其给容器中添加了RabbitTemplate组件负责和RabbitMQ进行交互;添加了AmqpAdmin组件负责提供系统管理功能。
- 测试RabbitTemplate
@RunWith(SpringRunner.class)
@SpringBootTest
public class Springboot02AmqpApplicationTests {
@Autowired
RabbitTemplate rabbitTemplate; //1. 注入RabbitTemplate
//2. 测试发送消息
@Test
public void contextLoads() {
//Message需要自己构造一个;定义消息体内容和消息头
//rabbitTemplate.send(exchage,routeKey,message);
//object默认当成消息体,只需要传入要发送的对象,自动序列化发送给rabbitmq;
//rabbitTemplate.convertAndSend(exchage,routeKey,object);
Map<String,Object> map = new HashMap<>();
map.put("msg","这是第一个消息");
map.put("data", Arrays.asList("helloworld",123,true));
//对象被默认序列化以后发送出去
rabbitTemplate.convertAndSend("exchange.direct","sk.news",map);
}
//3. 测试接受数据
@Test
public void receive(){
Object o = rabbitTemplate.receiveAndConvert("sk.news");
System.out.println(o.getClass());
System.out.println(o);
}
}
通过测试结果可以发现上述代码可以成功将消息发送到RabbitMQ也可以成功从RabbitMQ获取到消息,但是此时在RabbitMQ的Web管理界面看到的消息是使用Java序列化后的内容,这是因为RabbitTemplate默认使用的是Java的序列化方式。
在RabbitTemplate中有一个消息转换器MessageConverter,如下
这个消息转化器指定了序列化规则,如果我们想要将数据以json的形式放入到RabbitMQ,我们可以来定制自己的MessageConverter。
@Configuration
public class MyAMQPConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
上面是一个单播模式,我们还可以进行广播:
@Test
public void sendMsg() {
//广播不用指定路由键
rabbitTemplate.convertAndSend("exchange.fanout", "", new Book("红楼梦", "曹雪芹"));
}
3.2.1 @RabbitListener和@EnableRabbit
前面是使用RabbitTemplate来操作RabbitMQ发送和接收消息,在实际开发中我们需要一些监听场景,比如前面例子中的订单系统下单之后将相关信息放到消息队列,而库存系统则对该队列进行监听,一旦有新的订单内容库存系统就能够及时得到消息并进行相关的操作。
下面将介绍如何使用@RabbitListener和@EnableRabbit注解进行对消息队列的监听。
- 使用@RabbitListener进行监听
@Service
public class BookService {
@RabbitListener(queues = "sk.news")
public void receive(Book book){
System.out.println("收到消息:"+book);
}
@RabbitListener(queues = "sk")
public void receive02(Message message){//使用Message可以获取消息头信息,上面的只是能获取消息内容(Body)
System.out.println(message.getBody());
System.out.println(message.getMessageProperties());
}
}
- 开启注解模式
@EnableRabbit //开启基于注解的RabbitMQ模式
@SpringBootApplication
public class Springboot02AmqpApplication {
3.2.2 AmqpAdmin管理组件的使用
前面所有的测试都是在Exchange和Queue已经创建完成的基础上,像这样手动创建很明显过于繁琐,下面将介绍如何使用AmqpAdmin自动化管理交换器、队列和绑定规则。
在自动配置类中已经往容器中添加了AmqpAdmin组件,我们可以直接自动注入,之后就可以使用了。
@RunWith(SpringRunner.class)
@SpringBootTest
public class Springboot02AmqpApplicationTests {
@Autowired
AmqpAdmin amqpAdmin;
@Test
public void createExchange() {
//创建交换器,可以根据需求创建TopicExchange、DirectExchange等
amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
//创建消息队列
amqpAdmin.declareQueue(new Queue("amqpadmin.queue",true));
//创建绑定规则
//参数分别是:
// 目的地:指定要发送到哪个队列
// 绑定类型:这里要绑定到队列,所以是Binding.DestinationType.QUEUE
// EXchange名:指定目标交换器
// 路由键:指定路由键
amqpAdmin.declareBinding(new Binding("amqpadmin.queue", Binding.DestinationType.QUEUE,"amqpadmin.exchange","amqp.haha",null));
}
}