RabbitMQ
简介
消息中间件:它接收消息并且转发,就类似于一个快递站,卖家把快递通过快递站,送到我们的手上,MQ也是这样,接收并存储消息,再转发。
RabbitMQ在 2007 年由Rabbit科技有限公司发布,是一个在 AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。
RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue 高级消息队列协议 )的开源实现,由于erlang 语言的高并发特性,性能较好,本质是个队列,FIFO 先入先出,里面存放的内容是message。
用途
MQ的用途最常用的有三个:流量削峰、应用解耦和异步处理。
流量削峰
流量削峰简单概括就是在访问量剧增的情况下,但是应用仍然不能停。比如"双十一"下单的人多,平时的服务没有办法一下子处理如此巨大的流量,那么就可以把这些订单放入到MQ中,使用MQ作缓冲,把一秒内下的订单分散到一段时间去处理,这样虽然用户那边过了十几秒才收到下单成功的消息,但总比直接下单失败要好很多,当然真正的双十一措施要远比这个复杂得多。
应用解耦
以电商中的订单服务举例,订单作为核心业务要涉及到支付、库存、物流等服务,若是不解耦其中一个子系统出了问题,下单就会失败,这显然是违背了高可用原则的。MQ就可以用于这些服务的解耦,将这些服务都放入到MQ中进行处理,这样即使某个服务突然挂了,订单服务也不会立刻失败,需要处理的内存被缓存在MQ中,当失败的服务恢复后可以继续处理订单业务。
异步调用
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api, B 执行完之后调用 api 通知 A 服务。
这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不用做这些操作。A 服务还能及时的得到异步处理成功的消息。
组件
- Broker: 接收和分发消息的应用,RabbitMQ Server就是Message Broker
- Connection: publisher / consumer和 broker之间的TCP连接
- Channel:如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TC PConnection的开销将是巨大的,效率也较低。Channel是在connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id 帮助客户端和message broker识别 channel,所以channel 之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建TCP connection的开销
- Exchange: message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有: direct (point-to-point), topic(publish-subscribe) and fanout
- Routing Key:生产者将消息发送到交换机时会携带一个key,来指定路由规则
- binding Key:在绑定Exchange和Queue时,会指定一个BindingKey,生产者发送消息携带的RoutingKey会和bindingKey对比,若一致就将消息分发至这个队列
- vHost 虚拟主机:每一个RabbitMQ服务器可以开设多个虚拟主机每一个vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的 "交换机exchange、绑定Binding、队列Queue",更重要的是每一个vhost拥有独立的权限机制,这样就能安全地使用一个RabbitMQ服务器来服务多个应用程序,其中每个vhost服务一个应用程序。
交换机类型
-
direct Exchange(直接交换机)
匹配路由键,只有完全匹配消息才会被转发
-
Fanout Excange(扇出交换机)
将消息发送至所有的队列
-
Topic Exchange(主题交换机)
将路由按模式匹配,此时队列需要绑定要一个模式上。符号"#"匹配一个或多个词,符号"."匹配不多不少一个词。因此"abc.#"能够匹配到"abc.def.ghi",但是"abc." 只会匹配到"abc.def"。
-
Header Exchange
在绑定Exchange和Queue的时候指定一组键值对,header为键,根据请求消息中携带的header进行路由
工作模式
- simple(简单模式)
一个生产者对应一个消费者。 - Work queues(工作模式)
一个生产者生产,多个消费者进行消费,一条消息消费一次。 - Publish/Subscibe(发布订阅模式)
生产者首先投递消息到交换机,订阅
了这个交换机的队列就会收到生产者投递的消息。 - Routing(路由模式)
生产者生产消息投递到direct
交换机中,扇出交换机会根据消息携带的routing Key匹配相应的队列。 - Topics(主题模式)
生产者生产消息投递到topic交换机中,上面是完全匹配路由键,而主题模式是模糊匹配,只要有合适规则的路由就会投递给消费者。
基础整合
引入maven依赖
xml
<!--amqp依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
RabbitMQ的自动配置类RabbitAutoConfiguration
自动配置了连接工厂ConnectionFactory
。ConnectionFactory从配置RabbitProperties
中获取连接信息完成连接到RabbitMQ服务器。程序中可以注入RabbitTemplate
给RabbitMQ发送和接收消息。
全局配置文件中配置RabbitMQ连接信息
yaml
spring:
rabbitmq:
host: 192.168.0.117 #rabbitmq服务器地址
port: 5672 #端口号
username: guest #用户名
password: guest #密码
#virtual-host: #虚拟主机
测试发送数据
java
@RunWith(SpringRunner.class)
@SpringBootTest
public class MQTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendDirect(){
// Message需要自己构造一个;定义消息体内容和消息头
// rabbitTemplate.send(exchage,routeKey,message);
HashMap<String, Object> map = new HashMap<>();
map.put("name", "baobao");
map.put("age", 18);
map.put("list", Arrays.asList(1,2,3,4,5));
// 将map对象序列化以后,以baobao为路由键发送到exchange.direct,exchange会根据路由键将消息路由到具体的队列
rabbitTemplate.convertAndSend("exchange.direct", "baobao", map);
}
}
可以发现默认发给RabbitMQ的数据以jdk的方式进行序列化,并且消息头中的content_type
保存了消息体的类型。
测试接收数据
java
@Test
public void testReceiveDirect(){
// 从指定队列中接收数据
Object data = rabbitTemplate.receiveAndConvert("baobao");
System.out.println(data.getClass());
System.out.println(data);
}
自定义json序列化
SpringBoot中,默认发送的对象实例是以JDK序列化的,这总序列化不仅不容易查看消息,还占用较大的内存。
我们可以自己定制序列化方式,这里以json为例:
在自定义配置文件中添加一个自己的MessageConverter
,类型是Jackson2JsonMessageConverter
java
@Configuration
public class MyRabbitConfig {
// 添加json格式序列化器
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
进行测试
java
// 测试广播和json序列化
@Test
public void testJsonSerilize(){
// 广播可以不需要传路由键
rabbitTemplate.convertAndSend("exchange.fanout", "", new Person("文亮", 18));
}
可以发现消息头中的content_type属性保存了消息体的类型为application/json,__TypeId__属性保存了javabean的全类名,用于反序列化
监听消息
@RabbitListener
先在主程序上加@EnableRabbit
,开启基于注解的RabbitMQ模式
java
@SpringBootApplication
@EnableRabbit
public class MainApplicationMQ {
public static void main(String[] args) {
SpringApplication.run(MainApplicationMQ.class, args);
}
}
编写1个Service,声明一个监听方法,方法上标注@RabbitListener
,传入需要监听的队列名。监听方法可以接收的参数如下(无需保证参数顺序):
Message
对象:原生消息的详细信息,包括消息头+消息体- 自定义实体类对象:用消息体反序列化后得到的Javabean
Channel
对象:当前传输的数据通道
java
@Service
public class PersonService {
/**
* 监听队列baobao中的消息,有消息会自动取出并回调该方法
* @param message 原生消息的详细信息,包括消息头+消息体
* @param person 从消息体中解码出的javabean
* @param channel 当前传输的数据通道
*/
@RabbitListener(queues = "baobao")
public void listen(Message message, Person person, Channel channel){
System.out.println(message);
System.out.println(person);
System.out.println(channel);
}
}
启动该消费者,从队列中消费1条消息:
控制台打印结果
json
// message
(Body:'{"name":"文亮","age":18}' MessageProperties [headers={__TypeId__=com.baobao.springbootdemo.mq.bean.Person}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=amq.fanout, receivedRoutingKey=, deliveryTag=1, consumerTag=amq.ctag-81sPttG477H4uOtS_e6tHA, consumerQueue=baobao])
// person
Person{name='文亮', age=18}
// channel
Cached Rabbit Channel: AMQChannel(amqp://guest@192.168.56.55:5672/,1), conn: Proxy@21a02097 Shared Rabbit Connection: SimpleConnection@2496b460 [delegate=amqp://guest@192.168.56.55:5672/, localPort= 6257]
注意:
- 如果只有一个消费客户端,那么rabbitmq默认会将队列中的所有一次性发到消费者,但是消费者接收到消息后只能1个1个处理,只有处理完1个消息(即监听方法运行完毕,哪怕执行时间很长),才能继续处理下一个消息
- 如果启动多个客户端,都对应同一个监听消息的方法,那么对于同一个消息,只有1个客户端可以接收到
- 监听方法中的消息实例对象要与发送端对应,比如发送端发送字节数组那么接收端也要声明为字节数组参数;发送端发送Person对象那么接收端也要声明为Person类型参数
@RabbitHandler
我们还可以采用@RabbitListener
配合@RabbitHandler
的方式完成对消息的监听:
@RabbitListener
:标注在类上,指定监听哪些队列@RabbitHandler
:标注在每个接收并处理不同消息的重载方法上,区分处理不同类型的消息
java
@Service
@RabbitListener(queues = {"baobao","baobao.news","baobao.map"})
public class PersonService {
@RabbitHandler
public void handlePersonMsg(Person person){
System.out.println(person);
}
@RabbitHandler
public void handleUserMsg(User user){
System.out.println(user);
}
}
创建交换机、队列、绑定关系
利用AmqpAdmin
给程序中注入AmqpAdmin
可以实现对RabbitMQ的管理,它的declareXXX
方法可以创建exchange、queue、binding等
java
public class MQTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private AmqpAdmin amqpAdmin;
@Test
public void testAmqpAdmin(){
// 创建一个Direct类型的exchange
amqpAdmin.declareExchange(new DirectExchange("exchange.amqpadmin"));
// 创建一个queue
amqpAdmin.declareQueue(new Queue("queue.amqpadmin"));
// 添加exchange和queue之间的绑定
amqpAdmin.declareBinding(new Binding("queue.amqpadmin", Binding.DestinationType.QUEUE,
"exchange.amqpadmin","queue.amqpadmin",null));
}
使用amqpAdmin创建交换机、队列、绑定关系时,会先检查rabbitmq有是否已经存在对应的交换机、队列、绑定关系,如果不存在才创建,已存在就什么都不做
直接在容器中放置对象
另外还有一种方法可以创建Queue、exchange和绑定关系,直接在容器中放置
即可。当执行任何操作rabbitmq的方法时,如果rabbitmq发现还没有队列、交换机或绑定关系,就会自动创建
java
@Configuration
public class MyRabbitConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
// 创建一个交换机:参数1 交换机名称,参数2 是否持久化,参数3 是否自动删除
@Bean
public Exchange exchange(){
return new DirectExchange("test.direct", true, false);
}
// 创建一个队列:参数1 队列名称,参数2 是否持久化,参数3 是否排他,参数4 是否自动删除
@Bean
public Queue queue(){
return new Queue("test.queue", true, false, false);
}
// 创建交换机和队列的绑定关系:参数1 绑定的目标,参数2 绑定的目标类型,参数3 交换机名称,参数4 路由键,参数5 绑定参数
@Bean
public Binding binding(){
return new Binding("test.queue", Binding.DestinationType.QUEUE,"test.direct",
"queue", null);
}
注意:
- 直接在容器中放置Bean相比于直接利用
AmqpAdmin
来创建交换机、队列、绑定关系的区别是,容器中放置Bean的方式是懒加载的
,也就是说并不会在容器启动时就创建,而是等我们的应用第一次连接rabbitmq进行操作的时候才创建交换机、队列、绑定关系。其底层原理是:当连接第一次创建时,会回调连接创建的监听方法,从容器中查找所有Exchange、Queue和Binding对象,然后利用AmqpAdmin将它们进行创建。也就是说SpringBoot应用刚启动时是不会创建这些对象的,只有程序首次连接rabbitmq获取connection时才会创建- 只有rabbitmq中不存在对应的交换机、队列、绑定关系时才会创建,已存在就什么都不做
另外也可以利用Builder模式链式创建
java
// 利用Builder模式链式创建
@Bean
public Exchange exchange2(){
// 默认就是非自动删除
return ExchangeBuilder.directExchange("direct.exchange").durable(true).build();
}
@Bean
public Queue queue2(){
// 默认就是非自动删除,不排他
return QueueBuilder.durable("queue").build();
}
@Bean
public Binding binding2(@Qualifier("queue2") Queue queue,@Qualifier("exchange2") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("routingkey").noargs();
}