英文原文/Scheduling Jobs With Yii
有时在你的应用中会有长时间运行或大量计算的工作, 我们不应该让用户等待服务器去处理它们,而应该是让他们在后台处理. 你可能需要在很晚的时间执行一些操作, 如在一天的结尾发送用户报告. 解决以上场景的解决办法是创建一个队列, 并且在非 web 端处理这个队列.
Heroku 提供了一个叫做 "Heroku 调度器" 的附加组件,它可以每 10 分钟, 每小时, 或每天执行一次任务. 我们将使用这个调度器来访问我们的 Yii 控制台命令(Yii Console Command).
我将使用一个 MySQL 表作为一个队列,尽管有些人认为这是一个糟糕的设计模式. 我这么做的原因是我不会遇到文章中提到的问题. 你也可以使用任何你喜欢的 *MQ 附加组件.
队列表的迁移非常简单. 它里面包含执行的时间戳(在这个时间之后执行), 实际执行时间, 是否执行成功, 要执行的操作, 参数和可选的执行结果. 我们将讨论如何在控制台命令中使用这些字段.(你可以在 https://github.com/aarondfrancis/yii-CronCommand 找到这些源码)
$this->createTable('tbl_cron_jobs', array( 'id' => 'pk', 'execute_after' => 'timestamp', 'executed_at' => 'timestamp NULL', 'succeeded' => 'boolean', 'action' => 'string NOT NULL', 'parameters' => 'text', 'execution_result' => 'text' ));
CronJob 模型是标准的使用 Gii 生成的模型, 下面两个方法除外:
public function beforeValidate(){ if(gettype($this->parameters) !== "string"){ $this->parameters = serialize($this->parameters); } return parent::beforeValidate(); } public function afterFind(){ $this->parameters = unserialize($this->parameters); return parent::afterFind(); }
beforeValidate方法序列化任何参数,afterFind方法再反序列话它们. 例如,我们要在用户注册两小时后给他发送一封欢迎邮件, 我们应该创建一个新的 CronJob 并且传递一个包含用户 ID 的数组. 模型将会序列化这个数组并把它保存到数据库中, 当我们获取它的时候模型又会将它反序列化.
现在我们有了工作需要的模型,接下来看一下我们将要运行的CronCommand. (同样你可以可以在 https://github.com/aarondfrancis/yii-CronCommand 获得源码). 这里我只说一下关键部分.
这个例子中的脚本入口点是 Index 动作. 这也是我们在 Heroku 调度器中将要调用的, 我们将会让 index 动作来决定什么将被处理.(这只是处理方法中的一个. 我喜欢这么做的原因是我只需要在一个地方处理逻辑. 如果我添加了一个新需要处理的命令我需要做的只是在 CronCommand 中添加它并且在下一次 Heroku 调用我的 index 动作时它就会处理这个新的命令. 其他可选择的方法可能是为每一个新的动作添加action{New}方法,并添加一个新的计划任务到 Heroku 中来调用新的动作. 我比较喜欢一个入口: actionIndex.)
首先要做的是获取需要处理的任务列表. 我们选择现在时间大于 execute_after 时间(意思是应该执行) 并且没有执行的任务. 我们按 ID 升序排序, 这样先添加的任务可以先执行.(如果我们有一个正在执行队列我们可能不会看到最早的任务, 除非我们首先来选择他们)
$jobs = CronJob::model()->findAll('execute_after <:now AND executed_at IS NULL ORDER BY id ASC', array(':now'=>$now));
接下来我们遍历所有任务并处理它们. 但我们首先要确保有方法来处理它. 如果有的话, 调用这个方法.
if(method_exists($this, $job->action)){ $result = $this->{$job->action}($job->parameters);
如果你添加了一个 action 为 "testJob" 的 CronJob 模型, CronCommand 将会在处理这个任务时调用$this->testJob($job->parameters)方法. 如果结果是FALSE, 我们将跳过它, 等下次再处理. 否则将返回一个数组其中succeeded是boolean类型,execution_result你喜欢的任何结果.
现在我们已经设置了 Cron 命令, 你可以通过终端在protected文件夹下运行./yiic来测试. 你可以看到一个选项 "cron". 现在你可以试着运行./yiic cron. 如果你的表里存在任务你就可以看到它正在执行.
剩下的是我们需要写一个运行在 Heroku 的 bash 脚本. 它超简单的, 仅需两行, 如下:
export LD_LIBRARY_PATH=/app/php/ext bin/php www/protected/yiic.php cron
第一行设置了运行 PHP 命令行所需要的路径(感谢 诺伯特?克里 指出), 第二行调用了我们的 yiic.php 的 cron 命令. 把它们保存为 "heroku.sh" 或其它名称, 通过输入下面的命令把它添加到你的 Heroku 调度器中:
www/protected/heroku.sh
现在你的actionIndex将会每 10 分中运行一次, 抓取队列项(任务), 处理他们并且保存返回的结果.
如果你有任何问题请告诉我, 我会尽最大努力来帮助你.
如果你想阅读更多关于 Yii 计划任务的内容, Yii Rapid Application Development book (第 7 章) 中有深入的解释.