PHP自己是沒有定時功能的,PHP也不能多線程。PHP的定時任務功能必須經過和其餘工具結合才能實現,例如WordPress內置了wp-cron的功能,很厲害。本文,咱們就來深刻的解析幾種常見的php定時任務的思路。php
咱們先從相對比較複雜的服務器執行php談起。服務器上安裝了php,就能夠執行php文件,不管是否安裝了nginx或Apache這樣的服務器環境軟件。而Linux中,使用命令行,用CronTab來定時任務,又是絕佳的選擇,並且也是效率最高的選擇。linux
首先,進入命令行模式。做爲服務器的linux通常都默認進入命令行模式的,固然,咱們管理服務器也通常經過putty等工具遠程鏈接到服務器,爲了方便,咱們用root用戶登陸。在命令行中鍵入:nginx
crontab -e
以後就會打開一個文件,而且是非編輯狀態,則是vi的編輯界面,經過敲鍵盤上的i,進入編輯模式,就能夠編輯內容。這個文件中的每一行就是一個定時任務,咱們新建一行,就是新建一條定時任務(固然是指這一行內按照必定的格式進行書寫)。咱們如今來舉個例子,增長一行,內容以下:git
00 * * * * lynx -dump https://www.yourdomain.com/script.php
這是什麼意思呢?實際上上面這一行由兩部分組成,前面一部分是時間,後面一部分是操做內容。例如上面這個,github
00 * * * *
就是指噹噹前時間的分鐘數爲00時,執行該定時任務。時間部分由5個時間參數組成,分別是:web
分 時 日 月 周
第1列表示分鐘1~59 每分鐘用或者 */1表示,/n表示每n分鐘,例如*/8就是每8分鐘的意思,下面也是類推
第2列表示小時1~23(0表示0點)
第3列表示日期1~31
第4列表示月份1~12
第5列標識號星期0~6(0表示星期天)shell
整個句子的後面部分就是操做的具體內容。數據庫
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訪問,經過服務器環境來執行哦,而是直接執行,由於繞過了服務器環境,因此效率固然要高不少。windows
好了,已經添加了幾條須要的定時任務了吧。點擊鍵盤上的Esc鍵,輸入「:wq」回車,這樣就保存了設置的定時任務,屏幕上也能看到提示建立了新的定時任務。接下來就是好好寫你的script.php了。
關於CronTab的更多用法這裏就不介紹了,若是你想更靈活的使用這個定時任務功能,應該本身再去深刻學習一下crontab。
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);
這時,經過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去遠程請求目標網站的定時任務腳本。
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執行原生程序的。本文僅提供一些解決的思路,我也僅僅是在學習中,有不少問題或表述都不正確,但願你指出來;你能夠經過本文的思路,開發出本身的一種解決方案,但願你能將方案發布,並與我一塊兒探討。