在 Linux 環境下,透過 Crontab 執行的 PHP 排程任務,若執行時間較長,下一個排程時間到來時上一個尚未結束,會導致重複執行,造成資料不一致、資源爭用甚至系統過載。需要一種可靠的進程同步機制,確保同一任務同一時間僅有一個執行實例。
pidFile = $pidFile;
}
/**
* 非阻塞檢查:若有其他實例正在執行回傳 true,否則取得鎖並回傳 false
*/
public function isRunning(): bool
{
if (PHP_OS_FAMILY !== 'Linux') {
trigger_error('ProcessSync 僅支援 Linux', E_USER_WARNING);
return true; // 保守策略:非 Linux 視為正在執行
}
$dir = dirname($this->pidFile);
if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
trigger_error("PID 目錄建立失敗: $dir", E_USER_WARNING);
return true;
}
// 確保檔案存在
if (!file_exists($this->pidFile)) {
file_put_contents($this->pidFile, '', LOCK_EX);
}
$fp = fopen($this->pidFile, 'r+');
if (!$fp) {
trigger_error("無法開啟 PID 檔案: {$this->pidFile}", E_USER_WARNING);
return true;
}
// 非阻塞獨佔鎖
if (!flock($fp, LOCK_EX | LOCK_NB)) {
fclose($fp);
return true; // 取得鎖失敗,代表有其他進程持有
}
// 檢查檔案中記錄的 PID 是否仍在運行
$pid = trim(fgets($fp));
$running = $this->isProcessAlive((int)$pid);
if (!$running) {
// 死鎖清理:寫入目前 PID
fseek($fp, 0);
ftruncate($fp, 0);
fwrite($fp, (string)getmypid());
}
flock($fp, LOCK_UN);
fclose($fp);
return $running;
}
private function isProcessAlive(int $pid): bool
{
if ($pid <= 0) return false;
$output = [];
exec("ps -o pid --no-headers -p $pid", $output);
$output = array_map('trim', $output);
return in_array((string)$pid, $output, true);
}
}
isRunning()) {
// 已有實例執行中,靜默退出或記錄日誌
error_log('[SKIP] daily_report 已在執行中 ' . date('c'));
exit(0);
}
// === 實際業務邏輯開始 ===
// 長時間運作:資料匯出、報表產生、API 同步等
sleep(10); // 模擬耗時任務
// === 實際業務邏輯結束 ===
// 程式正常結束,鎖會隨進程結束自動釋放(檔案描述符關閉)
// 也可顯式清空 PID 檔案(可選)
file_put_contents('/var/run/myapp/tasks/daily_report.pid', '');
| 機制 | 說明 | 優勢 |
|---|---|---|
| flock 建議鎖 | 進程結束自動釋放,不會殘留死鎖 | 無需手動清理,極高可靠度 |
| 非阻塞模式 | LOCK_NB 立即返回,不阻塞 | 適合排程「跳過」而非「等待」語意 |
| PID 存活檢查 | ps -p $pid 驗證進程是否存在 | 自動偵測並清理異常中斷的殘留鎖 |
| 原子性寫入 | ftruncate + fwrite 覆寫 PID | 避免競態條件 |
public function waitLock(int $timeout = 30): bool
{
$fp = fopen($this->pidFile, 'r+');
$start = time();
while (time() - $start < $timeout) {
if (flock($fp, LOCK_EX | LOCK_NB)) {
ftruncate($fp, 0);
fwrite($fp, (string)getmypid());
return true;
}
usleep(200000); // 200ms
}
fclose($fp);
return false;
}
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| flock 檔案鎖(本文) | 簡單、無額外依賴、自動釋放 | 僅限單機、NFS 不支援 | 單伺服器排程任務 |
| Redis SETNX 分散式鎖 | 跨多伺服器、高效能 | 需維護 Redis、處理時鐘漂移 | 分散式叢集排程 |
| 資料庫 GET_LOCK() | 無額外元件、支援交易 | 佔用資料庫連線、效能較低 | 已強依賴 DB 的簡單場景 |
| systemd timer + oneshot | 系統級管理、無重複執行保證 | 需 root、學習曲線較高 | 正式生產環境替代 Crontab |
檔案鎖方案零依賴、部署簡單,是單機 PHP 排程任務防重的首選方案。