工作队列

第一篇教程中,我们已经写了一个从已知队列中发送和获取消息的程序。在这篇教程中,我们将创建一个工作队列(Work Queue),它会发送一些耗时的任务给多个工作者(Works )。

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

这个概念在网络应用中是非常有用的,它可以在短暂的HTTP请求中处理一些复杂的任务。

准备

之前的教程中,我们发送了一个包含“Hello World!”的字符串消息。现在,我们将发送一些字符串,把这些字符串当作复杂的任务。我们没有真是的例子,例如图片缩放、pdf文件转换。所以使用 sleep()函数来模拟这种情况。我们在字符串中加上点号(.)来表示任务的复杂程度,一个点(.)将会耗时1秒钟。比 如”Hello…”就会耗时3秒钟。

我们对之前教程的send.php做些简单的调整,以便可以发送随意的消息。这个程序会按照计划发送任务到我们的工作队列中。我们把它命名为new_task.php:

$message = empty($argv[1]) ? 'Hello World!' : ' '.$argv[1];
$exchange->publish($message, $routeKey);
var_dump("[x] Sent $message");

我们的旧脚本(receive.php)同样需要做一些改动:它需要为消息体中每一个点号(.)模拟1秒钟的操作。它会从队列中获取消息并执行,我们把它命名为worker.php:

function callback($envelope, $queue) {
$msg = $envelope->getBody();
var_dump(" [x] Received:" . $msg);
sleep(substr_count($msg,'.'));
$queue->ack($envelope->getDeliveryTag());
}

轮询分发

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

首先,我们先同时运行两个worker.php脚本,它们都会从队列中获取消息,到底是不是这样呢?我们看看。

你需要打开三个终端,两个用来运行worker.php脚本,这两个终端就是我们的两个消费者(consumers)—— C1 和 C2。

shell1

php worker.php
[*] Waiting for messages. To exit press CTRL+C

shell2

php worker.php
[*] Waiting for messages. To exit press CTRL+C

第三个终端,我们用来发布新任务。你可以发送一些消息给消费者(consumers):

shell3

php new_task.php message.....1

shell3

php new_task.php message.....2

shell3

php new_task.php message.....3

shell3

php new_task.php message.....4

看看到底发送了什么给我们的工作者(workers):

shell1

php worker.php
[*] Waiting for messages. To exit press CTRL+C
[x] Received 'message.....1'
[x] Received 'message.....3'

shell2

php worker.php
[*] Waiting for messages. To exit press CTRL+C
[x] Received 'message.....2'
[x] Received 'message.....4'

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

消息响应

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

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

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

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

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

消息是没有超时这个概念的;当工作者与它断开连的时候,RabbitMQ会重新发送消息。这样在处理一个耗时非常长的消息任务的时候就不会出问题了。 之前的例子中我们使用$queue->ack()。当工作者(worker)完成了任务,就发送一个响应。

function callback($envelope, $queue) {
$msg = $envelope->getBody();
var_dump(" [x] Received:" . $msg);
sleep(substr_count($msg,'.'));
$queue->ack($envelope->getDeliveryTag());
}
$queue->consume('callback');

运行上面的代码,我们发现即使使用CTRL+C杀掉了一个工作者(worker)进程,消息也不会丢失。当工作者(worker)挂掉这后,所有没有响应的消息都会重新发送。

忘了响应

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

为了排除这种错误,你可以使用rabbitmqctl命令,输出messages_unacknowledged字段:

``` $ sudo rabbitmqctl listqueues name messagesready messages_unacknowledged Listing queues ... hello 0 0 ...done.

```

消息持久化

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

首先,为了不让队列丢失,需要把它声明为持久化(durable):

$queue->setFlags(AMQP_DURABLE);

尽管这行代码本身是正确的,但是仍然不会正确运行。因为我们已经定义过一个叫hello的非持久化队列。RabbitMq不允许你使用不同的参数重新定义一个队列,它会返回一个错误。但我们现在使用一个快捷的解决方法——用不同的名字,例如task_queue。

$queue->setName('task_queue');
$queue->setFlags(AMQP_DURABLE);
$queue->declare();

这个$queue->declare();必须在生产者(producer)和消费者(consumer)对应的代码中修改。

这时候,我们就可以确保在RabbitMq重启之后queue_declare队列不会丢失。

注意:消息持久化

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

公平分发

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

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

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

$channel->qos(0,1);

关于队列大小

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

整合代码

new_task.php

<?php
$exchangeName = 'demo';
$queueName = 'task_queue';
$routeKey = 'task_queue';
$message = empty($argv[1]) ? 'Hello World!' : ' '.$argv[1]; $connection = new AMQPConnection(array('host' => '127.0.0.1', 'port' => '5672', 'vhost' => '/', 'login' => 'guest', 'password' => 'guest'));
$connection->connect() or die("Cannot connect to the broker!\n"); $channel = new AMQPChannel($connection);
$exchange = new AMQPExchange($channel);
$exchange->setName($exchangeName);
$queue = new AMQPQueue($channel);
$queue->setName($queueName);
$queue->setFlags(AMQP_DURABLE);
//$queue->declare();
$queue->declareQueue();
$exchange->publish($message, $routeKey);
var_dump("[x] Sent $message"); $connection->disconnect();

worker.php

<?php
$exchangeName = 'demo';
$queueName = 'task_queue';
$routeKey = 'task_queue'; $connection = new AMQPConnection(array('host' => '127.0.0.1', 'port' => '5672', 'vhost' => '/', 'login' => 'guest', 'password' => 'guest'));
$connection->connect() or die("Cannot connect to the broker!\n");
$channel = new AMQPChannel($connection);
$exchange = new AMQPExchange($channel);
$exchange->setName($exchangeName);
$exchange->setType(AMQP_EX_TYPE_DIRECT);
//$exchange->declare();
$exchange->declareExchange();
$queue = new AMQPQueue($channel);
$queue->setName($queueName);
$queue->setFlags(AMQP_DURABLE);
//$queue->declare();
$queue->declareQueue();
$queue->bind($exchangeName, $routeKey); var_dump('[*] Waiting for messages. To exit press CTRL+C');
while (TRUE) {
$queue->consume('callback');
$channel->qos(0,1);
}
$connection->disconnect(); function callback($envelope, $queue) {
$msg = $envelope->getBody();
var_dump(" [x] Received:" . $msg);
sleep(substr_count($msg,'.'));
$queue->ack($envelope->getDeliveryTag());
}

使用方法:需要打开三个终端,两个用来运行worker.php脚本【先启动】,一个用来运行new_task.php脚本【后启动】

运行效果:

使用消息响应和prefetch_count你就可以搭建起一个工作队列了。这些持久化的选项使得在RabbitMQ重启之后仍然能够恢复。

RabbitMQ 入门教程(PHP版) 第二部分:工作队列(Work queues)的更多相关文章

  1. RabbitMQ官方中文入门教程(PHP版) 第二部分:工作队列(Work queues)

    工作队列 在第一篇教程中,我们已经写了一个从已知队列中发送和获取消息的程序.在这篇教程中,我们将创建一个工作队列(Work Queue),它会发送一些耗时的任务给多个工作者(Works ). 工作队列 ...

  2. RabbitMQ 入门教程(PHP版) 第三部分:发布/订阅(Publish/Subscribe)

    发布/订阅 在上篇第二部分教程中,我们搭建了一个工作队列.每个任务之分发给一个工作者(worker).在本篇教程中,我们要做的之前完全不一样——分发一个消息给多个消费者(consumers).这种模式 ...

  3. RabbitMQ 入门教程(PHP版) 第一部分:Hello World

    abbitMQ是一个消息代理.它的核心原理非常简单:接收和发送消息.你可以把它想像成一个邮局:你把信件放入邮箱,邮递员就会把信件投递到你的收件人处.在这个比喻中,RabbitMQ是一个邮箱.邮局.邮递 ...

  4. RabbitMQ 入门教程(PHP版) 使用rabbitmq-delayed-message-exchange插件实现延迟功能

    延迟任务应用场景 场景一:物联网系统经常会遇到向终端下发命令,如果命令一段时间没有应答,就需要设置成超时. 场景二:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单. 场景三:过1分钟给新 ...

  5. RabbitMQ 入门教程(PHP版) 延迟队列,延迟任务

    延迟任务应用场景 场景一:物联网系统经常会遇到向终端下发命令,如果命令一段时间没有应答,就需要设置成超时. 场景二:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单. 场景三:过1分钟给新 ...

  6. RabbitMQ 入门教程(PHP版) 第四部分:路由(Routing)

    路由(Routing) 在前面的第三部分教程中,我们实现了一个简单的日志系统.可以把日志消息广播给多个接收者. 本篇教程中我们打算新增一个功能——使得它能够只订阅消息的一个字集.例如,我们只需要把严重 ...

  7. 转 RabbitMQ 入门教程(PHP版) 使用rabbitmq-delayed-message-exchange插件实现延迟功能

    延迟任务应用场景 场景一:物联网系统经常会遇到向终端下发命令,如果命令一段时间没有应答,就需要设置成超时. 场景二:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单. 场景三:过1分钟给新 ...

  8. RabbitMQ 入门教程(PHP版) 简单Demo

    RabbitMQ的关键字说明 (1)Broker:经纪人.提供一种传输服务,维护一条从生产者到消费者的传输线路,保证消息数据能按照指定的方式传输.粗略的可以将图中的RabbitMQ Server当作B ...

  9. RabbitMQ 入门教程(PHP版) 第六部分:远程调用(RPC)

    在云计算环境中,很多时候需要用它其他机器的计算资源,把一部分计算任务分配到其他节点来完成.RabbitMQ 如何使用 RPC 呢?下面将会通过其它节点完成斐波纳契示例. 流程图  当客户端启动时,它 ...

随机推荐

  1. BZOJ1396 识别子串 和 BZOJ2865 字符串识别

    字符串识别 2865: 字符串识别 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 839  Solved: 261[Submit][Status][D ...

  2. *.net框架 - IEnumerable类 & IQueryable类

    什么使用IQueryable<T> 查询返回类型为什么用IQueryable<T>,而不用 IEnumerable<T>类型? IQueryable接口实现IEnu ...

  3. Java【基础学习】 之调用构造方法顺序【坑】

    解释:这里的super()仅仅是用来占位的,实际上,必须是严格按照分层初始化的过程:1.先初始化父类X的成员变量,即初始化成员变量Y,打印出:Y2.初始化父类X的构造方法,打印出:X3.父类初始化完成 ...

  4. winform DateTimePicker 设置成秒

    C# Windows窗体应用中,用到时间选择控件DateTimePicker,发现不能选择时分秒,难道要自己写一个控件?! 答案是否定的,通过属性修改是可以选择时间的,DateTimePicker完全 ...

  5. 2019暑期金华集训 Day5 树上数据结构

    自闭集训 Day5 树上数据结构 前置知识 点分治 边分治 树链剖分 LCT Top Tree LCT时间复杂度 线段树每次查询是严格\(\log n\)的,然而splay维护连续段的时候,如果每次查 ...

  6. __enter__,__exit__区别

    __enter__():在使用with语句时调用,会话管理器在代码块开始前调用,返回值与as后的参数绑定 __exit__():会话管理器在代码块执行完成好后调用,在with语句完成时,对象销毁之前调 ...

  7. SpringBoot整合ElasticSearch:基于SpringDataElasticSearch

    0.注意事项 SpringDataElasticSearch可能和远程的ElasticSearch版本不匹配,会宝座 版本适配说明:https://github.com/spring-projects ...

  8. 分享7个shell脚本实例--shell脚本练习必备

    概述 看多shell脚本实例自然就会有shell脚本的编写思路了,所以我一般比较推荐看脚本实例来练习shell脚本.下面分享几个shell脚本实例. 1.监测Nginx访问日志502情况,并做相应动作 ...

  9. 利用python的matplotlib处理计算数据

    #!/usr/bin/python # -*- coding: UTF-8 -*- import numpy as np import matplotlib.pyplot as plt import ...

  10. [转载] 浏览器Browser对同域名下的请求并发数量

    原文链接:https://blog.csdn.net/a562550212/article/details/79552713 另附原文作者贴的一个知乎地址,几个答主讲的非常好  https://www ...