php队列 php-resque+redis使用详解

最近这几天在公司SDK接口项目使用到在插入数据库之前对其他平台的数据进行数据上报。
上报需要访问别的服务,比较耗时,会影响到正常的数据插入。因此用到了队列。

收获:这次自己独立完成队列让我更明白compser的自动加载原理,以及PHP进程的相关内容、
队列说白了就是Job、Queue、Worker;其中Job负责处理对应事件的逻辑,Queue用于接收队列消息,Worker常驻内存,循环POP队列中的服务。
就一个while(true)循环,然后其中实现fork子进程来执行任务的过程

PHP队列有多种实现方式
1. PHP-redis 自己做消息队列
2. PHP自带扩展 gearman,和OMQ消息系统(这两个没研究过,但是看文档里面有,有时间研究研究)
3. MemcacheQ
4. RabbitMQ
5. PHP-resque

之前公司用过RabbitMQ,自己掌握的不怎么好。这次选型的时候,首相考虑到用redis和php来实现队列、
最后看php-reques 用的人蛮多,然后就选了这个。

注意:

  1. php-resque 需要 PCNTL 函数的支持。需要加入PHP的PCNTL的扩展。
  2. 需要有redis,php-resque不支持有密码验证的redis,需要自己来实现下功能
  3. PHP-resque 没有使用命名空间,引用的时候用的是顶级命名空间。

安装:

composer.json写入:
    "require": {
        "chrisboulton/php-resque": "1.2.x"
    },

执行

composer update

Tip:可以看到这里autoload 使用的psr-0 的协议。然后指向了lib这个文件夹,在写woker的时候有个地方一直不明白。待会贴出代码。
这里写图片描述
-

使用

tip:可以参照 chrisboulton/php-resque/demo 下面的文件
* 如何使用
* https://packagist.org/packages/chrisboulton/php-resque
* https://icewing.cc/post/background-jobs-and-phpresque-5.html
* http://avnpc.com/pages/run-background-task-by-php-resque#toc8

 *
 * 首先启动守护队列:
 * QUEUE=default VVERBOSE=1 php demo/resque.php
 * 前面的QUEUE部分是设置环境变量,我们指定当前的Worker只负责处理default队列。也可以使用QUEUE=* php demo/resque.php 来处理所有队列
 * QUEUE: 需要执行的队列的名字
 * INTERVAL:在队列中循环的间隔时间,即完成一个任务后的等待时间,默认是5秒
 * APP_INCLUDE:需要自动载入 PHP 文件路径,Worker 需要知道你的 Job 的位置并载入 Job
 * COUNT:需要创建的 Worker 的数量。所有的 Worker 都具有相同的属性。默认是创建1个Worker
 * REDIS_BACKEND:Redis 服务器的地址,使用 hostname:port 的格式,如 127.0.0.1:6379,或 localhost:6379。默认是 localhost:6379
 * REDIS_BACKEND_DB:使用的 Redis 数据库的名称,默认是 0
 * VERBOSE:啰嗦模式,设置 1 为启用,会输出基本的调试信息
 * VVERBOSE:设置“1”启用更啰嗦模式,会输出详细的调试信息
 * PREFIX:前缀。在 Redis 数据库中为队列的 KEY 添加前缀,以方便多个 Worker 运行在同一个Redis 数据库中方便区分。默认为空
 * PIDFILE:手动指定 PID 文件的位置,适用于单 Worker 运行方式
 * 以上参数中只有QUEUE是必须的。如果让 Worker 监视执行多个队列,可以用逗号隔开多个队列的名称,如:queue1,queue2,queue3,队列执行是有顺序的,如上 queue2 和 queue3 总是会在 queue1 后面被执行。
 * 也可以设置QUEUE为*让 Worker 以字母顺序执行所有的队列。
 *
 * 结束进程
 * kill -QUIT YOUR-WORKER-PID
 * QUIT - 等待子进程结束后再结束
 * TERM / INT - 立即结束子进程并退出
 * USR1 - 立即结束子进程,但不退出
 * USR2 - 暂停Worker,不会再执行新任务
 * CONT - 继续运行Worker
 *
 * 记录下 Worker 的输出
 \\192.168.2.142\share\XXX\vendor\chrisboulton\php-resque\demo\resque.php
 * nohup QUEUE=notification VVERBOSE=1  INTERVAL=10 COUNT=5 APP_INCLUDE=/usb/html/localapi/sdkapi/bin/loader.php REDIS_BACKENT=192.168.2.142:6379 php /usb/html/localapi/sdkapi/bin/daemo_queue.php   >> /var/log/phpresque/infofile.log 2>&1 &
 * nohup 表示后台运行守护进程
 *
 * Resque_Job_Status::STATUS_WAITING = 1; (等待)
 * Resque_Job_Status::STATUS_RUNNING = 2; (正在执行)
 * Resque_Job_Status::STATUS_FAILED = 3; (失败)
 * Resque_Job_Status::STATUS_COMPLETE = 4; (结束)
 * 移除Jobs
 * Removes multiple jobs
 * Resque::dequeue('default');
 * Resque::dequeue('default', ['My_Job']);
 * Resque::dequeue('default', ['My_Job' => '087df5819a790ac666c9608e2234b21e']);
 * Resque::dequeue('default', ['My_Job' => array('foo' => 1, 'bar' => 2)]);
 * Resque::dequeue('default', ['My_Job', 'My_Job2']); 移除多个jobs
 *
 *
 *
 * 消费者可以有三个方法 worker
 * public function setUp() {}       // .. Set up environment for this job
 * public function perform() {}     // .. Run job
 * public function tearDown() {}    // ... Remove environment for this job
 *
 * 生产者
 * Resque::setBackend('127.0.0.1:6379');
 * $args = array(
 *  'time' => time(),
 *  'array' => array(
 *      'test' => 'test',
 *  ),
 * );
 * $jobId = Resque::enqueue('default', $argv[1], $args, true);
 * $argv[1]为调用的类, $args 为参数
 * 最后一行的第一个参数表示 消息队列的名称(可随意标记,比如 email,log等),第二个参数表示取出任务后,由My_Job这个类来处理此条任务
 *
 *
 * http://www.jianshu.com/p/395652dc66f1
 * 监控resque-web
 * gem install resque-web -v 0.0.8
 * 运行resque-web -p 40000
 * /usr/bin/python /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
 *
 *
 * 总结:
 * 开启守护队列 QUEUE=* php resque.php >> /var/log/phpresque/logfile.log  &
 * 调用job  php queue.php PHP_Job
 * 执行worker PHP_Job->perform()   在 tail -f /var/log/phpresque/logfile.log 可以看到执行的结果
 */

代码:
worker常驻进程:
这里写图片描述

JOBS: 这里用这个命名空间需要在composer中加入 JOBS 这个目录
这里写图片描述

Queue:
这里写图片描述

插入队列
这里写图片描述

使用上面的代码:启动脚本,第二个参数使用命名空间才能导入自动加载。很多案例里面没有使用到命名空间,这里可能会有问题。
php /usb/html/XXX/queue_resident.php >> /var/log/phpresque/infofile.log 2>&1

关于composer自动加载的,可以参照别人的文章,好好阅读下,我这边感觉也没有特别的要总结的~~就不写啦

ThinkPHP3.2 集成 php-resque: PHP Resque Worker =========================================== php-resquephp环境中一个轻量级的队列服务。具体队列服务是做什么用的,请自行百度! ## 运行环境 ## * PHP 5.2+ * Redis 2.2+ ## 集成方法 ## ### 将源码放到ThinkPHP的Vendor目录中 ### 将源码更新到 ThinkPHP/Library/Vendor/php-resque/ 目录中 <font color=red>注意要定义应用目录,之前发的内容没定义应用目录导致部分小伙伴引发了找不到Queue类的异常</font> ### 在项目根目录中创建resque入口脚本 ### #!/usr/bin/env php <?php ini_set('display_errors', true); error_reporting(E_ERROR); set_time_limit(0); // 定义应用目录 define('APP_PATH','./Application/'); define('MODE_NAME', 'cli'); // 自定义cli模式 define('BIND_MODULE', 'Home'); // 绑定到Home模块 define('BIND_CONTROLLER', 'Queue'); // 绑定到Queue控制器 define('BIND_ACTION', 'index'); // 绑定到index方法 // 处理自定义参数 $act = isset($argv[1]) ? $argv[1] : 'start'; putenv("Q_ACTION={$act}"); putenv("Q_ARGV=" . json_encode($argv)); require './ThinkPHP/ThinkPHP.php'; ### 创建Queue控制器 ### 在`Home`模块的`Controller`中创建`Queue`控制器 <?php namespace Home\\\\\\\\Controller; if (!IS_CLI) die('The file can only be run in cli mode!'); use Exception; use Resque; /*** * queue入口 * Class Worker * @package Common\\\\\\\\Controller */ class QueueController { protected $vendor; protected $args = []; protected $keys = []; protected $queues = '*'; public function __construct() { vendor('php-resque.autoload'); $argv = json_decode(getenv('Q_ARGV')); foreach ($argv as $item) { if (strpos($item, '=')) { list($key, $val) = explode('=', $item); } else { $key = $val = $item; } $this->keys[] = $key; $this->args[$key] = $val; } $this->init(); } /** * 执行队列 * 环境变量参数值: * --queue|QUEUE: 需要执行的队列的名字 * --interval|INTERVAL:在队列中循环的间隔时间,即完成一个任务后的等待时间,默认是5秒 * --app|APP_INCLUDE:需要自动载入PHP文件路径,Worker需要知道你的Job的位置并载入Job * --count|COUNT:需要创建的Worker的数量。所有的Worker都具有相同的属性。默认是创建1个Worker * --debug|VVERBOSE:设置“1”启用更啰嗦模式,会输出详细的调试信息 * --pid|PIDFILE:手动指定PID文件的位置,适用于单Worker运行方式 */ private function init() { $is_sington = false; //是否单例运行,单例运行会在tmp目录下建立一个唯一的PID // 根据参数设置QUEUE环境变量 $QUEUE = in_array('--queue', $this->keys) ? $this->args['--queue'] : '*'; if (empty($QUEUE)) { die("Set QUEUE env var containing the list of queues to work.\n"); } $this->queues = explode(',', $QUEUE); // 根据参数设置INTERVAL环境变量 $interval = in_array('--interval', $this->keys) ? $this->args['--interval'] : 5; putenv("INTERVAL={$interval}"); // 根据参数设置COUNT环境变量 $count = in_array('--count', $this->keys) ? $this->args['--count'] : 1; putenv("COUNT={$count}"); // 根据参数设置APP_INCLUDE环境变量 $app = in_array('--app', $this->keys) ? $this->args['--app'] : ''; putenv("APP_INCLUDE={$app}"); // 根据参数设置PIDFILE环境变量 $pid = in_array('--pid', $this->keys) ? $this->args['--pid'] : ''; putenv("PIDFILE={$pid}"); // 根据参数设置VVERBOSE环境变量 $debug = in_array('--debug', $this->keys) ? $this->args['--debug'] : ''; putenv("VVERBOSE={$debug}"); } public function index() { $act = getenv('Q_ACTION'); switch ($act) { case 'stop': $this->stop(); break; case 'status': $this->status(); break; default: $this->start(); } } /** * 开始队列 */ public function start() { // 载入任务类 $path = COMMON_PATH . "Job"; $flag = \FilesystemIterator::KEY_AS_FILENAME; $glob = new \FilesystemIterator($path, $flag); foreach ($glob as $file) { if('php' === pathinfo($file, PATHINFO_EXTENSION)) require realpath($file); } $logLevel = 0; $LOGGING = getenv('LOGGING'); $VERBOSE = getenv('VERBOSE'); $VVERBOSE = getenv('VVERBOSE'); if (!empty($LOGGING) || !empty($VERBOSE)) { $logLevel = Resque\Worker::LOG_NORMAL; } else { if (!empty($VVERBOSE)) { $logLevel = Resque\Worker::LOG_VERBOSE; } } $APP_INCLUDE = getenv('APP_INCLUDE'); if ($APP_INCLUDE) { if (!file_exists($APP_INCLUDE)) { die('APP_INCLUDE (' . $APP_INCLUDE . ") does not exist.\n"); } require_once $APP_INCLUDE; } $interval = 5; $INTERVAL = getenv('INTERVAL'); if (!empty($INTERVAL)) { $interval = $INTERVAL; } $count = 1; $COUNT = getenv('COUNT'); if (!empty($COUNT) && $COUNT > 1) { $count = $COUNT; } if ($count > 1) { for ($i = 0; $i < $count; ++$i) { $pid = pcntl_fork(); if ($pid == -1) { die("Could not fork worker " . $i . "\n"); } // Child, start the worker else { if (!$pid) { $worker = new Resque\Worker($this->queues); $worker->logLevel = $logLevel; fwrite(STDOUT, '*** Starting worker ' . $worker . "\n"); $worker->work($interval); break; } } } } // Start a single worker else { $worker = new Resque\Worker($this->queues); $worker->logLevel = $logLevel; $PIDFILE = getenv('PIDFILE'); if ($PIDFILE) { file_put_contents($PIDFILE, getmypid()) or die('Could not write PID information to ' . $PIDFILE); } fwrite(STDOUT, '*** Starting worker ' . $worker . "\n"); $worker->work($interval); } } /** * 停止队列 */ public function stop() { $worker = new Resque\Worker($this->queues); $worker->shutdown(); } /** * 查看某个任务状态 */ public function status() { $id = in_array('--id', $this->keys) ? $this->args['--id'] : ''; $status = new \Resque\Job\Status($id); if (!$status->isTracking()) { die("Resque is not tracking the status of this job.\n"); } echo "Tracking status of " . $id . ". Press [break] to stop.\n\n"; while (true) { fwrite(STDOUT, "Status of " . $id . " is: " . $status->get() . "\n"); sleep(1); } } } ### 新增队列配置 ### 在公共`config.php`中新增队列配置,如下 /* 消息队列配置 */ 'QUEUE' => array( 'type' => 'redis', 'host' => '127.0.0.1', 'port' => '6379', 'persistent' => false, //是否启用 'prefix' => 'queue', 'password' => '', // 密码 ), ### 新增队列初始化行为 ### 在`app_init`行为中新增队列初始化的行为,`run`内容为 public function run() { // 处理队列配置 $config = C('QUEUE'); if ($config) { vendor('php-resque.autoload'); // 初始化队列服务 $select = isset($config['select']) ? $config['select'] : 0; $password = isset($config['password']) ? $config['password'] : null; $persistent = isset($config['persistent']) ? $config['persistent'] : false; $timeout = isset($config['timeout']) ? $config['timeout'] : 30; $server = $config['host'] . ":" . $config['port']; \Resque::setBackend($server, $select, $password, $persistent, $timeout); // 初始化缓存前缀 if(isset($config['prefix']) && !empty($config['prefix'])){ \Resque\Redis::prefix($config['prefix']); } } } 到此,整个队列服务基本已配置完成。 接下来就要创建队列执行的任务了 ## Jobs ## ### 创建 Jobs ### 目前任务类固定在`Common`模块的`Job`中,命名格式为`XxxxJob.class.php` <?php namespace Common\Job; class XxxxJob { public function perform() { $args = $this->args; fwrite(STDOUT, json_encode($args) . PHP_EOL); } } 要获取队列中传入的参数值请使用`$this->args` 任务perform方法中抛出的任何异常都会导致任务失败,所以在写任务业务时要小心,并且处理异常情况。 任务也有`setUp`和`tearDown`方法,如果定义了一个`setUp`方法,那么它将在`perform`方法之前调用,如果定义了一个`tearDown`方法,那么它将会在`perform`方法之后调用。 <?php namespace Common\Job; class XxxxJob { public function setUp() { // ... Set up environment for this job } public function perform() { // .. Run job } public function tearDown() { // ... Remove environment for this job } } ### 添加任务到队列中 ### 在程序控制器的任意方法中引入队列类库时,使用`Resque::enqueue`方法执行入栈,`Resque::enqueue`方法有四个参数,第一个是当前的队列名称,第二个参数为任务类,第三个是传入的参数,第四个表示是否返回工作状态的令牌 vendor('php-resque.autoload'); // 引入队列类库 $job = '\\Common\\Job\\XxxxJob'; // 定义任务类 // 定义参数 $args = array( 'time' => time(), 'array' => array( 'test' => 'test', ), ); // 入栈 $jobId = \Resque::enqueue('default', $job, $args, true); echo "Queued job ".$jobId."\n\n"; 如果要查看当前任务的工作状态可以使用如下方法: $status = new \Resque\Job\Status($jobId); echo $status->get(); // Outputs the status 任务的工作状态值有专门的常量``\Resque\Job\Status``对应类。 具体的对应关系如下: * `Resque\Job\Status::STATUS_WAITING` - 任务在队列中 * `Resque\Job\Status::STATUS_RUNNING` - 任务正在运行 * `Resque\Job\Status::STATUS_FAILED` - 任务执行失败 * `Resque\Job\Status::STATUS_COMPLETE` - 任务执行完成 * `false` - 无法获取状态 - 检查令牌是否有效? 任务的过期时间为任务完成后的24小时后,也可以定义过期类的`stop()`方法 ## 队列任务启动 ## 在命令行中转到项目根目录,执行 $ php resque start 即可启动服务 启动时也可以加入部分参数: * `--queue` - 需要执行的队列的名字,可以为空,也可以多个以`,`分割 * `--interval` -在队列中循环的间隔时间,即完成一个任务后的等待时间,默认是5秒 * `--count` - 需要创建的Worker的数量。所有的Worker都具有相同的属性。默认是创建1个Worker * `--debug` - 设置“1”启用更啰嗦模式,会输出详细的调试信息 * `--pid` - 手动指定PID文件的位置,适用于单Worker运行方式 如: $ php resque start --queue=default --pid=/tmp/resque.pid --debug=1 如果要使用守护进程方式启动则需要在最后加入`&`即可 如: $ php resque start --queue=default --pid=/tmp/resque.pid --debug=1 & 也可以配合supervisord实现进程长驻 更多的操作请参考php-resque官方文档。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值