當PHP處理一個不確定執行時間的工作的時候,很可能會發生第一個還沒完成,下一個就已經開始的情況。例如,我們執行一個定時傳送郵件任務,如果需要傳送的郵件數量超出了預期,就會導致兩個不同的PHP程式在同時執行,可能發生異常情況。
這篇文章裡不使用Redis、RDS等系統,因為它們不適用於一些功能簡單的獨立指令碼。如果你使用了諸如Laravel之類的框架,使用它們是更優解。
問題分析和解決
我們以定時傳送郵件這個小功能為例子,來思考一下真實場景裡可能會出現的問題
1. 同時有多個不同程式在傳送郵件
PHP程式在處理完髮送郵件這個動作後會把郵箱從待傳送列表中刪除。某個時段的使用者量大增,同時有多個程式先後被Cron呼叫來處理郵件,因為傳送郵件和修改列表之間有時間差,導致部分使用者收到了多封相同的郵件,浪費了錢,使用者體驗也不好。
解決方法:
使用檔案鎖,同一時間只允許一個程式執行。
<?php
// 設定檔案鎖的路徑和檔名
$lock = __DIR__ . '/cron.lock';
// 假如檔案鎖存在,退出
if(file_exists($lock)) {
exit('Other cron task is running');
}
// 建立檔案鎖
touch($lock);
// 處理髮送郵件的邏輯
// 執行完畢,釋放檔案鎖
unlink($lock);
2. 郵件程式丟擲的異常未處理
我們發現有個使用者蜜汁操作,繞過了各種過濾措施設定了一個沒辦法解析的郵箱地址,當系統嘗試給這個郵箱傳送郵件時必報錯,導致PHP異常退出。退出就算了,退出的時候還沒釋放檔案鎖,後來的定時任務都沒辦法處理郵件,只能人工介入處理。
解決方法:
使用try,catch,finally三件套,保證不管是正常、異常,都能釋放檔案鎖。
<?php
$lock = __DIR__ . '/cron.lock';
if(file_exists($lock)) {
exit('Other cron task is running');
}
try{
touch($lock);
// 處理髮送郵件的邏輯
}catch (Exception $exception){
// 回顯報錯資訊
echo $exception->getMessage();
}finally{
unlink($lock);
}
3. 郵件伺服器當機,程式僵死
由於遠端的伺服器出問題,PHP不斷嘗試傳送未果,陷入死迴圈,我們需要在PHP執行時間遠超預期的情況下,強行終止PHP程式,避免伺服器被死迴圈拖累了效能。
解決方法:
給設定PHP超時時間,當執行時間超出我們設定的時間後,程式會被強行終止。只需要在程式碼開頭呼叫 set_time_limit($time)
即可,$time
是超時的秒數,當 $time
的值為 0
時,程式永不超時。
// 設定PHP最大執行時間為600秒
set_time_limit(600);
4. 出現致命錯誤
這個錯誤是由上一個問題引出的,我們發現當指令碼超時後,會發生一個致命錯誤(Fatal error),由於致命錯誤不是異常(Exception),不能被捕獲,當PHP超時或者其他原因發生了致命錯誤,很可能發生沒有釋放檔案鎖的情況。
解決方法:
通過 register_shutdown_function()
註冊PHP結束時執行的函式,當PHP程式結束之前會呼叫註冊的函式。這個方法只能處理來自PHP程式內部的退出,不能處理系統結束程式的情況,所以手動kill掉程式還是不行的。
<?php
$lock = __DIR__ . '/cron.lock';
// 設定PHP超時時間(秒)
set_time_limit(600);
// 註冊PHP結束時執行的函式
register_shutdown_function('unlock');
if(file_exists($lock)) {
exit('Other cron task is running');
}
touch($lock);
// 傳送郵件的邏輯
// 釋放檔案鎖的函式
function unlock(){
unlink($lock);
}
總結
我們已經解決了上面這一大堆的坑,這個PHP指令碼總算是可堪一用,不會日常爆炸了。
使用時我們需要給PHP程式使用者讀寫 cron.lock
檔案鎖的許可權。
網站使用者一般為apache
或www
,命令列呼叫則是設定計劃任務的使用者。
如果是命令列指令碼的模式,Linux環境中只需設定cron
* * * * * /path/to/php /path/to/cron.php
優點
- 結構簡單,易於理解
- 系統資源需求低
- 不依賴 Redis、RDS 等佇列系統
- 不依賴特定系統,系統只提供計劃任務
缺點
- 需要進行IO讀寫檔案鎖
- 不適合叢集部署
- 無法解決系統kill程式的問題
- 意外上鎖後無法自己解鎖,需要人工刪除鎖檔案
適用場景
- 資源緊缺的VPS,路由器等
- 需要方便在不同平臺部署
- 單指令碼或簡單的網站
同步釋出在我的部落格 玉米君 中
本作品採用《CC 協議》,轉載必須註明作者和本文連結