应用场景1-“Hello Word”

一个P向queue发送一个message,一个C从该queue接收message并打印。

producer,连接至RabbitMQ Server,声明队列,发送message,关闭连接,退出。

应用场景2-work queues

工作队列(又称:任务队列——Task Queues)是为了避免等待一些占用大量资源、时间的操作。当我们把任务(Task)当作消息发送到队列中,一个运行在后台的工作者(worker)进程就会取出任务然后处理。当你运行多个工作者(workers),任务就会在它们之间共享。

循环调度

使用工作队列的一个好处就是它能够并行的处理队列。如果堆积了很多任务,我们只需要添加更多的工作者(workers)就可以了,扩展很简单。

默认来说,RabbitMQ 会按顺序得把消息发送给每个消费者(consumer)。平均每个消费者都会收到同等数量得消息。这种发送消息得方式叫做——轮询(round-robin)。

消息确认

当处理一个比较耗时得任务的时候,你也许想知道消费者(consumers)是否运行到一半就挂掉。当消息被 RabbitMQ 发送给消费者(consumers)之后,马上就会在内存中移除。这种情况,你只要把一个工作者(worker)停止,正在处理的消息就会丢失。同时,所有发送到这个工作者的还没有处理的消息都会丢失。

我们不想丢失任何任务消息。如果一个工作者(worker)挂掉了,我们希望任务会重新发送给其他的工作者(worker)。

为了防止消息丢失,RabbitMQ 提供了消息响应(acknowledgments)。消费者会通过一个 ack(响应),告诉 RabbitMQ 已经收到并处理了某条消息,然后RabbitMQ 就会释放并删除这条消息。

如果消费者(consumer)挂掉了,没有发送响应,RabbitMQ 就会认为消息没有被完全处理,然后重新发送给其他消费者(consumer)。这样,即使工作者(workers)偶尔的挂掉,也不会丢失消息。

消息是没有超时这个概念的;当工作者与它断开连的时候,RabbitMQ 会重新发送消息。这样在处理一个耗时非常长的消息任务的时候就不会出问题了。

消息响应默认是开启的。可以使用 no_ack=True 标识把它关闭。当工作者(worker)完成了任务,就发送一个响应。

忘记确认

一个很容易犯的错误就是忘了 basic_ack,后果很严重。消息在你的程序退出之后就会重新发送,如果它不能够释放没响应的消息,RabbitMQ 就会占用越来越多的内存。

消息持久化

如果你没有特意告诉 RabbitMQ,那么在它退出或者崩溃的时候,将会丢失所有队列和消息。为了确保信息不会丢失,有两个事情是需要注意的:我们必须把“队列”和“消息”设为持久化。

注意:消息持久化

将消息设为持久化并不能完全保证不会丢失。 RabbitMq 要把消息存到硬盘,但从 RabbitMq 收到消息到保存之间还是有一个很小的间隔时间。因为 RabbitMq 并不是所有的消息都使用 fsync(2) ——它有可能只是保存到缓存中,并不一定会写到硬盘中。并不能保证真正的持久化,但已经足够应付我们的简单工作队列。如果你一定要保证持久化,你需要改写你的代码来支持事务(transaction)。

公平调度

你应该已经发现,它仍旧没有按照我们期望的那样进行分发。比如有两个工作者(workers),处理奇数消息的比较繁忙,处理偶数消息的比较轻松。然而RabbitMQ 并不知道这些,它仍然一如既往的派发消息。

这时因为 RabbitMQ 只管分发进入队列的消息,不会关心有多少消费者(consumer)没有作出响应。它盲目的把第 n-th 条消息发给第 n-th 个消费者。

RabbitMQ 6种业务场景_第1张图片

我们可以使用 basic.qos 方法,并设置 prefetch_count=1。这样是告诉RabbitMQ,再同一时刻,不要发送超过1条消息给一个工作者(worker),直到它已经处理了上一条消息并且作出了响应。这样,RabbitMQ 就会把消息分发给下一个空闲的工作者(worker)。

关于队列大小

如果所有的工作者都处理繁忙状态,你的队列就会被填满。你需要留意这个问题,要么添加更多的工作者(workers),要么使用其他策略。

应用场景3-Publish/Subscribe

为了描述这种模式,我们将会构建一个简单的日志系统。它包括两个程序——第一个程序负责发送日志消息,第二个程序负责获取消息并输出内容。

在我们的这个日志系统中,所有正在运行的接收方程序都会接受消息。我们用其中一个接收者(receiver)把日志写入硬盘中,另外一个接受者(receiver)把日志输出到屏幕上。

最终,日志消息被广播给所有的接受者(receivers)。

交换机(Exchanges)

我们发送消息到队列并从中取出消息。介绍 RabbitMQ 中完整的消息模型。

让我们简单的概括一下之前的教程:

  • 发布者(producer)是发布消息的应用程序。

  • 队列(queue)用于消息存储的缓冲。

  • 消费者(consumer)是接收消息的应用程序。

RabbitMQ 消息模型的核心理念是:发布者(producer)不会直接发送任何消息给队列。事实上,发布者(producer)甚至不知道消息是否已经被投递到队列。

发布者(producer)只需要把消息发送给一个交换机(exchange)。交换机非常简单,它一边从发布者方接收消息,一边把消息推送到队列。交换机必须知道如何处理它接收到的消息,是应该推送到指定的队列还是是多个队列,或者是直接忽略消息。这些规则是通过交换机类型(exchange type)来定义的。

RabbitMQ 6种业务场景_第2张图片

有几个可供选择的交换机类型:直连交换机(direct), 主题交换机(topic), (头交换机)headers和 扇型交换机(fanout)。我们在这里主要说明最后一个 —— 扇型交换机(fanout)

扇型交换机(fanout)很简单,你可能从名字上就能猜测出来,它把消息发送给它所知道的所有队列。交换器列表

这个列表中有一些叫做 amq.* 的交换器。这些都是默认创建的,不过这时候你还不需要使用他们。

匿名的交换器

我们对交换机一无所知,但仍然能够发送消息到队列中。因为我们使用了命名为空字符串 ("") 默认的交换机。

exchange 参数就是交换机的名称。空字符串代表默认或者匿名交换机:消息将会根据指定的 routing_key 分发到指定的队列。

临时队列

给一个队列命名是很重要的——我们需要把工作者(workers)指向正确的队列。如果你打算在发布者(producers)和消费者(consumers)之间共享同队列的话,给队列命名是十分重要的。

首先,当我们连接上 RabbitMQ 的时候,我们需要一个全新的、空的队列。我们可以手动创建一个随机的队列名,或者让服务器为我们选择一个随机的队列名(推荐)。

第二步,当与消费者(consumer)断开连接的时候,这个队列应当被立即删除。exclusive 标识符即可达到此目的。

绑定(Bindings)

10.png

我们已经创建了一个扇型交换机(fanout)和一个队列。现在我们需要告诉交换机如何发送消息给我们的队列。交换器和队列之间的联系我们称之为绑定(binding)。

绑定(binding)列表

你可以使用 rabbitmqctl list_bindings 列出所有现存的绑定。

RabbitMQ 6种业务场景_第3张图片

最重要的改变就是我们把消息发送给 logs 交换机而不是匿名交换机。在发送的时候我们需要提供 routing_key参数,但是它的值会被扇型交换机(fanout exchange)忽略。

应用场景4-Routing

绑定(Bindings)

绑定(binding)是指交换机(exchange)和队列(queue)的关系。可以简单理解为:这个队列(queue)对这个交换机(exchange)的消息感兴趣。

绑定的时候可以带上一个额外的 routing_key 参数。为了避免与basic_publish的参数混淆,我们把它叫做绑定键(binding key)。

绑定键的意义取决于交换机(exchange)的类型。我们之前使用过的扇型交换机(fanout exchanges)会忽略这个值。

直连交换机(Direct exchange)

我们也许只是希望将比较严重的错误(error)日志写入磁盘,以免在警告(warning)或者信息(info)日志上浪费磁盘空间。

我们使用的扇型交换机(fanout exchange)没有足够的灵活性 —— 它能做的仅仅是广播。

我们将会使用直连交换机(direct exchange)来代替。路由的算法很简单 —— 交换机将会对绑定键(binding key)和路由键(routing key)进行精确匹配,从而确定消息该分发到哪个队列。

下图能够很好的描述这个场景:

RabbitMQ 6种业务场景_第4张图片

在这个场景中,我们可以看到直连交换机 X 和两个队列进行了绑定。第一个队列使用 orange 作为绑定键,第二个队列有两个绑定,一个使用 black 作为绑定键,另外一个使用 green。

这样以来,当路由键为 orange 的消息发布到交换机,就会被路由到队列 Q1。路由键为 black 或者 green 的消息就会路由到 Q2。其他的所有消息都将会被丢弃。

多个绑定(Multiple bindings)

RabbitMQ 6种业务场景_第5张图片

多个队列使用相同的绑定键是合法的。这个例子中,我们可以添加一个 X 和 Q1 之间的绑定,使用 black 绑定键。这样一来,直连交换机就和扇型交换机的行为一样,会将消息广播到所有匹配的队列。带有 black 路由键的消息会同时发送到 Q1 和 Q2。

发送日志

我们将会发送消息到一个直连交换机,把日志级别作为路由键。这样接收日志的脚本就可以根据严重级别来选择它想要处理的日志。

订阅

处理接收消息的方式和之前差不多,只有一个例外,我们将会为我们感兴趣的每个严重级别分别创建一个新的绑定。

RabbitMQ 6种业务场景_第6张图片

应用场景5-topic

主题交换机

发送到主题交换机(topic exchange)的消息不可以携带随意什么样子的路由键(routing_key),它的路由键必须是一个由.分隔开的词语列表。这些单词随便是什么都可以,但是最好是跟携带它们的消息有关系的词汇。以下是几个推荐的例子:"stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit"。词语的个数可以随意,但是不要超过 255 字节。

绑定键也必须拥有同样的格式。主题交换机背后的逻辑跟直连交换机很相似 —— 一个携带着特定路由键的消息会被主题交换机投递给绑定键与之想匹配的队列。但是它的绑定键和路由键有两个特殊应用方式:

  • (星号) 用来表示一个单词.

  • (井号) 用来表示任意数量(零个或多个)单词。

下边用图说明:

RabbitMQ 6种业务场景_第7张图片

这个例子里,我们发送的所有消息都是用来描述小动物的。发送的消息所携带的路由键是由三个单词所组成的,这三个单词被两个.分割开。路由键里的第一个单词描述的是动物的手脚的利索程度,第二个单词是动物的颜色,第三个是动物的种类。所以它看起来是这样的: ..。

我们创建了三个绑定:Q1 的绑定键为 .orange.,Q2 的绑定键为 ..rabbit 和 lazy.# 。

这三个绑定键被可以总结为:

  • Q1 对所有的桔×××动物都感兴趣。

  • Q2 则是对所有的兔子和所有懒惰的动物感兴趣。

一个携带有 quick.orange.rabbit 的消息将会被分别投递给这两个队列。携带着 lazy.orange.elephant 的消息同样也会给两个队列都投递过去。另一方面携带有 quick.orange.fox 的消息会投递给第一个队列,携带有 lazy.brown.fox 的消息会投递给第二个队列。携带有 lazy.pink.rabbit 的消息只会被投递给第二个队列一次,即使它同时匹配第二个队列的两个绑定。携带着 quick.brown.fox 的消息不会投递给任何一个队列。

如果我们违反约定,发送了一个携带有一个单词或者四个单词("orange" or "quick.orange.male.rabbit")的消息时,发送的消息不会投递给任何一个队列,而且会丢失掉。

但是另一方面,即使 "lazy.orange.male.rabbit" 有四个单词,他还是会匹配最后一个绑定,并且被投递到第二个队列中。

主题交换机

主题交换机是很强大的,它可以表现出跟其他交换机类似的行为

当一个队列的绑定键为 "#"(井号) 的时候,这个队列将会无视消息的路由键,接收所有的消息。

当 * (星号) 和 # (井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为。

应用场景6-PRC

关于 RPC 的注意事项

尽管 RPC 在计算领域是一个常用模式,但它也经常被诟病。当一个问题被抛出的时候,程序员往往意识不到这到底是由本地调用还是由较慢的 RPC 调用引起的。同样的困惑还来自于系统的不可预测性和给调试工作带来的不必要的复杂性。跟软件精简不同的是,滥用 RPC 会导致不可维护的面条代码.

考虑到这一点,牢记以下建议:

确保能够明确的搞清楚哪个函数是本地调用的,哪个函数是远程调用的。给你的系统编写文档。保持各个组件间的依赖明确。处理错误案例。明确客户端该如何处理 RPC 服务器的宕机和长时间无响应情况。

当对避免使用 RPC 有疑问的时候。如果可以的话,你应该尽量使用异步管道来代替RPC 类的阻塞。结果被异步地推送到下一个计算场景。

回调队列

一般来说通过 RabbitMQ 来实现 RPC 是很容易的。一个客户端发送请求信息,服务器端将其应用到一个回复信息中。为了接收到回复信息,客户端需要在发送请求的时候同时发送一个回调队列(callback queue)的地址。

消息属性

AMQP 协议给消息预定义了一系列的14个属性。大多数属性很少会用到,除了以下几个:

  • delivery_mode(投递模式):将消息标记为持久的(值为2)或暂存的(除了2之外的其他任何值)。

  • content_type(内容类型):用来描述编码的 mime-type。例如在实际使用中常常使用 application/json 来描述 JOSN 编码类型。

  • reply_to(回复目标):通常用来命名回调队列。

  • correlation_id(关联标识):用来将RPC的响应和请求关联起来。

关联标识

上边介绍的方法中,我们建议给每一个 RPC 请求新建一个回调队列。这不是一个高效的做法,幸好这儿有一个更好的办法 —— 我们可以为每个客户端只建立一个独立的回调队列。

这就带来一个新问题,当此队列接收到一个响应的时候它无法辨别出这个响应是属于哪个请求的。correlation_id 就是为了解决这个问题而来的。我们给每个请求设置一个独一无二的值。当我们从回调队列中接收到一个消息的时候,我们就可以查看这条属性从而将响应和请求匹配起来。如果我们接手到的消息的correlation_id 是未知的,那就直接销毁掉它,因为它不属于我们的任何一条请求。

你也许会问,为什么我们接收到未知消息的时候不抛出一个错误,而是要将它忽略掉?这是为了解决服务器端有可能发生的竞争情况。尽管可能性不大,但RPC服务器还是有可能在已将应答发送给我们但还未将确认消息发送给请求的情况下死掉。如果这种情况发生,RPC 在重启后会重新处理请求。这就是为什么我们必须在客户端优雅的处理重复响应,同时RPC也需要尽可能保持幂等性。

总结

RabbitMQ 6种业务场景_第8张图片

我们的 RPC 如此工作:

  • 当客户端启动的时候,它创建一个匿名独享的回调队列。

  • 在 RPC 请求中,客户端发送带有两个属性的消息:一个是设置回调队列的 reply_to 属性,另一个是设置唯一值的 correlation_id 属性。

  • 将请求发送到一个 rpc_queue 队列中。

  • RPC 工作者(又名:服务器)等待请求发送到这个队列中来。当请求出现的时候,它执行他的工作并且将带有执行结果的消息发送给 reply_to 字段指定的队列。

  • 客户端等待回调队列里的数据。当有消息出现的时候,它会检查 correlation_id 属性。如果此属性的值与请求匹配,将它返回给应用。 整合到一起