天天看點

PHP實作執行定時任務的幾種思路詳解

PHP實作執行定時任務的幾種思路詳解

HP本身是沒有定時功能的,php也不能多線程。

PHP的定時任務功能必須通過和其他工具結合才能實作,例如WordPress内置了wp-cron的功能,很厲害。

本文,我們就來深入的解析幾種常見的php定時任務的思路。

Linux伺服器上使用CronTab定時執行php

我們先從相對比較複雜的伺服器執行php談起。伺服器上安裝了php,就可以執行php檔案,無論是否安裝了nginx或Apache這樣的伺服器環境軟體。

而Linux中,使用指令行,用CronTab來定時任務,又是絕佳的選擇,而且也是效率最高的選擇。

首先,進入指令行模式。

作為伺服器的linux一般都預設進入指令行模式的,當然,我們管理伺服器也一般通過putty等工具遠端連接配接到伺服器,為了友善,我們用root使用者登入。在指令行中鍵入:

crontab -e      

之後就會打開一個檔案,并且是非編輯狀态,則是vi的編輯界面,通過敲鍵盤上的i,進入編輯模式,就可以編輯内容。

這個檔案中的每一行就是一個定時任務,我們建立一行,就是建立一條定時任務(當然是指這一行内按照一定的格式進行書寫)。我們現在來舉個例子,增加一行,内容如下:​

00 * * * * lynx -dump https://www.yourdomain.com/script.php      

這是什麼意思呢?實際上上面這一行由兩部分組成,前面一部分是時間,後面一部分是操作内容。例如上面這個,

00 * * * *      

就是指當目前時間的分鐘數為00時,執行該定時任務。時間部分由5個時間參數組成,分别是:

分 時 日 月 周      

第1清單示分鐘1~59 每分鐘用或者 */1表示,/n表示每n分鐘,例如*/8就是每8分鐘的意思,下面也是類推

第2清單示小時1~23(0表示0點)

第3清單示日期1~31

第4清單示月份1~12

第5列辨別号星期0~6(0表示星期天)

整個句子的後面部分就是操作的具體内容。

lynx -dump https://www.yourdomain.com/script.php      

意思就是說通過lynx通路這個url。我們在使用中主要用到lynx、curl、wget來實作對url的遠端通路,而如果要提高效率,直接用php去執行本地php檔案是最佳選擇,例如:

00 */2 * * * /usr/local/bin/php /home/www/script.php      

這條語句就可以在每2小時的0分鐘,通過linux内部php環境執行script.php,注意,這裡可不是通過url通路,通過伺服器環境來執行哦,而是直接執行,因為繞過了伺服器環境,是以效率當然要高很多。

好了,已經添加了幾條需要的定時任務了吧。點選鍵盤上的Esc鍵,輸入“:wq”回車,這樣就儲存了設定的定時任務,螢幕上也能看到提示建立了新的定時任務。接下來就是好好寫你的script.php了。

關于CronTab的更多用法這裡就不介紹了,如果你想更靈活的使用這個定時任務功能,應該自己再去深入學習一下crontab。

Windows伺服器上使用bat定時執行php

windows上和linux上有一個類似的cmd和bat檔案,bat檔案類似于shell檔案,執行這個bat檔案,就相當于依次執行裡面的指令(當然,還可以通過邏輯來實作程式設計),是以,我們可以利用bat指令檔案在windows伺服器上面實作PHP定時任務。實際上在windows上定時任務,和linux上道理是一樣的,隻不過方法和途徑不同。

好了,下面開始。

首先,在一個你覺得比較适當的位置建立一個cron.bat檔案,然後用文本編輯器打開它(記事本都可以),在裡面寫上這樣的内容:

D:\php\php.exe -q D:\website\test.php      

這句話的意思就是,使用php.exe去執行test.php這個php檔案,和上面的contab一樣,繞過了伺服器環境,執行效率也比較高。寫好之後,點選儲存,關閉編輯器。

接下來就是設定定時任務來運作cron.bat。依次打開:“開始–>控制台–>任務計劃–>添加任務計劃”,在打開的界面中設定定時任務的時間、密碼,通過選擇,把cron.bat挂載進去。

确定,這樣一個定時任務就建立好了,在這個定時任務上右鍵,運作,這個定時任務就開始執行了,到點時,就會運作cron.bat處理,cron.bat再去執行php。

非自有伺服器(虛拟主機)上實作php定時任務

如果站長沒有自己的伺服器,而是租用虛拟主機,就無法進入伺服器系統進行上述操作。這個時候應該如何進行php定時任務呢?其實方法又有多個。

使用ignore_user_abort(true)和sleep死循環

在一個php文檔的開頭直接來一句:

ignore_user_abort(true);      

這時,通過url通路這個php的時候,即使使用者把浏覽器關掉(斷開連接配接),php也會在伺服器上繼續執行。利用這個特性,我們可以實作非常牛的功能,也就是通過它來實作定時任務的激活,激活之後就随便它自己怎麼辦了,實際上就有點類似于背景任務。

而sleep(n)則是指當程式執行到這裡時,暫時不往下執行,而是休息n秒鐘。如果你通路這個php,就會發現頁面起碼要加載n秒鐘。實際上,這種長時間等待的行為是比較消耗資源的,不能大量使用。

那麼定時任務到底怎麼實作呢?使用下面的代碼即可實作:

<?php


ignore_user_abort(true);
set_time_limit(0);
date_default_timezone_set(‘PRC‘); // 切換到中國的時間


$run_time = strtotime(‘+1 day‘); // 定時任務第一次執行的時間是明天的這個時候
$interval = 3600*12; // 每12個小時執行一次


if(!file_exists(dirname(__FILE__).‘/cron-run‘)) exit(); // 在目錄下存放一個cron-run檔案,如果這個檔案不存在,說明已經在執行過程中了,該任務就不能再激活,執行第二次,否則這個檔案被多次通路的話,伺服器就要崩潰掉了


do {
  if(!file_exists(dirname(__FILE__).‘/cron-switch‘)) break; // 如果不存在cron-switch這個檔案,就停止執行,這是一個開關的作用
  $gmt_time = microtime(true); // 目前的運作時間,精确到0.0001秒
  $loop = isset($loop) && $loop ? $loop : $run_time - $gmt_time; // 這裡處理是為了确定還要等多久才開始第一次執行任務,$loop就是要等多久才執行的時間間隔
  $loop = $loop > 0 ? $loop : 0;
  if(!$loop) break; // 如果循環的間隔為零,則停止
  sleep($loop); 
  // ...
  // 執行某些代碼
  // ...
  @unlink(dirname(__FILE__).‘/cron-run‘); // 這裡就是通過删除cron-run來告訴程式,這個定時任務已經在執行過程中,不能再執行一個新的同樣的任務
  $loop = $interval;
} while(true);      

通過執行上面這段php代碼,即可實作定時任務,直到你删除cron-switch檔案,這個任務才會停止。

但是有一個問題,也就是如果使用者直接通路這個php,實際上沒有任何作用,頁面也會停在這個地方,一直處于加載狀态,有沒有一種辦法可以消除這種影響呢?fsockopen幫我們解決了這個問題。

fsockopen可以實作在請求通路某個檔案時,不必獲得傳回結果就繼續往下執行程式,這是和curl通常用法不一樣的地方,我們在使用curl通路網頁時,一定要等curl加載完網頁後,才會執行curl後面的代碼,雖然實際上curl也可以實作“非阻塞式”的請求,但是比fsockopen複雜的多。

是以,我們優先選擇fsockopen,fsockopen可以在規定的時間内,比如1秒鐘以内,完成對通路路徑送出請求,完成之後就不管這個路徑是否傳回内容了,它的任務就到這裡結束,可以繼續往下執行程式了。

利用這個特性,我們在正常的程式流中加入fsockopen,對上面我們建立的這個定時任務php的位址送出請求,即可讓定時任務在背景執行。如果上面這個php的url位址是www.yourdomain.com/script.php,那麼我們在程式設計中,可以這樣:​

// ...
// 正常的php執行程式
// ..


// 遠端請求(不擷取内容)函數,下面可以反複使用
function _sock($url) {
  $host = parse_url($url,PHP_URL_HOST);
  $port = parse_url($url,PHP_URL_PORT);
  $port = $port ? $port : 80;
  $scheme = parse_url($url,PHP_URL_SCHEME);
  $path = parse_url($url,PHP_URL_PATH);
  $query = parse_url($url,PHP_URL_QUERY);
  if($query) $path .= ‘?‘.$query;
  if($scheme == ‘https‘) {
    $host = ‘ssl://‘.$host;
  }


  $fp = fsockopen($host,$port,$error_code,$error_msg,1);
  if(!$fp) {
    return array(‘error_code‘ => $error_code,‘error_msg‘ => $error_msg);
  }
  else {
    stream_set_blocking($fp,true);//開啟了手冊上說的非阻塞模式
    stream_set_timeout($fp,1);//設定逾時
    $header = "GET $path HTTP/1.1\r\n";
    $header.="Host: $host\r\n";
    $header.="Connection: close\r\n\r\n";//長連接配接關閉
    fwrite($fp, $header);
    usleep(1000); // 這一句也是關鍵,如果沒有這延時,可能在nginx伺服器上就無法執行成功
    fclose($fp);
    return array(‘error_code‘ => 0);
  }
}


_sock(‘www.yourdomain.com/script.php‘);


// ...
// 繼續執行其他動作
// ..      

把這段代碼加入到某個定時任務送出結果程式中,在設定好時間後,送出,然後執行上面這個代碼,就可以激活該定時任務,而且對于送出的這個使用者而言,沒有任何頁面上的堵塞感。

借用使用者的通路行為來執行某些延遲任務

但是上面使用sleep來實作定時任務,是效率很低的一種方案。我們希望不要使用這種方式來執行,這樣的話就可以解決效率問題。

我們借用使用者通路行為來執行任務。使用者對網站的通路其實是一個非常豐富的行為資源,包括搜尋引擎蜘蛛對網站的通路,都可以算作這個類型。

在使用者通路網站時,内部加一個動作,去檢查任務清單中是否存在沒有被執行的任務,如果存在,就将這個任務執行。對于使用者而言,利用上面所說的fsockopen,根本感覺不到自己的通路竟然還做出了這樣的貢獻。

但是這種通路的缺點就是通路很不規律,比如你希望在淩晨2點執行某項任務,但是這個時間段非常倒黴,沒有使用者或任何行為到達你的網站,直到早上6點才有一個新通路。

這就導緻你原本打算2點執行的任務,到6點才被執行。

這裡涉及到一個定時任務清單,也就是說你需要有一個清單來記錄所有任務的時間、執行什麼内容。

一般來說,很多系統會采用資料庫來記錄這些任務清單,比如wordpress就是這樣做的。我則利用檔案讀寫特性,提供了托管在github上的開源項目php-cron,你可以去看看。

總之,如果你想要管理多個定時任務,靠上面的單個php是無法合理布局的,必須想辦法建構一個schedules清單。由于這裡面的邏輯比較複雜,就不再詳細闡述,我們僅停留在思路層面上。

借用第三方定時任務跳闆

很好玩的是,一些服務商提供了各種類型的定時任務,例如阿裡雲的ACE提供了單獨的定時任務,你可以填寫自己應用下的某個uri。

百度雲BCE提供了伺服器監測功能,每天會按照一定的時間規律通路應用下的固定uri。類似的第三方平台上還有很多定時任務可以用。

你完全可以用這些第三方定時任務作為跳闆,為你的網站定時任務服務。

比如說,你可以在阿裡雲ACE上建立一個每天淩晨2點的定時任務,執行的uri是/cron.php。

然後,你建立一個cron.php,裡面則采用fsockopen去通路你真正要執行某些任務的網站的url,例如上面的www.yourdomain.com/script.php,而且在cron.php中還可以通路多個url。

然後,把cron.php上傳到你的ACE上面去,讓ACE的定時任務去通路/cron.php,然後讓cron.php去遠端請求目标網站的定時任務腳本。

循環利用include包含檔案(待驗證)

php面向過程的特性使得其程式是從上往下執行的,利用這個特性,在我們使用include某個檔案時,就會執行被引入的檔案,知道include的檔案内程式執行完之後,再往下執行。

如果我們建立一個循環,再利用sleep,不斷的include某個檔案,使循環執行某段程式,則可以達到定時執行的目的。我們再進一步,并不是利用while(true)來實作循環,而是利用被include檔案本身再include自身來實作循環,比如我們建立一個do.php,它的内容如下:​

if(...) exit(); // 通過某個開關來關閉執行


// ... 
// 執行某些程式
// ...


sleep($loop); // 這個$loop在include(‘do.php‘);之前指派


include(dirname(__FILE__).‘/do.php‘);      

其實通過這種方法執行和while的思路也像。而且同樣用到sleep,效率低。

PHP定時任務是一個非常有意思的東西,雖然說實話,用系統的php.exe去直接執行php檔案的效率更高,但是對于很多普通站長而言,虛拟主機是無法做到直接php執行原生程式的。

本文僅提供一些解決的思路,我也僅僅是在學習中,有很多問題或表述都不正确,希望你指出來;你可以通過本文的思路,開發出自己的一種解決方案,希望你能将方案釋出,并與我一起探讨。

PHP實作執行定時任務的幾種思路詳解