RabbitMQ中文文档PHP版本(二)--发布/订阅

2019年12月10日10:01:00

原文:https://www.rabbitmq.com/tutorials/tutorial-three-php.html

工作队列

(使用php-amqplib

先决条件

本教程假定RabbitMQ 在标准端口(5672)的本地主机安装并运行如果您使用其他主机,端口或凭据,则连接设置需要进行调整。

在哪里获得帮助

如果您在阅读本教程时遇到困难,可以 通过邮件列表与我们联系。

在第一个教程中,我们编写了程序来发送和接收来自命名队列的消息。在这一部分中,我们将创建一个工作队列,该队列将用于在多个工作人员之间分配耗时的任务。

工作队列(又称任务队列的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反,我们安排任务在以后完成。我们将任务封装 为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当您运行许多工作人员时,任务将在他们之间共享。

这个概念在Web应用程序中特别有用,因为在Web应用程序中,不可能在较短的HTTP请求窗口内处理复杂的任务。

制备

在本教程的上半部分,我们发送了一条包含“ Hello World!”的消息。现在,我们将发送代表复杂任务的字符串。我们没有现实世界的任务,例如要调整大小的图像或要渲染的pdf文件,所以我们假装自己很忙-使用sleep()函数来伪造它我们将字符串中的点数作为它的复杂度。每个点将占“工作”的一秒。例如,Hello ...描述的虚假任务 将花费三秒钟。

我们将稍微修改上一个示例中send.php代码,以允许从命令行发送任意消息。该程序会将任务安排到我们的工作队列中,因此将其命名为 new_task.php

$data = implode(' ', array_slice($argv, 1));
if (empty($data)) {
    $data = "Hello World!";
}
$msg = new AMQPMessage($data);

$channel->basic_publish($msg, '', 'hello');

echo ' [x] Sent ', $data, "\n";

我们旧的receive.php脚本也需要进行一些更改:它需要为消息正文中的每个点伪造一秒钟的工作。它会从队列中弹出消息并执行任务,因此我们将其称为worker.php

$callback = function ($msg) {
  echo ' [x] Received ', $msg->body, "\n";
  sleep(substr_count($msg->body, '.'));
  echo " [x] Done\n";
};

$channel->basic_consume('hello', '', false, true, false, false, $callback);

请注意,我们的假任务模拟执行时间。

按照教程一运行它们:

# shell 1
php worker.php
# shell 2
php new_task.php "A very hard task which takes two seconds.."

循环调度

使用任务队列的优点之一是能够轻松并行化工作。如果我们正在积压工作,我们可以增加更多的工人,这样就可以轻松扩展。

首先,让我们尝试同时运行两个worker.php脚本。他们俩都将从队列中获取消息,但是究竟如何呢?让我们来看看。

您需要打开三个控制台。两个将运行worker.php 脚本。这些游戏机将成为我们的两个使用者-C1和C2。

# shell 1
php worker.php
# => [*] Waiting for messages. To exit press CTRL+C
# shell 2
php worker.php
# => [*] Waiting for messages. To exit press CTRL+C

在第三篇中,我们将发布新任务。启动使用者之后,您可以发布一些消息:

# shell 3
php new_task.php First message.
php new_task.php Second message..
php new_task.php Third message...
php new_task.php Fourth message....
php new_task.php Fifth message.....

让我们看看交付给我们工人的东西:

# shell 1
php worker.php
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'First message.'
# => [x] Received 'Third message...'
# => [x] Received 'Fifth message.....'
# shell 2
php worker.php
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'Second message..'
# => [x] Received 'Fourth message....'

默认情况下,RabbitMQ将每个消息依次发送给下一个使用者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式称为循环。与三个或更多的工人一起尝试。

消息确认

执行任务可能需要几秒钟。您可能想知道,如果其中一个使用者开始一项漫长的任务而仅部分完成而死掉,会发生什么情况。使用我们当前的代码,RabbitMQ一旦向消费者发送了一条消息,便立即将其标记为删除。在这种情况下,如果您杀死一个工人,我们将丢失正在处理的消息。我们还将丢失所有发送给该特定工作人员但尚未处理的消息。

但是我们不想丢失任何任务。如果一个工人死亡,我们希望将任务交付给另一个工人。

为了确保消息永不丢失,RabbitMQ支持 消息确认消费者发送回一个确认(告知),告知RabbitMQ特定的消息已被接收,处理,并且RabbitMQ可以自由删除它。

如果使用者死了(其通道已关闭,连接已关闭或TCP连接丢失)而没有发送确认,RabbitMQ将了解消息未完全处理,并将重新排队。如果同时有其他消费者在线,它将很快将其重新分发给另一个消费者。这样,您可以确保即使工人偶尔死亡也不会丢失任何消息。

没有任何消息超时;消费者死亡时,RabbitMQ将重新传递消息。即使处理一条消息花费非常非常长的时间也没关系。

消息确认默认为关闭。现在是时候通过将第四个参数basic_consume设置false来打开它们了 (true表示没有ack),并在完成任务后从工作人员发送适当的确认。

$callback = function ($msg) {
  echo ' [x] Received ', $msg->body, "\n";
  sleep(substr_count($msg->body, '.'));
  echo " [x] Done\n";
  $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
};

$channel->basic_consume('task_queue', '', false, false, false, false, $callback);

使用此代码,我们可以确保,即使您在处理消息时使用CTRL + C杀死工作人员,也不会丢失任何信息。工人死亡后不久,所有未确认的消息将重新发送。

确认必须在收到交货的同一通道上发送。尝试使用其他通道进行确认将导致通道级协议异常。请参阅有关确认文档指南 以了解更多信息。

被遗忘的确认

错过ack是一个普遍的错误这是一个简单的错误,但是后果很严重。当您的客户端退出时,消息将被重新发送(可能看起来像是随机重新发送),但是RabbitMQ将消耗越来越多的内存,因为它将无法释放任何未确认的消息。

为了调试这种错误,您可以使用rabbitmqctl 打印messages_unacknowledged字段:

sudo rabbitmqctl list_queues名称messages_ready messages_unacknowledged

在Windows上,删除sudo:

rabbitmqctl.bat list_queues名称messages_ready messages_unacknowledged

讯息持久性

我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是,如果RabbitMQ服务器停止,我们的任务仍然会丢失。

RabbitMQ退出或崩溃时,它将忘记队列和消息,除非您告知不要这样做。要确保消息不会丢失,需要做两件事:我们需要将队列和消息都标记为持久。

首先,我们需要确保RabbitMQ永远不会丢失我们的队列。为此,我们需要将其声明为持久的为此,我们将第三个参数作为true传递给queue_declare

$channel->queue_declare('hello', false, true, false, false);

尽管此命令本身是正确的,但在我们当前的设置中将无法使用。这是因为我们已经定义了一个名为hello的队列 ,该队列并不持久。RabbitMQ不允许您使用不同的参数重新定义现有队列,并且将向尝试执行此操作的任何程序返回错误。但是有一个快速的解决方法-让我们声明一个名称不同的队列,例如task_queue

$channel->queue_declare('task_queue', false, true, false, false);

设置为true的此标志需要同时应用于生产者代码和消费者代码。

在这一点上,我们确保即使RabbitMQ重新启动task_queue队列也不会丢失。现在,我们需要将消息标记为持久消息-通过设置delivery_mode = 2 message属性,AMQPMessage将其作为属性数组的一部分。

$msg = new AMQPMessage(
    $data,
    array('delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT)
);

关于消息持久性的说明

将消息标记为持久性并不能完全保证不会丢失消息。尽管它告诉RabbitMQ将消息保存到磁盘,但是RabbitMQ接受消息并且尚未保存消息时,还有很短的时间。而且,RabbitMQ不会对每条消息都执行fsync(2)-它可能只是保存到缓存中,而没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果您需要更强有力的保证,则可以使用 发布者确认

公平派遣

您可能已经注意到,调度仍然无法完全按照我们的要求进行。例如,在有两名工人的情况下,当所有奇怪的消息都很重,甚至消息很轻时,一位工人将一直忙碌而另一位工人将几乎不做任何工作。好吧,RabbitMQ对此一无所知,并且仍将平均分配消息。

发生这种情况是因为RabbitMQ在消息进入队列时才调度消息。它不会查看使用者的未确认消息数。它只是盲目地将每第n条消息发送给第n个使用者。

为了克服这一点,我们可以将basic_qos方法与 prefetch_count = 1设置一起使用。这告诉RabbitMQ一次不要给工人一个以上的消息。换句话说,在处理并确认上一条消息之前,不要将新消息发送给工作人员。而是将其分派给不忙的下一个工作程序。

$channel->basic_qos(null, 1, null);

关于队列大小的注意事项

如果所有工作人员都忙,您的队列就满了。您将需要留意这一点,也许会增加更多的工作人员,或者有其他一些策略。

放在一起

我们的new_task.php文件的最终代码

<?php

require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();

$channel->queue_declare('task_queue', false, true, false, false);

$data = implode(' ', array_slice($argv, 1));
if (empty($data)) {
    $data = "Hello World!";
}
$msg = new AMQPMessage(
    $data,
    array('delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT)
);

$channel->basic_publish($msg, '', 'task_queue');

echo ' [x] Sent ', $data, "\n";

$channel->close();
$connection->close();

(new_task.php源代码)

还有我们的worker.php

<?php

require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();

$channel->queue_declare('task_queue', false, true, false, false);

echo " [*] Waiting for messages. To exit press CTRL+C\n";

$callback = function ($msg) {
    echo ' [x] Received ', $msg->body, "\n";
    sleep(substr_count($msg->body, '.'));
    echo " [x] Done\n";
    $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
};

$channel->basic_qos(null, 1, null);
$channel->basic_consume('task_queue', '', false, false, false, false, $callback);

while ($channel->is_consuming()) {
    $channel->wait();
}

$channel->close();
$connection->close();

(worker.php源代码)

使用消息确认和预取,您可以设置工作队列。耐用性选项即使重新启动RabbitMQ也可以使任务继续存在。

猜你喜欢

转载自www.cnblogs.com/zx-admin/p/12014964.html