当我们提到消息通信时,经常会联想到向微信以及QQ这些即时通信的工具,不过这些不是我们所要讨论的RabbitMQ消息通信,二者之间有着很大的区别,虽然RabbitMQ与前者有着类似的地方,但是RabbitMQ更为灵活,向邮箱一类的通信工具,具有着一定的格式,但是AMQP(高级消息队列)更为灵活,消息没有固定的结构,同时可以存储二进制数据。
当我们学习多线程的时候,最经典的问题就是生产者以及消费者,二者有着一定的耦合数据,那么我们今天讨论的RabbitMQ可以理解为数据的存储容器,他不负责生产数据,同时我们也可以理解为,RabbitMQ是一个路由的作用负责接收生产者的数据,同时负责发送数据给消费者。
生产者:创建信息,发送给RabbitMQ,消息包含两部分内容:有效载荷(payload)和标签(label),有效载荷就是我们所要传递的数据,标签即为描述有效载荷的信息,RabbitMQ根据标签,来决定谁将获得信息的拷贝。
消费者:它们链接到代理服务器RabbitMQ上,然后订阅到队列( quene)上,把消息队列想象为特定的邮箱,每当消息到达特定的邮箱的时候,RabbitMQ或将其发送给一个订阅的消费者上面。
理解起来非常简单,我们可以把它想象成为一个特别大的通道,同时RabbitMQ为一个转送站, 负责将将生产者的数据发送给特定的消费者,生产者以及消费者之间的连接也就是我们经常提到的主题(topic)。
当我们要使用RabbitMQ的时候生产者以及消费者都需要链接到RabbitMQ上,你的应用程序和RabbitMQ程序之间创建一条TCP连接,一旦TCP连接打开之后,应用程序就可以创建AMQP信道,信道是建立在TCP连接之上,每条信道都会有唯一的ID,AMQP库进行保存,不论发送信息还是接受信息都是建立在信道的基础上,在信道上进行传输。
信道的作用:避免过多的TCP连接,当信息并发量较大的时候,如果都采用TCP链接则会创建过多的连接,很快就会碰到性能瓶颈,但是为了使每个线程具有私密性,各个不相互干扰,那么我们便创建了信道。
当我们在学习多线程的时候,用于存放生产者生产出来的信息成为队列。那么我们来看看RabbitMQ的队列。
从概念上来讲,AMQP消息路由必须有三部分:交换器、队列、绑定。生产者将信息发送到交换器上,消息最终到达队列,并被消费者接收,绑定决定了信息如何从路由器到特定的队列上。
消费者通过以下两种命令从特定的队列中获取信息:
如果没有队列上没有消费者订阅,那么消息会存储在队列中,知道有订阅的消费者订阅,然后发送消息给消费者,那么当多个消费者订阅了队列又该如何处置呢。
当有多个消费者订阅到RabbitMQ的队列上,那么队列收到的消息将会以循环的方式发送给消费者,例如第一次发给A,第二次发给B,然后依次循环。当消息被消费者消费后,队列将会删除消息。
RabbitMQ在删除队列中的消息时,要求消费者发送消息接收到的确认,消费者通过basic.ack命令显示的想RabbitMQ发送确认,然后队列在执行删除操作,或者我们将basic.ack参数设置为true,那么RabbitMQ会自动视为消费者确认接收到消息。
如果消费者在接收到消息之后,确认之前与RabbitMQ断开连接,RabbitMQ认为这条消息没有被分发,会继续发送给下一个消费者,同时如果我们的程序忘了发送确认信息,那么RabbitMQ将不会发送信息给该消费者,认为其没有做好接受消息的准备。
我们有两种方式拒绝消息:
在RabbitMQ中有一个特殊的队列“死信”,我们可以通过来获取被拒绝然后移除队列的消息,我们可以通过这个方案来查询程序问题。
生产者以及消费者都可以通过queue.declare命令来创建队列,但是消费者在一个信道上订阅了另一个队列的话,就无法创建队列,必须取消订阅,才可。
当我们创建队列的时候,通常会指定队列的名称,消费者订阅队列的时候需要队列的名称,并在创建绑定的时候也需要指定队列的名称,如果我们不指定队列的名称的话,RabbitMQ会分配一个随机名称并在queue.declare命令中返回。队列设置的参数:
如果我们尝试声明一个已经存在的队列,如果声明队列的参数与已有队列相同,RabbitMQ什么也不会做,就如同创建队列成功,但是如果参数不相同,那么会报错。如果我们只是想要知道队列是否创建成功,可以采用queue.declare的passive参数设置为true,如果队列存在返回返回成功,不存在报错。
当我们吧消息投递给交换器,交换器根据 路由键(touting key),队列通过路由键绑定到交换器上,如果路由键不存在那么消息会进入黑洞也就是消息丢失。
交换器通过路右键将消息发送个队列,但是如何处理投递到多个队列的情况呢?
协议中定义了四种不同的类型的交换器发挥了作用,direct、 fanout、topic、headers。
basic_publish($msg,"","queue—name")
第一个参数发送的消息内容,第二个参数默认的交换器,第三个路由键,我们之前声明队列的名称。queue.bind(msg-inbox-errors,'logs-exchange','error.msg-inbox')
,消费者通过basic.publish($msg,'logs-exchange','error.msg-inbox')
,发送日志,这是我们想要一个队列监听msg-inbox模块上的所有error级别的日志,我们可以通过将新的队列绑定到同一个交换器上queue.bind('msg-inbox-logs','logs-exchange','*.msg-inbox')
那么msg-inbox-logs队列将会接受所有的msg-inbox的日志。每个RabbitMQ都能创建虚拟消息服务器,我们称之为虚拟主机(vhost),每个vhost是一个mini版的RabbitMQ,拥有自己的队列、交换器和绑定等,拥有自己的权限控制。那么我们一个RabbitMQ可以服务多个应用程序,而不用担心程序之间的相互干扰,vhost是RabbitMQ的基本概念,必须在连接时进行指定,由于RabbitMQ采用了开箱即用的默认vhost‘/’,因此使用起来非常方便,默认的用户名密码为guest。
当我们在RabbitMQ创建一个用户的时候,需要为其指定至少一个vhost,并且只能指定 被指派的vhost,vhost之间是绝对隔离的,当我们在RabbitMQ集群上创建vhost时,那么整个集群都会创建该vhost。
创建vhost:
rabbitmqctl add_vhost [hostname]
删除vhost:
rabbitmqctl delete_vhost [hostname]
显示vhost:
rabbitmqctl list_vhosts
当服务器宕机时,存储于RabbitMQ中消息,交换器以及队列等都会不翼而飞,我们需要打开每个队列以及交换器的durable属性,该属性默认为false,这样当服务器重启后,队列以及交换器都会重新进行创建。
但是对于消息来说,在消息发布之前,通过它的“投递模式”选项设置为2来吧消息标志位持久化,但是他只是呗标记为持久化,他还需要被发布到持久化的交换器中,并到达持久化的队列中才行。因此持久化消息需要进行:
只有做到了以上3点,消息才会持久化。RabbitMQ的持久化采用写入磁盘的持久化日志中。
RabbitMQ提供事务的支持,确保生产者发送到RabbitMQ中并持久化到磁盘中,但是效率比较低,不做过多的介绍。
更好的解决方案:
发送方确认模式,需要告诉RabbitMQ将信道设置为confirm模式,而且只能通过重新创建信道来关闭该设置,一旦信道进入confirm模式,所有在信道中发送的信息都会被指定唯一的ID,一定被发送到消息队列,那么会发送一个确认信息给生产者程序包含信息的唯一ID,如果消息队列是可持久的话,那么消息只有在被写入到磁盘后才会返回信息。
发送确认模式采用异步的方式,生产者可以在发送完消息后继续发送下一条,当确认消息最终收到的时候,应用程序会调用回调方法来触发处理该信息,如果发生错误,RabbitMQ会发送nack(not acknowledged,未确认)消息。