PHP 避免同時執行一個指令碼

zylntxx發表於2020-03-27

當PHP處理一個不確定執行時間的工作的時候,很可能會發生第一個還沒完成,下一個就已經開始的情況。例如,我們執行一個定時傳送郵件任務,如果需要傳送的郵件數量超出了預期,就會導致兩個不同的PHP程式在同時執行,可能發生異常情況。

這篇文章裡不使用Redis、RDS等系統,因為它們不適用於一些功能簡單的獨立指令碼。如果你使用了諸如Laravel之類的框架,使用它們是更優解。

[TOC]

問題分析和解決

我們以定時傳送郵件這個小功能為例子,來思考一下真實場景裡可能會出現的問題

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 檔案鎖的許可權。

網站使用者一般為apachewww,命令列呼叫則是設定計劃任務的使用者。

如果是命令列指令碼的模式,Linux環境中只需設定cron

* * * * * /path/to/php /path/to/cron.php

優點

  • 結構簡單,易於理解
  • 系統資源需求低
  • 不依賴 Redis、RDS 等佇列系統
  • 不依賴特定系統,系統只提供計劃任務

缺點

  • 需要進行IO讀寫檔案鎖
  • 不適合叢集部署
  • 無法解決系統kill程式的問題
  • 意外上鎖後無法自己解鎖,需要人工刪除鎖檔案

適用場景

  • 資源緊缺的VPS,路由器等
  • 需要方便在不同平臺部署
  • 單指令碼或簡單的網站

同步釋出在我的部落格 玉米君

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章