首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 進程同步鎖實作:防止排程任務重複執行

     編輯:PHP 綜合知識

問題背景

在 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 排程任務防重的首選方案。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved