MQ全称是Message Queue,可以理解为消息队列的意思,简单来说就是消息以管道的方式进行传递。
RabbitMQ是一个实现了AMQP(Advanced Message Queuing Protocol)高级消息队列协议的消息队列服务,用Erlang语言的。
在我们秒杀抢购商品的时候,系统会提醒我们稍等排队中,而不是像几年前一样页面卡死或报错给用户。
像这种排队结算就用到了消息队列机制,放入通道里面一个一个结算处理,而不是某个时间断突然涌入大批量的查询新增把数据库给搞宕机,所以RabbitMQ本质上起到的作用就是削峰填谷,为业务保驾护航。
1.除了Qpid,RabbitMQ是唯一一个实现了AMQP标准的消息服务器;
2.可靠性,RabbitMQ的持久化支持,保证了消息的稳定性;
3.高并发,RabbitMQ使用了Erlang开发语言,Erlang是为电话交换机开发的语言,天生自带高并发光环,和高可用特性;
4.集群部署简单,正是应为Erlang使得RabbitMQ集群部署变的超级简单;
5.社区活跃度高,根据网上资料来看,RabbitMQ也是首选;
生产者、消费者和代理
在了解消息通讯之前首先要了解3个概念:生产者、消费者和代理。
生产者:消息的创建者,负责创建和推送数据到消息服务器;
消费者:消息的接收方,用于处理数据和确认消息;
代理:就是RabbitMQ本身,用于扮演“快递”的角色,本身不生产消息,只是扮演“快递”的角色。
消息发送原理
首先你必须连接到Rabbit才能发布和消费消息,那怎么连接和发送消息的呢?
你的应用程序和Rabbit Server之间会创建一个TCP连接,一旦TCP打开,并通过了认证,认证就是你试图连接Rabbit之前发送的Rabbit服务器连接信息和用户名和密码,有点像程序连接数据库,使用Java有两种连接认证的方式,后面代码会详细介绍,一旦认证通过你的应用程序和Rabbit就创建了一条AMQP信道(Channel)。
信道是创建在“真实”TCP上的虚拟连接,AMQP命令都是通过信道发送出去的,每个信道都会有一个唯一的ID,不论是发布消息,订阅队列或者介绍消息都是通过信道完成的。
为什么不通过TCP直接发送命令?
对于操作系统来说创建和销毁TCP会话是非常昂贵的开销,假设高峰期每秒有成千上万条连接,每个连接都要创建一条TCP会话,这就造成了TCP连接的巨大浪费,而且操作系统每秒能创建的TCP也是有限的,因此很快就会遇到系统瓶颈。
如果我们每个请求都使用一条信道连接,既满足了性能的需要,又能确保每个连接的私密性,这就是引入信道概念的原因。
你必须知道的Rabbit
想要真正的了解Rabbit有些名词是你必须知道的。
包括:ConnectionFactory(连接管理器)、Channel(信道)、Exchange(交换器)、Queue(队列)、RoutingKey(路由键)、BindingKey(绑定键)。
ConnectionFactory(连接管理器):应用程序与Rabbit之间建立连接的管理器,程序代码中使用;
Channel(信道):消息推送使用的通道;
Exchange(交换器):用于接受、分配消息;
Queue(队列):用于存储生产者的消息;
RoutingKey(路由键):用于把生成者的数据分配到交换器上;
BindingKey(绑定键):用于把交换器的消息绑定到队列上;
看到上面的解释,最难理解的路由键和绑定键了,那么他们具体怎么发挥作用的,请看下图
当你把消息发送到Rabbit服务器的时候,你需要选择你是否要进行持久化,但这并不能保证Rabbit能从崩溃中恢复,想要Rabbit消息能恢复必须满足3个条件:
投递消息的时候durable设置为true,消息持久化,代码:channel.queueDeclare(x, true, false, false, null),参数2设置为true持久化;
设置投递模式deliveryMode设置为2(持久),代码:channel.basicPublish(x, x, MessageProperties.PERSISTENT_TEXT_PLAIN,x),参数3设置为存储纯文本到磁盘;
消息已经到达持久化交换器上;
消息已经到达持久化的队列;
持久化工作原理
Rabbit会将你的持久化消息写入磁盘上的持久化日志文件,等消息被消费之后,Rabbit会把这条消息标识为等待垃圾回收。
持久化的缺点
消息持久化的优点显而易见,但缺点也很明显,那就是性能,因为要写入硬盘要比写入内存性能较低很多,从而降低了服务器的吞吐量,尽管使用SSD硬盘可以使事情得到缓解,但他仍然吸干了Rabbit的性能,当消息成千上万条要写入磁盘的时候,性能是很低的。
所以使用者要根据自己的情况,选择适合自己的方式。
虚拟主机
每个Rabbit都能创建很多vhost,我们称之为虚拟主机,每个虚拟主机其实都是mini版的RabbitMQ,拥有自己的队列,交换器和绑定,拥有自己的权限机制。
vhost特性
RabbitMQ默认的vhost是“/”开箱即用;
多个vhost是隔离的,多个vhost无法通讯,并且不用担心命名冲突(队列和交换器和绑定),实现了多层分离;
举例用到的三个类:MqConfig,MqSender,MqReceiver
direct原理图
我们可以看到direct模式下,无需路由键和绑定键,生产者生产消息通过交换器到了队列,然后由消费者消费
direct实现
MqConfig
/**
* direct的队列
*/
public static final String DIRECT_QUEUE = "direct.queue";
@Bean
public Queue directQueue() {
return new Queue(DIRECT_QUEUE, true);
}
MqSender
/**
* direct 模式
*
* @param messages 发的消息
*/
public void sendDirectMessage(Object messages) {
String message = RedisService.beanToString(messages);
log.info("rabbitmq send direct message:" + message);
amqpTemplate.convertAndSend(MqConfig.DIRECT_QUEUE, message);
}
MqReceiver
@RabbitListener(queues = MqConfig.DIRECT_QUEUE)
public void receiveDirectMessage(String message) {
log.info("rabbitmq receive direct message:" + message);
}
topic原理图
我们可以看到topic模式下,路由键和绑定键,生产者生产消息通过交换器到了队列,然后由消费者消费,绑定键中的#,#代表匹配一个或者多个单词,*代表不多于1个单词,就是出现0或者1个
实现
MqConfig
/**
* 第二种方式:topic方式
*
* @return
*/
/**
* topic的队列
*/
public static final String TOPIC_QUEUE1 = "topic.queue1";
public static final String TOPIC_QUEUE2 = "topic.queue2";
public static final String TOPIC_QUEUE3 = "topic.queue3";
public static final String TOPIC_EXCHANGE = "topic.exchange";
@Bean
public Queue topicQueue1() {
return new Queue(TOPIC_QUEUE1, true);
}
@Bean
public Queue topicQueue2() {
return new Queue(TOPIC_QUEUE2, true);
}
@Bean
public Queue topicQueue3() {
return new Queue(TOPIC_QUEUE3, true);
}
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(TOPIC_EXCHANGE);
}
@Bean
public Binding bindingTopicExchange1() {
return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("topic.#");
}
@Bean
public Binding bindingTopicExchange2() {
return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.keys1");
}
@Bean
public Binding bindingTopicExchange3() {
return BindingBuilder.bind(topicQueue3()).to(topicExchange()).with("topic.*");
}
MqSender
/**
* topic 模式
*
* @param messages 发的消息
*/
public void sendTopicMessage(Object messages) {
String message = RedisService.beanToString(messages);
log.info("rabbitmq send topic message:" + message);
amqpTemplate.convertAndSend(MqConfig.TOPIC_EXCHANGE, "topic.keys2", "topic.keys2:" + message);
amqpTemplate.convertAndSend(MqConfig.TOPIC_EXCHANGE, "topic.keys1.time", "topic.keys1.time:" + message);
}
MqReceiver
@RabbitListener(queues = MqConfig.TOPIC_QUEUE1)
public void receiveTopicMessage1(String message) {
log.info("rabbitmq receive topic message:" + message);
}
@RabbitListener(queues = MqConfig.TOPIC_QUEUE2)
public void receiveTopicMessage2(String message) {
log.info("rabbitmq receive topic message:" + message);
}
@RabbitListener(queues = MqConfig.TOPIC_QUEUE3)
public void receiveTopicMessage3(String message) {
log.info("rabbitmq receive topic message:" + message);
}
测试结果:
绑定键topic.keys1
amqpTemplate.convertAndSend(MqConfig.TOPIC_EXCHANGE, "topic.keys1", "topic.keys1:" + message);
绑定键topic.keys1,满足条件的队列为1,2,3
绑定键topic.keys2
amqpTemplate.convertAndSend(MqConfig.TOPIC_EXCHANGE, "topic.keys2", "topic.keys2:" + message);
绑定键topic.keys2,满足条件的队列为1,3
绑定键topic.keys1.time
amqpTemplate.convertAndSend(MqConfig.TOPIC_EXCHANGE, "topic.keys1.time", "topic.keys1.time:" + message);
fanout理图
我们可以看到fanout模式下,有路由键,无绑定键,生产者生产消息通过交换器到了队列,然后由消费者消费
fanout实现
MqConfig
/**
* 第三种:fanout广播模式
*/
/**
* fanout
*/
public static final String FANOUT_QUEUE1 = "fanout.queue1";
public static final String FANOUT_QUEUE2 = "fanout.queue2";
public static final String FANOUT_EXCHANGE = "fanout.exchange";
@Bean
public Queue fanoutQueue1() {
return new Queue(FANOUT_QUEUE1,true);
}
@Bean
public Queue fanoutQueue2() {
return new Queue(FANOUT_QUEUE2,true);
}
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange(FANOUT_EXCHANGE);
}
@Bean
public Binding bindingFanoutBinding1(){
return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
}
@Bean
public Binding bindingFanoutBinding2(){
return BindingBuilder.bind(fanoutQueue2()).to(fanoutExchange());
}
MqSender
/**
* fanout 模式
*
* @param messages 发的消息
*/
public void sendFanoutMessage(Object messages) {
String message = RedisService.beanToString(messages);
log.info("rabbitmq send fanout message:" + message);
amqpTemplate.convertAndSend(MqConfig.FANOUT_EXCHANGE, "", message);
}
MqReceiver
@RabbitListener(queues = MqConfig.FANOUT_QUEUE1)
public void receiveFanoutMessage1(String message) {
log.info("rabbitmq receive fanout message:" + message);
}
@RabbitListener(queues = MqConfig.FANOUT_QUEUE2)
public void receiveFanoutMessage2(String message) {
log.info("rabbitmq receive fanout message:" + message);
}