学习RabbitMQ

https://medium.com/ryans-dev-notes/learning-rabbitmq-3f59d11f66b4

我们正在将软件体系结构转换为微服务,在这些微服务上进行通信的一种方法是使用消息代理。所以,几周前,我们做了一个关于RabbitMQ的研发。我们的目标是理解什么是RabbitMQ,它是如何工作的,并最终确定如何在我们的微服务上实现RabbitMQ。我们正在转向微服务体系结构,以使我们的系统更具可伸缩性。在以前的体系结构中,我们使用Redis作为消息代理。Redis易于设置、使用和部署,但根据我所了解,RabbitMQ是实现更可伸缩软件的方法。除了可伸缩性问题,Redis作为消息代理还存在以下问题:

  • 默认情况下不支持TLS。在Redis中,可以通过隧道策略来保护消息和连接。Redis推荐Spiped。
  • 仅支持基本消息队列和路由。
  • Redis,发布者或消费者崩溃时,邮件丢失率很高。
  • 处理大型消息时的高延迟。Redis更适合小消息。
  • Redis是使用不同的意图,内存中的键值数据库构建的,而不是作为消息代理。

我们希望通过在我们的微服务架构上实现RabbitMQ,我们可以解决并防止这些问题。


什么是RabbitMQ

RabbitMQ是一个最初实现高级消息队列协议(AMQP)的消息代理,但现在它通过插件支持不同的消息传递协议。AMQP是在应用程序或组织之间传递业务消息的开放标准。AMQP标准的设计具有以下主要特征:安全性,可靠性,互操作性,标准,开放性。那么RabbitMQ如何实现这个特性:

  • 安全性 - 通过RabbitMQ插件支持身份验证,授权,LDAP和TLS。
  • 可靠性 - 确认消息已成功传递到消息代理并确认消息已成功处理。RabbitMQ还具有内置群集功能,可实现高可用性和可伸缩性。还有一个选项可以使您的数据持久化,以便在代理退出或崩溃时不会丢失消息。
  • 互操作性 - 消息作为字节流传输,因此任何客户端都可以对其进行操作。RabbitMQ 以不同的编程语言支持许多客户端库和开发工具。
  • 开放和标准 - 除了遵循AMQP的开放标准,RabbitMQ是开源的,任何人都可以做出贡献,使其更好。

RabbitMQ架构

首先,让我们看看我们如何将Redis实现为消息代理,它遵循以下过程:

  1. 应用程序将消息发布到消息代理,在本例中为Redis。消息被直接推送到队列中。
  2. 消息存储在队列中,等待消费者从相同或不同的应用程序中使用。
  3. 消费者使用队列中的消息。消息被消耗的那一刻,它将从队列中删除。请注意,在这一部分,消费者是从队列中检索消息的消费者。
  4. 如果消费者未能处理消息,则消费者将消息推送到队列,并且该过程将从步骤2开始重复。

这个过程非常简单明了,但它很脆弱,不灵活,难以扩展。它无法处理这些情况:

  • 如何确保消息已成功发布到消息代理?
  • 如果Redis崩溃怎么办?路由到队列的消息很可能会消失,并且没有可用的消息代理来处理传入和传出的消息。
  • 如果消费者崩溃从队列中消耗消息的时间,该怎么办?邮件不会重新排队。
  • 如果我想将消息发布到多个队列或符合一组条件的队列,该怎么办?为此,我们需要手动修改代码库。

上面的那些案例可以通过RabbitMQ轻松解决,而且难以实现。但首先,让我们看看RabbitMQ消息代理如何工作:

  1. 应用程序将消息发布到消息代理,在本例中为RabbitMQ。邮件被推送到Exchange而不是队列。
  2. Exchange将邮件路由到绑定到Exchange的队列或队列。
  3. RabbitMQ消息代理可以通知发布者,如果消息已成功路由到队列或队列,并且无法路由消息,则Exchange可以通知发布者消息无法路由。在此失败的方案中,发布者可以选择是否重新发布消息。
  4. 消息存储在等待活动使用者的队列中,如果有任何活动使用者,则消息代理将消息从队列传递给活动使用者。
  5. 消费者使用消息代理从队列发送的消息。使用者可以自动或手动向消息代理发送确认消息已成功处理,并且可以从队列中安全地删除消息。

这是RabbitMQ消息代理的高级方法和体系结构。与Redis作为消息代理相比。RabbitMQ有一个额外的组件,即将消息路由到队列或队列的Exchange。此外,RabbitMQ提供了一种对数据安全至关重要的机制。我们可以保证消息成功路由到队列或队列,否则我们可以选择重新发布消息,我们可以保证消息成功处理消息,否则我们可以重新排队消息以便消费由其他消费者。通过理解这种方法和架构,我们可以得出结论,RabbitMQ不仅简单而且是一个强大的消息代理。


代码示例

这些代码示例最初来自RabbitMQ教程,我只是进行了一些修改,因此我们可以使用RabbitMQ创建一个健壮的应用程序。此外,这些代码是使用Python Pika RabbitMQ Client编写的。我们将逐行剖析这些代码,以便更好地理解RabbitMQ的工作原理。

发布者示例:

import pika
import sys

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='direct_exchange', exchange_type='direct')
channel.queue_declare(queue='direct_queue', durable=True)
channel.queue_bind(exchange='direct_exchange', queue="direct_queue", routing_key="direct.routing.key")

message = " ".join(sys.argv[1:]) or "Hello World!"

channel.confirm_delivery()
try:
    channel.basic_publish(exchange='direct_exchange', routing_key='direct.routing.key',
                          body=message, properties=pika.BasicProperties(delivery_mode=2)
                          )

    print("Sent %r" % message)
except pika.exceptions.UnroutableError:
    print("Failed to send message %r" % message)
connection.close()
connection = pika.BlockingConnection(pika.ConnectionParameters(‘localhost’)) 

在line1-2上导入所需的包之后。第4行,我们创建一个RabbitMQ连接实例,此连接使用TCP作为协议。TCP协议仅处理数据包(数据位)并启用两个主机之间的连接,以便它们可以交换数据。此外,TCP还保证按照发送消息的顺序传递消息。

channel = connection.channel()

第5行,我们创建一个频道,所有客户端操作都在一个频道上进行。我们可以在一个连接中拥有多个通道。这背后的原因是:某些应用程序需要与代理程序的多个逻辑连接。但是,不希望同时打开许多TCP连接,因为这样做会消耗系统资源并使配置防火墙变得更加困难。因此,可以将通道视为“共享单个TCP连接的轻量级连接”。

channel.exchange_declare(exchange=’direct_exchange’,   exchange_type=’direct’)

第7行,我们创建了一个Exchange。正如我们之前讨论的那样,Exchange的职责是将消息路由到队列或队列。Exchange根据指定的路由密钥知道在何处路由邮件。我们用两个参数声明我们的Exchange:exchange- 交换的名称,以及exchange_type- 交换的类型控制消息的路由方式。交换有四种类型:

  • 直接交换 - 根据消息路由密钥将消息传递到队列。
  • fanout交换 - 将消息路由到绑定到它的所有队列,并忽略路由密钥。
  • 主题交换 - 根据消息路由密钥与用于将队列绑定到交换的模式之间的匹配,将消息路由到一个或多个队列。路由键遵循此模式..,并找到匹配,我们使用*(星号)代替正好一个单词和#(哈希)来代替零个或多个单词。
  • headers exchange - 用于在多个属性上进行路由,这些属性比路由密钥更容易表示为消息头。
channel.queue_declare(queue=’direct_queue’, durable=True)

第8行,我们创建一个队列——队列名称和持久作为参数。当RabbitMQ退出或崩溃时,它将忘记队列和消息,除非您告诉它不要这样做。通过将队列设置为持久队列,我们可以确保即使RabbitMQ退出或崩溃,我们的队列也不会被删除。

channel.queue_bind(exchange='direct_exchange', queue="direct_queue", routing_key="direct.routing.key")

第9行,我们将队列绑定到交换机并指定路由密钥。因此,Exchange现在知道根据指定的路由密钥和交换类型路由消息的位置。

channel.confirm_delivery()

第13行,我们启用发布确认,通过这样做,如果消息代理无法将消息路由到我们的队列或队列,则会引发错误。请注意,通过启用发布确认,它会增加一点开销,因为消息代理需要确认向发布者传递消息。

try:
    channel.basic_publish(exchange='direct_exchange', routing_key='direct.routing.key',
                          body=message, properties=pika.BasicProperties(delivery_mode=2)
                          )

    print("Sent %r" % message)
except pika.exceptions.UnroutableError:
    print("Failed to send message %r" % message)

在第14-21行,我们向队列发布消息。根据参数,我们告诉发布者将我们的消息(body参数)发布到以direct_exchanged路由键命名的交换direct.routing.key。这些参数除了属性参数外是自解释的。使用其他属性,我们会告知发布商使用其传递消息delivery_mode=2意思是我们想让我们的信息持久化。就像队列一样,消息是非持久性的,除非我们告诉RabbitMQ使其持久化。如果RabbitMQ退出或崩溃,将删除非持久性队列和消息,通过使队列和消息持久化,我们可以确保在RabbitMQ退出或崩溃的情况下队列和消息将继续存在。在第20行,当消息代理无法将消息路由到队列或队列时,我们捕获异常。如果我们想要重新发布消息或删除它,这为我们提供了一个选项。

connection.close()

第22行,我们正在关闭连接。每次发布消息时打开和关闭连接和通道都不是一个好习惯。连接是长期存在的,它需要资源来保持打开和关闭它们。我只是将这一行包含在关于如何关闭连接的示例目的中。

消费者示例:

import pika
import time

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='direct_exchange', exchange_type='direct')
channel.queue_declare(queue='direct_queue', durable=True)
channel.queue_bind(exchange='direct_exchange', queue="direct_queue", routing_key="direct.routing.key")


def callback(ch, method, properties, body):
        print("Received %r" % body)
        time.sleep(body.count(b'.'))
        print("Done")
        ch.basic_ack(delivery_tag=method.delivery_tag)


channel.basic_qos(prefetch_count=1)
channel.basic_consume(callback, queue='direct_queue')

print('Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))                       channel = connection.channel()

第1-9行与发布商应用有相同的解释。对于第4-5行,可以理解我们需要创建一个连接和通道,以便我们可以连接到消息代理。

channel.exchange_declare(exchange='direct_exchange', exchange_type='direct')
channel.queue_declare(queue='direct_queue', durable=True)
channel.queue_bind(exchange='direct_exchange', queue="direct_queue", routing_key="direct.routing.key")

但对于第7-9行,它没有意义,因为我们已经在发布者应用程序中创建并配置了队列。这种冗余背后的原因是我们需要确保队列存在,否则RabbitMQ将丢弃消息。因此从技术上讲,在发布者和消费者应用程序上声明和配置队列被认为是RabbitMQ中的一个好习惯。声明具有相同名称和属性的队列是幂等的,这意味着我们可以多次运行它,但只会创建一个队列。但请注意,如果我们创建一个具有不同属性的相同名称的队列,RabbitMQ将引发错误。

def callback(ch, method, properties, body):
        print("Received %r" % body)
        time.sleep(body.count(b'.'))
        print("Done")
        ch.basic_ack(delivery_tag=method.delivery_tag)

第12-16行是我们的回调函数,这意味着一旦我们从队列中消费消息就会触发这个函数。这个回调函数需要四个参数:

  • ch - 频道实例。
  • method-包括如何将消息传递的细节(例如routing_keyexchangedelivery_tag
  • properties- 我们在发布商处设置的属性(例如delivery_mode
  • body - 我们从队列中消耗的消息。它是以字节为单位的数据类型。
ch.basic_ack(delivery_tag=method.delivery_tag)

第16行,我们确认消息已成功处理消费者,现在可以安全地将其从队列中删除。默认情况下,确认会自动发生。这意味着一旦消费者使用该消息,即使消费者没有处理该消息,也将删除队列中的消息。这种模式通常被称为“即发即忘”。与手动确认模型不同,如果消费者的TCP连接或通道在成功交付之前关闭,则服务器发送的消息将丢失。因此,自动消息确认应被视为不安全,并不适用于所有工作负载。

channel.basic_qos(prefetch_count=1)

第19行,我们使用prefetch=1设置QoS(服务质量),以确保只使用一条消息,并且RabbitMQ在确认当前消息之前不会将任何消息推送给消费者。如果我们不设置任何qos预取,消费者将接受它能够处理的尽可能多的消息,这可能导致瓶颈,因为我们可以在消费者上拥有尽可能多的飞行消息和未确认的消息,而这些消息本应由另一个消费者实例处理。

channel.basic_consume(callback, queue='direct_queue')

在第20行,我们将消费者设置为使用direct_queue和设置我们callback()作为消费者回调。因此,每当消费者从队列中消费消息时,这callback()将自动触发。

channel.start_consuming()

最后,在第23行,我们触发一个等待消息并触发我们的无限循环callback()

现在,通过逐行剖析我们的代码示例,我们可以更好地掌握和理解RabbitMQ如何工作以及如何在我们的应用程序适用时实现这个强大的消息代理。

快乐的编码!


转:https://medium.com/ryans-dev-notes/learning-rabbitmq-3f59d11f66b4

你可能感兴趣的:(学习RabbitMQ)