天天看點

利用redis和php-resque實作背景任務

在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數組中移除。