在PHP的頁面程式設計過程中,我們總遇到這樣一個問題,即是PHP是一個順序運作的過程,僅僅能在一個任務完畢後接着去實作下一個任務,而這當中存在一個問題,就是假如當中一個任務耗費大量時間的時候,我們可能就必須要等待。借助redis能夠将耗時任務放到背景去運作,進而降低等待時間。
Redis 是一個高性能的key-value資料庫。能夠幫助我們有效的實作背景任務,将耗費大量時間的任務遷移到背景去運作,能夠節約非常多的時間。
php-resque是來自Ruby的項目Resque的一個PHP擴充,正是由于Resque清晰簡單的攻克了背景任務帶來的一系列問題。
在Resque中背景任務的角色劃分:
在Resque中,一個背景任務被抽象為由三種角色共同完畢:
Job | 任務 : 一個Job就是一個須要在背景完畢的任務,比方發送郵件。就能夠抽象為一個Job。在Resque中一個Job就是一個Class。
Queue | 隊列 : 也就是上文的消息隊列,在Resque中,隊列則是由Redis實作的。Resque還提供了一個簡單的隊列管理器,能夠實作将Job插入/取出隊列等功能。
Worker | 運作者 : 負責從隊列中取出Job并運作,能夠以守護程序的方式運作在背景。
那麼基于這個劃分。一個背景任務在Resque下的基本流程是這種:
1、将一個背景任務編寫為一個獨立的Class,這個Class就是一個Job。
2、在須要使用背景程式的地方,系統将Job Class的名稱以及所需參數放入隊列。
3、以指令行方式開啟一個Worker,并通過參數指定Worker所須要處理的隊列。
4、Worker作為守護程序運作,而且定時檢查隊列。
5、當隊列中有Job時。Worker取出Job并運作,即執行個體化Job Class并運作Class中的方法。
至此就能夠完整的運作完一個背景任務。
在Resque中,另一個非常重要的設計:一個Worker。能夠處理一個隊列,也能夠處理非常多個隊列,而且能夠通過添加Worker的程序/線程數來加快隊列的運作速度。
注:本文中的安裝等操作。均在Linux下完畢。
步驟一、php-resque的安裝
此處可參閱:PHP的輕量消息隊列php-resque使用說明
須要提前說明的是,由于涉及到程序的開辟與管理,php-resque使用了php的PCNTL函數,是以僅僅能在Linux下運作,而且須要php編譯PCNTL函數。假設希望用Windows做相同的工作,那麼能夠去找找Resque的其他語言版本号。php在Windows下非常不适合做背景任務。
安裝Redis
apt-get install redis-server
安裝Composer
apt-get install curl
cd /usr/local/bin
curl -s http://getcomposer.org/installer | php
chmod a+x composer.phar
alias composer='/usr/local/bin/composer.phar'
使用Composer安裝php-resque
假設web檔案夾在/opt/htdocs
apt-get install git git-core
cd /opt/htdocs
git clone git://github.com/chrisboulton/php-resque.git
cd php-resque
composer install
至此php-resque就可以完畢,能夠進行其使用。
步驟二:php-resque的使用
首先須要運作Worker。
此處可參閱:背景任務和PHP-Resque的使用介紹
1、了解Worker的本質
技術上講一個Worker就是一個不斷運作的PHP程序,而且不斷監視新的任務并運作。
一個簡單的Worker的代碼例如以下:
while (true) {
$jobs = pullData(); // 從隊列中拉取任務
foreach ($jobs as $class => $args) { // 循環每一個找到的任務
$job = new $class();
$job->perform($args); // 運作任務
}
sleep(300); // 等待5分鐘後再次嘗試拉取任務
}
以上這些代碼的具體實作都能夠交給php-resque。建立一個Worker,php-resque須要下面參數:
QUEUE: 須要運作的隊列的名字
INTERVAL:在隊列中循環的間隔時間,即完畢一個任務後的等待時間,預設是5秒
APP_INCLUDE:須要自己主動加載PHP檔案路徑,Worker須要知道你的Job的位置并加載Job
COUNT:須要建立的Worker的數量。全部的Worker都具有相同的屬性。
預設是建立1個Worker
REDIS_BACKEND:Redisserver的位址。使用 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以字母順序運作全部的隊列。
Worker 必須以CLI方式啟動。你不能夠從浏覽器啟動Worker,由于:
你無法從浏覽器運作背景任務
PCNTL擴充僅僅能運作在CLI模式
2、啟動Worker
能夠從resque.php啟動Worker。這個位置位于php-resque/bin檔案夾下(也可能不帶.php字尾)。
在終端中運作:
cd /path/to/php-resque/bin/
php resque.php
非常顯然Worker不會被啟動,由于缺少必須的參數QUEUE,程式将會傳回例如以下錯誤:
Set QUEUE env var containing the list of queues to work.
php-resque通過getenv擷取參數。是以在啟動Worker的時候應該傳遞環境變量過去。是以應該下面面的方式啟動Worker:
QUEUE=notification php resque.php
假設啟用VVERBOSE模式:
QUEUE=notification VVERBOSE=1 php resque.php
終端将會輸出:
*** Starting worker KAMISAMA-MAC.local:84499:notification
** [23:48:18 2012-10-11] Registered signals
** [23:48:18 2012-10-11] Checking achievement
** [23:48:18 2012-10-11] Checking notification
** [23:48:18 2012-10-11] Sleeping for 5
** [23:48:23 2012-10-11] Checking achievement
** [23:48:23 2012-10-11] Checking notification
** [23:48:23 2012-10-11] Sleeping for 5
... etc ...
Worker會自己主動被命名為KAMISAMA-MAC.local:84499:notification,命名的規則是hostname:process-id:queue-names。
假設覺得這種啟動方式太麻煩且難記,能夠自己手動寫一個bash腳本來幫助你啟動Resque,如:
EXPORT QUEUE=notifacation
EXPORT VERBOSE=1
php resque.php
3、背景運作Worker
通過上面的方法成功啟動了Worker,但僅僅有在終端開啟的狀态下,關閉終端或按下Ctrl+C時Worker就會停止運作。我們能夠在指令後面加入一個&來使其背景運作。
QUEUE=notification php resque.php &
這樣就能夠讓resque在背景運作。但假設你開啟了VERBOSE模式。全部的輸出資訊将會丢失。是以我們須要在resque背景運作時把輸出的資訊儲存起來。
我們能夠使用nohup來保持resque背景運作,即使是在使用者登出後。
nohup QUEUE=notification php resque.php &
4、确認你的Worker成功運作了
通過管道操作無法知道Worker是否成功啟動。目前通過檢視log檔案裡有沒有輸出* Starting worker …..的内容也能夠知道是否啟動。
也能夠通過檢視系統程序的方法确認Worker是否正在運作。
ps -ef|grep resque.php
将會輸出名稱中包括resque.php的程序。當中第二列是程序的PID。
使用這種方法能夠非常好的知道Worker是否正在運作,以及有沒有意外終止。
5、暫停和停止Worker
要停止一個Worker,直接kill掉它的程序就可以了。能夠通過ps -ef|grep resque.php檢視Worker程序的PID。當然通過這個指令你無法知道哪個PID代碼的哪個Worker。
假設要結束一個PID是86681的程序:
kill 86681
這個指令将會馬上結束掉PID為86681的程序及子程序。假設Worker正在運作一個任務也不會等待任務運作完畢(未完畢的部分将會丢失)。
有一個能夠平滑的停止Worker的方法,能夠通過給kill指令發送一個SIGSPEC信号來告訴kill應該怎麼做,這須要PCNTL擴充的支援。
當然下面所講述的全部指令都須要PCNTL擴充支援。
通過PCNTL擴充,Worker能夠支援下面信号:
QUIT - 等待子程序結束後再結束
TERM / INT - 馬上結束子程序并退出
USR1 - 馬上結束子程序,但不退出
USR2 - 暫停Worker,不會再運作新任務
CONT - 繼續運作Worker
當沒有信号發出時預設是TERM / INT信号。
假設想在全部目前正在運作的任務都完畢後再停止,使用QUIT信号:
kill -QUIT YOUR-WORKER-PID
結束全部子程序,但保留Worker:
kill -USR1 YOUR-WORKER-PID
暫停和繼續運作Worker:
kill -USR2 YOUR-WORKER-PID
kill -CONT YOUR-WORKER-PID
簡單的說,任務就是傳遞給Worker要運作的内容。我們須要把Job依次加入到Queue來運作。
要把任務加入到隊列,程式必須要包括php-resque庫以及Redis。
使用
require_once '/path/to/php-resque/lib/Resque.php';
包括php-resque的庫檔案,它會自己主動連接配接到Redisserver,假設你的Redisserver不是預設的localhost:6379,你須要使用
Resque::setBackent('192.168.1.56:3680');
這種格式來設定你的Redisserver的位址。相同setBackent支援可選的第二個參數為使用的Redis資料庫名,默覺得0。
如今php-resque已經準備好了,使用下面代碼加入一個任務到隊列:
Resque::enqueue('default', 'Mail', array('[email protected]', 'hi!', 'this is a test content'));
第一個參數。’default’是指隊列的名字(即上文中QUEUE後的參數)。示範樣例中将會把任務推送到名為default的隊列中
第二個參數是Job的類名,表示要運作哪個Job
第三個參數是要發送給Job的參數也能夠使用關聯數組的形式
傳遞給Job的參數(上面第三個參數)能夠是普通數組、關聯數組的形式。也能夠是一個字元串,但使用數組能夠非常友善的傳遞很多其他的資訊給Job。
全部的參數在推送到隊列前都會經過json_encode處理。
步驟三、Job類建立和使用:
1、編寫一個Worker,建立job類。
如上面的樣例中。第一個參數是隊列的名字(還記得上一節裡面啟動php resque.php時傳遞的QUEUE環境變量嗎?)。第二個參數是Job的類名,即要運作的Job。Mail類就是一個Job類。
全部的Job類都應該包括一個perform()方法,使用Resque::enqueue()傳遞的第三個參數能夠在perform()方法中使用$this->args來得到。
class PHP_Job
{
public function perform()
{
sleep(120);
fwrite(STDOUT, 'Hello!');
}
}
在Resque的設計中,一個Job必須存在一個perform方法,Worker則會自己主動運作這種方法。
Job類也能夠包括setUp()和tearDown()方法,可選的這兩個方法分别會在perform()方法之前和之後運作。
class Mail{
public function setUp(){
# 這種方法會在perform()之前運作,能夠用來做一些初始化工作
# 如連接配接資料庫、處理參數等
}
public function perform(){
# 運作Job
}
public function tearDown(){
# 會在perform()之後運作,能夠用來做一些清理工作
}
}
2、包括Job類,将job插入隊列。
在執行個體化Job類之前,必須讓Worker找到并包括這個類。有非常多種方法能夠做到。
(1)、使用include_path
當PHP運作于Apache model方式的時候能夠使用.htaccess設定包括:
php_value include_path ".:/already/existing/path:/path/to/job-classes"
(2)、通過php.ini
include_path = ".:/php/includes:/path/to/job-classes"
(3)、使用APP_INCLUDE包括
上一節說了使用APP_INCLUDE指定Worker運作時要包括的PHP檔案的路徑。如:
QUEUE=default APP_INCLUDE=/path/to/loader.php php resque.php
loader.php的内容能夠是下面的那樣:(當中包括了全部的job類)
include '/path/to/Mail.php';
include '/path/to/AnotherJobClass.php';
include '/path/to/somewhere/AnotherJobClass.php';
include '/JobClass.php';
當然也能夠使用PHP的autoloader方法——sql_autoloader。
2、在你的項目中使用背景任務
下面面的代碼為例。把耗時較多的工作交給背景任務來做。
class User{
# functions(){} // 其他函數
public function updateLocation($location) {
$db->updateUserTable($this->userId, 'location', $location);
$this->recomputeNewFriends(); # 此操作耗時較長
}
public function recomputeNewFriends() {
# 查找新的朋友
}
}
把以上代碼改成:
class User {
# functions(){} // 其他函數
public function updateLocation($location) {
$db->updateUserTable($this->userId, 'location', $location);
# 把任務加入到隊列
# 這裡的隊列名為 'queueName'
# 任務名為 'FriendRecommendator'
Resque::enqueue('queueName', 'FriendRecommendator', array('id' => $this->userId));
}
}
下面是任務FriendRecommendator類的實作代碼:
class FriendRecommendator {
function perform() {
# 這裡沒有User類,須要建立一個User類對象
$user = new User($this->args['id']);
# 查找新朋友的操作
}
}
簡單的說,你僅僅須要把你的運作任務的代碼放到Job類中并改名為perform()就可以,僅僅要你願意甚至能夠将普通類改成Job類,但并不推薦這樣做。
perform()方法有個缺點,即一個Job類僅僅能包括一個perform()方法,也就是說一個Job類僅僅能運作一種背景任務。
3、程式
當中須要注意的幾點:
Hack的方法Resque::enqueue()的第三個參數必須是一個數組。而且它的第一個元素是要運作的任務的方法名,而且這個元素會在運作時從$args數組中移除。