第二章 理解消息通信

AMQP消息能以1对多的广播方式进行路由,也可以选择1对1的方式路由。应用程序(客户端)可以发送和接收包裹(数据),而数据所在的服务器也可以发送和接收。RabbitMQ在应用程序和服务器之间扮演着路由器的角色。所以当应用程序连接到RabbitMQ时,它就必须做个决定:我是发送还是接收呢?或者从AMQP的角度思考,我是一个生产者还是一个消费者呢?

让我们忘记根植于脑海的客户端-服务器模型,从现在开始熟悉生产者-消费者的概念吧。

 

一、生产者-消费者

消息传递的整个过程:生产者创建消息,然后发布(发送)给代理服务器(即RabbitMQ),RabbitMQ根据标签把消息发送给感兴趣的接收方。这种通讯方式是一种“发后即忘”的单向方式。

你的应用既可以作为生产者向其他应用程序发送消息。也可以作为一个消费者接收并处理消息。或者可以在两者之间进行切换。

 

1、消息Message

包含两部分内容:有效载荷和消息标签。

有效载荷:你想要传输的实际数据(JSON或者你喜欢的任何格式,最终都以字节数组形式发送到RabbitMQ)。

消息标签:它描述有效载荷,RabbitMQ根据它决定谁(哪个交换器)将获得消息的副本。具体来讲就是一个交换器名+主题(可选)。

 

2、生产者Producer

创建消息(有效载荷和消息标签),然后发布(发送到)RabbitMQ。

 

3、绑定Bind

RabbitMQ如何解释标签。

 

4、消费者Consumer

连接到RabbitMQ,并订阅到队列(Queue)上。把消息队列想象成一个具名信箱,每当消息到达特性信箱时,RabbitMQ会将其(从信箱)发送到其中一个订阅的消费者。当消费者接收到消息时,它只得到消息的有效载荷,消息的标签不再随有效载荷一同传递。

 

5、信道Channel

TCP连接是物理连接,信道是RabbitMQ建立在TCP连接上的虚拟连接。一个TCP连接上可以创建多个信道,每个线程可以通过其独占的信道并行地利用(共享)同一条TCP连接。下面通过比较3种通讯方式理解信道的优越性。

通讯有这样几个方式:

1>、每个线程各自创建自己的TCP连接,专线专用,用时创建用完立即销毁。

缺点:(1)操作系统提供的TCP连接数量有限(socket)。这么做浪费连接资源。

      线程如果长时间占用连接,会降低连接利用效率。

   (2)线程如果频繁创建和销毁TCP连接,会严重增加系统开销。

2>、多个线程共用一个TCP连接(非信道方式),轮流使用。

缺点:如果有一个线程被耗时任务阻塞,其他线程可能得不到TCP连接的使用权,进而都会阻塞等待TCP空闲可用。

 

3>、使用Rabbit的信道方式(也是多线程共用一条TCP连接,但加入信道概念)

各个线程使用不同的信道,这些信道在物理上共用同一个TCP连接。(至于如何调度,RabbitMQ来解决)

优点:各个线程并行消息传递;创建和关闭信道不消耗系统资源(因为信道是虚拟连接);理论上在一个TCP连接上可以创建无限个信道。

 

总之,使用信道你能够根据应用需要,尽可能多地创建并行的传输层,而不会被TCP连接约束所限制。

 

二、从底部开始构造:队列

 

1、队列Queue

队列就像具名信箱,消息在RabbitMQ上最终到达队列中,并等待消费。

 

2、RabbitMQ消息路由3要素:交换器、队列、绑定

具体说生产者把消息发布到交换器上;信息最终到达队列,并被消费者接收;绑定决定了消息如何从交换器路由到特定的队列上。

 

3、消费者从队列获得消息

(1)订阅:消费者通过BasicConsume命令订阅。这样做会将信道置为接收模式,直到取消对队列的订阅为止。

(2)获取单条:消费者通过BasicGet命令向队列请求单条消息。

注意不要将该命令放在循环中来替代BasicConsume。因为每次BasicGet,Rabbit实际上做法是:订阅-> 获得单条 ->取消订阅。如果希望放在循环里达到不断接收消息的效果,会严重影响RabbitMQ性能。就是脱裤子放气——多此一举且瞎耽误功夫。

 

4、消息的分发(一个队列上)

(1)无消费者订阅时,消息在队列上等待被消费。如果设置了最大长度限制,超限后队首消息被抛弃,新消息放入队尾。如果不设置最大长度限制,撑爆RabbitMQ服务器为止。

(2)多个消费者订阅时,队列以轮转(round-robin)方式把消息发给消费者。每条消息只会发给一个订阅的消费者,下一条消息发给另一个订阅的消费者...

 

5、消息的确认和拒绝

(1)消息的确认:

  消费者接收到的每条消息都必须确认,要么显式地向RabbitMQ发送一个确认(通过BasicAck命令);要么在订阅到队列时就将autoAck参数设置为true,这样一收到消息RabbitMQ自动视为已确认。。每当消息被确认后(显式或自动),RabbitMQ将消息从队列中删除。

(2)消息确认前从RabbitMQ断开连接(或者从队列上取消订阅):

  RabbitMQ会认为这条消息没有分发,然后重新分发给下一个订阅的消费者。如果你的应用程序(消费者)在消息确认前崩溃了,这样做可以确保消息会发给另一个消费者进行处理。另一方面如果应用程序有bug忘记确认消息的话,RabbitMQ将不会给该消费者发送更多消息,因为在上一条消息被确认之前,RabbitMQ会认为这个消费者并没有准备好接收下一条消息。 

  你可以好好利用后一点:如果处理消息内容非常耗时,你的应用程序拖延了确认,直到处理完成这段时间内RabbitMQ不会持续不断地把后续消息丢给你,也就是说你的应用程序不会因为不断涌入的消息而超负荷。

 

(3)消息的拒绝

  因为某些原因你希望拒绝RabbitMQ发送的消息,可以使用BasicReject命令。如果把该命令的requeue参数设置为true,表示告诉RabbitMQ把该消息重新发送给下一个订阅的消费者(这不是我的菜,让给别人吃吧)。如果requeue参数设置为false,RabbitMQ立即把消息从队列删除(放入死信队列,供你debug时使用),而不会再发给新的消费者。

  还有一种情况:如果消息格式错误(比如说你无法解析),你确信任何一个消费者都不可能处理,那就直接ack确认这条消息而不进行任何处理(记入log日志)就OK了;不要BasicReject + requeue参数设置为true,因为这样RabbitMQ会轮转发给其他消费者,浪费服务器资源。

 

注意:这里讲的确认,是消费者对RabbitMQ进行的消息确认,而不是告诉生产者消息已经被接收。

 

6、队列的声明

(1)生产者和消费者都可以用QueueDeclare命令来创建队列。

(2)如果声明的队列之前已存在,只要声明的参数完全匹配,RabbitMQ实际什么也不做,并成功返回,就好像这个队列被创建成功一样。如果参数不匹配,该命令会返回错误。

(3)怎样测试队列是否存在?调用QueueDeclare命令,但是passive参数要设置为true,这样如果队列存在该命令会成功返回;如果不存在,该命令不会创建队列而是返回一个错误。

(4)简单起见,你的生产者和消费者最好都尝试用QueueDeclare命令创建队列。

 

三、联合起来:交换器和绑定

消息是如何从交换器到达队列的呢?

1、路由键routing key

  消息从交换器投递到哪个队列的规则就是路由键。即队列通过路由键绑定到交换器,当你把消息发送给RabbitMQ,消息将拥有一个路由键——即便是空的——RabbitMQ也会将其和绑定使用的路由键进行匹配。如果匹配消息将会投递到该队列。

 

2、交换器分类

  对于一个交换器绑定一个队列的情况,我们只讨论消息如何根据路由键路由到队列。(注意一个队列可以被多个消费者消费,这里讨论的是交换器与队列)

  对于一个交换器需要投递多个队列的情况,我们需要同时讨论交换器的分类+路由键。

(1)direct

  服务器必须实现direct交换器,包含一个空白字符串名称的默认交换器。当声明一个队列时,它会自动绑定到默认交换器,并以队列名称作为路由键。这意味着你可以使用下图下面的代码发送消息到之前声明的队列去。

  当默认的direct交换器无法满足应用程序需求时,你可以声明你自己的交换器。只需发送exchange.declare命令并设置适合的参数就行了。(这种情况再发布消息,就需要指定交换器名称了)

如上图代码,第一个参数是你要发的消息;第二个参数是空字符串(即你要用的默认交换器);第三个参数是路由键,也就是之前声明队列时的队列名。如果你希望通过一个direct交换器把不同的消息路由给不同队列(比如香蕉消息,只发给香蕉队列),发布消息时注意指定队列名。

 

(2)fanout

这种类型的交换器会将收到的消息广播到绑定的队列上。当你发布一条消息到fanout交换器,它会把消息投递给所有附加在此交换器上的队列。这允许你对单条消息做不同方式的反应。(比如不同任务的消费者订阅不同的队列,每个队列得到的消息副本都是一样的)

具体步骤:先声明交换器(指定参数的时候,类型是fanout);再声明新的队列;再声明绑定,将该队列绑定到交换器。

 

 

(3)topic

该类交换器允许你实现来自不同源头(发布者)的消息能够到达同一个队列。

 

以日志系统为例:假如你拥有多个不同日志级别:error、info、warning同时你有三个模块A、B、C发布自己的日志,它们都发布到topic交换器‘logs-exchange’上。现在你创建了3个队列:X队列只接收来自A模块的info消息,Y队列接收来自B模块的所有消息,Z队列接收所有模块的所有消息。

$channel->queue_bind('X'                //队列名
                     'logs-exchange',   //交换器名
                     'info.A')          //主题:接收来自A模块的info消息
                     
$channel->queue_bind('Y'                //队列名
                     'logs-exchange',   //交换器名
                     '*.B')             //主题:接收来自B模块的所有消息
                     
$channel->queue_bind('Z'                //队列名
                     'logs-exchange',   //交换器名
                     '#')               //主题:接收来自所有模块的所有消息

注意:点句号用来把路由键分为多个部分(主题),*符号匹配特定位置的任意文本,#匹配所有规则。

 

四、多租户模式:虚拟主机和隔离

1、vhost虚拟主机

  每一个RabbitMQ服务器上都能创建若干个vhost虚拟主机。每个vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的交换器、队列、绑定等等,更重要的它拥有自己的权限管理机制。这使得你能够安全地使用一个RabbitMQ来服务众多应用程序。

  vhost概念带来的好处:

1、将同一个RabbitMQ的众多客户端区分开。保证了安全性。比如不能跨vhost访问队列。

2、避免交换器和队列的命名冲突,又保证可移植性。这样从一个RabbitMQ服务器移植到另一个时,变得简单易行。

 

2、默认vhost名: "/"

默认账户guest,

默认密码guest,出于安全起见,你应该改密码

 

3、创建新的vhost

在RabbitMQ安装目录下用命令行执行rabbitmqctl.exe

执行:rabbitmqctl add_vhost  [主机名]   创建一个虚拟主机。

执行:rabbitmqctl delete_vhost  [主机名]   删除一个虚拟主机。

执行:rabbitmqctl list_vhosts 列出RabbitMQ上运行着哪些虚拟主机。

一旦vhost创建成功,你就可以连接上去开始添加队列、交换器了。

 

 

五、我的消息去了哪里?持久化和你的策略

  默认情况下创建的交换器、队列、绑定,连同队列里的消息,都不是持久化的。也就是说重启RabbitMQ服务后这些都会消失。

1、持久化的交换器和队列

  创建持久化的交换器和队列,你需要在它们的声明命令中把durable参数设置为true。(默认值为false)。这样一来,你创建的交换器和队列在服务器故障恢复、或断电重启之后依然存在。

 

2、消息的持久化

  能从RabbitMQ服务器崩溃、或者重启后恢复的消息,我们称之为持久化消息。发布持久化的消息,我们需要发布者执行BasicPublic命令时,将“投递模式”delivery mode参数设置为2(在RabbitMQ.Net Client等客户端库里都有枚举或常量等方便使用),然后发送到持久化的交换器,最后到达持久化的队列。这样才是真正的持久化消息。

简单说持久化消息需要以下3个条件:

(1)消息发布时,投递模式指定为2(持久的)

(2)发送到持久化的交换器(声明交换器时,要指定为持久化的)

(3)到达持久化的队列(声明队列时,要指定为持久化的)

以上3条缺一不能算是持久化消息,都不能从宕机或重启RabbitMQ服务后恢复。

 

3、消息持久化消耗RabbitMQ性能

  为了持久化消息,RabbitMQ在硬盘(持久化日志)中保存消息,并有一套写入、标记、垃圾回收的机制。使用消息持久化机制,会降低RabbitMQ消息吞吐量。

 

4、事务模式(性能太低,不建议使用)

  在AMQP中,在把信道设置成事务模式后,你通过信道发送那些想要确认的消息,之后还有多个其他AMQP命令。这些命令是否执行取决于第一条消息发送是否成功。一旦你发送完所有命令,就可以提交事务了。如果事务中的首次发布成功了,那么信道会在事务中完成其他AMQP命令,如果发送失败的话,其他AMQP命令将不会执行。

  事务的缺点:几乎吸干了RabbitMQ的性能;造成生产者应用程序同步(等待RabbitMQ处理事务)。本来我们使用消息队列的目的之一就是要消除同步。

 

 

5、发送方确认模式publisher confirm(性能好,建议使用)

发送方确认模式是RabbitMQ独有的针对AMQP的扩展。

  使用该模式,你要告诉RabbitMQ将信道设置为Confirm模式,而且你只能通过重新创建信道来关闭该Confirm设置。一旦信道进入Confirm模式,所有在信道上发布的消息都会被指派一个唯一ID号(从1开始)。一旦消息被投递给所有匹配的队列后,信道会发送一个确认给生产者(包含消息的唯一ID号)。这使得生产者知晓消息已经安全到达目的队列了。如果消息和队列都是持久化的,那么确认消息只会在队列将消息写入磁盘后才会发出。

  发送方确认的好处:

(1)异步:一旦发布了一条消息,生产者应用程序就可以在等待确认的同时继续发送下一条。当确认消息最终收到的时候,生产者应用的回调方法就会被触发来处理该确认消息。

 (2)轻量:由于没有消息回滚的概念(同事务相比),因此发送方确认模式更加轻量级,同时对RabbitMQ代理服务器的性能影响几乎可以忽略不计。

 

 

六、消息的生命周期

1、生产者

(1)连接到RabbitMQ

(2)获取信道

(3)声明交换器

(4)创建消息

(5)发布消息

(6)关闭信道

(7)关闭连接

 

2、消费者

(1)连接到RabbitMQ

(2)获得信道

(3)声明交换器

(4)声明队列

(5)把队列和交换器绑定起来

(6)消费消息

(7)关闭信道

(8)关闭连接

 

七、使用发送方确认模式来确认投递

1、消息ID的唯一性,是对于每次建立的信道而言的。

  当信道设置成Confirm模式时,发布的每一条消息都会获得唯一的ID。由于一条信道只能被单个线程使用,因而可以确保信道上发布的消息都是连续的。任一信道上发布的第一条消息将获得ID为1,并且该信道接下来每条消息的ID都将步进1。

 

2、我怎样知道哪条消息丢失了?

  你自己创建计数器,信道自创建后发布第一条消息开始计数,计数器每次加1。自己将发布的消息和计数对应保存。(比如存到字典里)

收到确认(ack或nack)后,根据确认中的frame.method.delivery_tag(即ID号),去自己的字典查找,就知道哪条消息丢失了。

 

posted on 2017-12-05 21:11  困兽斗  阅读(235)  评论(0)    收藏  举报

导航