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

Linux 下 PHP 判斷腳本是否已在運行的兩種方法(防止 Crontab 重複執行)

     編輯:PHP 相關知識

在 Linux 服務器上通過 Crontab 定時執行 PHP 腳本時,若上一次執行未結束就觸發下一次,會導致進程疊加、資源耗盡、數據重複處理等嚴重問題。本文介紹兩種純 PHP 實現的「單例運行」檢測方法:基於 ps 進程掃描和基於 PID 文件鎖,並分析優缺點與生產環境建議。

方法一:掃描進程列表匹配腳本名稱

<?php
/**
 * 檢測指定腳本是否正在運行
 * @param string $scriptName 腳本文件名(不含路徑),如 'cron_sync.php'
 * @param int $maxAllowed 允許的最大並發數(含當前),默認 1
 * @return bool true=已在運行/超限,false=可安全執行
 */
function isScriptRunning($scriptName, $maxAllowed = 1)
{
    // 獲取所有進程完整命令行
    $cmd = "ps -eo cmd --no-headers";
    $output = shell_exec($cmd);
    if ($output === null) {
        // shell_exec 失敗,保守策略:允許運行
        return false;
    }

    // 統計匹配行數(排除 grep 自身)
    $pattern = '/php.*' . preg_quote($scriptName, '/') . '/';
    $matches = array();
    preg_match_all($pattern, $output, $matches);
    $count = count($matches[0]);

    return $count >= $maxAllowed;
}

// --- 使用示例 ---
$script = 'pooy.php'; // 當前腳本文件名
if (isScriptRunning($script, 1)) {
    die("[$script] 已在運行,退出。\n");
}

// 正常業務邏輯...
echo "開始執行任務...\n";
sleep(10); // 模擬耗時操作
echo "任務完成。\n";
?>

優缺點分析

優點缺點
無需額外文件/目錄權限ps 輸出格式因系統而異,正則可能不準
實現簡單,無依賴高併發時竞態條件:檢測到執行之間可能已結束/新啟動
可檢測不同用戶/路徑下的同名腳本無法區分「正常運行」與「僵屍/卡死」進程

方法二:PID 文件鎖(推薦生產環境)

<?php
/**
 * 基於文件鎖的單例運行保護
 * @param string $lockFile 鎖文件路徑(建議放在 /var/run/ 或 /tmp/)
 * @return resource|false 成功返回文件句柄(腳本結束自動釋放),失敗返回 false
 */
function acquireLock($lockFile)
{
    $fp = fopen($lockFile, 'c'); // 'c' 模式:打開寫入,指針在開頭,不截斷
    if (!$fp) {
        return false;
    }

    // 非阻塞獨佔鎖
    if (!flock($fp, LOCK_EX | LOCK_NB)) {
        fclose($fp);
        return false;
    }

    // 寫入當前 PID,便於監控/調試
    ftruncate($fp, 0);
    fwrite($fp, getmypid() . "\n");
    fflush($fp);

    // 註冊關閉釋放(雖然腳本結束會自動釋放,但顯式釋放更規範)
    register_shutdown_function(function() use ($fp, $lockFile) {
        flock($fp, LOCK_UN);
        fclose($fp);
        @unlink($lockFile); // 可選:刪除鎖文件
    });

    return $fp;
}

// --- 使用示例 ---
$lockFile = '/var/run/my_cron_job.lock'; // 需寫入權限,建議專用目錄
$fp = acquireLock($lockFile);
if (!$fp) {
    die("另一個實例正在運行,退出。\n");
}

echo "獲得鎖,開始執行...\n";
// 業務邏輯...
sleep(15);
echo "執行結束,釋放鎖。\n";
// 腳本結束自動釋放鎖
?>

優缺點分析

優點缺點
原子操作,無竞態條件需文件系統支持 flock(NFS 可能不支持)
進程異常退出自動釋放鎖(內核保證)需管理鎖文件目錄權限(/var/run 需 root 創建子目錄授權)
可讀取 PID 進行監控/告警若腳本被 kill -9,鎖文件殘留需清理邏輯

生產環境最佳實踐:組合策略

<?php
// cron_wrapper.php - 通用包裝腳本
$targetScript = $argv[1] ?? '';
$lockDir = '/var/lock/myapp';
$lockFile = $lockDir . '/' . basename($targetScript, '.php') . '.lock';

if (!$targetScript || !is_file($targetScript)) {
    fwrite(STDERR, "用法: php cron_wrapper.php /path/to/script.php\n");
    exit(1);
}

// 1. 嘗試獲取文件鎖
$fp = @fopen($lockFile, 'c');
if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
    // 2. 兼容無 flock 環境:回退進程掃描
    $cmd = "ps -eo pid,cmd --no-headers | grep -v grep | grep " . escapeshellarg($targetScript);
    $count = count(explode("\n", trim(shell_exec($cmd))));
    if ($count > 0) {
        exit(0); // 靜默退出,不污染日誌
    }
    // 無鎖且無進程,可能是 flock 不支持,繼續執行
} else {
    fwrite($fp, getmypid());
}

// 3. 執行目標腳本(同進程 include,或 exec/php 命令)
// 方式 A:同進程(輕量,注意變量隔離)
include $targetScript;
// 方式 B:子進程(隔離性好,推薦長耗時/大內存任務)
// passthru("php " . escapeshellarg($targetScript) . " >> /var/log/myapp/cron.log 2>&1");

// 4. 釋放鎖
if ($fp) {
    flock($fp, LOCK_UN);
    fclose($fp);
    @unlink($lockFile);
}
?>

Crontab 配置示例

# 每分鐘檢查一次,由包裝腳本保證單例
* * * * * /usr/bin/php /opt/scripts/cron_wrapper.php /opt/scripts/sync_orders.php >> /dev/null 2>&1
* * * * * /usr/bin/php /opt/scripts/cron_wrapper.php /opt/scripts/cleanup_cache.php >> /dev/null 2>&1

總結建議

  1. 首選 flock 文件鎖:原子性、自動釋放、可監控 PID,適合 99% 的定時任務場景。
  2. 鎖文件放在持久化目錄:/var/lock/ 或 /var/run/(需 root 預創建子目錄並 chown 給 PHP 運行用戶),避免 /tmp 被系統清理導致鎖丟失。
  3. 包裝腳本統一管理:將鎖邏輯剝離到 cron_wrapper.php,業務腳本保持純粹,通過參數調用。
  4. 監控告警:定期檢查鎖文件年齡(find /var/lock -mmin +60),發現長期持有鎖的進程發送告警,防止死鎖。

正確的單例保護機制是 Linux 定時任務穩定運行的基石。拒絕「靠運氣不重疊」,從今天起為每個 Crontab 腳本加上專業的鎖。

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