Linux Crontab 定時任務若執行時間超過間隔周期,或因系統抖動導致重複啟動,會引發嚴重數據不一致(如重複扣款、重複發送郵件)。本文提供基於 ps aux 進程掃描的防重複執行機制,適用於無法使用文件鎖、數據庫鎖、Redis 鎖的輕量級場景。
<?php
class JobLock {
/**
* 驗證是否已有同名任務在運行
* @param string $jobName 任務標識(對應腳本文件名或業務標識)
* @param string $scriptPath 入口腳本完整路徑(如 /var/www/html/index.php)
* @param string $route 路由/參數部分(如 /records/job/syncData)
* @return void 若檢測到重複則直接 exit()
*/
public static function verify( $jobName, $scriptPath, $route ) {
// 構建進程命令特徵字符串
$cmdPattern = $scriptPath . ' ' . $route;
// 執行 ps aux 並 grep 過濾
exec("ps aux | grep '" . escapeshellarg($cmdPattern) . "'", $output);
$runningCount = 0;
foreach ( $output as $line ) {
// 過濾掉 grep 進程本身
if ( strpos($line, 'grep') !== false ) continue;
// 匹配完整命令特徵
if ( strpos($line, $cmdPattern) !== false ) {
$runningCount++;
}
}
// 當前進程 + 歷史殘留 = 1 為正常;>1 表示重複
if ( $runningCount > 1 ) {
$timestamp = date('Y-m-d H:i:s');
error_log("[$timestamp] Job '$jobName' duplicate detected (count: $runningCount), aborting.");
exit(1); // 非零退出碼便於監控告警
}
}
}
// === 使用示例 ===
// 在任務入口文件最開頭調用
require_once 'JobLock.php';
// 參數說明:
// $jobName: 業務標識,用於日誌
// $scriptPath: PHP 入口腳本絕對路徑
// $route: 框架路由或腳本參數
JobLock::verify(
'syncUserData',
'/usr/local/www/scrm/public/index.php',
'/records/job/syncUserData'
);
// 後續正常業務邏輯...
?>
| 要點 | 處理方式 |
|---|---|
| 命令注入防護 | escapeshellarg() 轉義傳入 exec() 的參數 |
| grep 進程自匹配 | 過濾包含 grep 關鍵字的行 |
| 命令特徵唯一性 | 使用「入口腳本 + 路由」組合,區分不同任務 |
| 計數邏輯 | 正常情況 $runningCount == 1(僅當前進程);>1 視為重複 |
| 退出碼 | exit(1) 非零碼,便於 Cron 監控工具識別異常 |
| 日誌記錄 | error_log() 寫入系統/應用日誌,包含時間戳與計數 |
ps aux 方案存在竞態條件(檢查與啟動間隙)且依賴進程命令行格式。生產環境強烈建議使用 PID 文件鎖:
class PidLock {
private $pidFile;
private $fp;
public function __construct( $jobName ) {
$this->pidFile = sys_get_temp_dir() . "/job_lock_{$jobName}.pid";
}
public function acquire(): bool {
$this->fp = fopen($this->pidFile, 'c+');
if ( ! $this->fp ) return false;
// 非阻塞排他鎖
if ( ! flock($this->fp, LOCK_EX | LOCK_NB) ) {
fclose($this->fp);
return false; // 已有進程持有鎖
}
// 寫入當前 PID
ftruncate($this->fp, 0);
fwrite($this->fp, getmypid());
fflush($this->fp);
return true;
}
public function release() {
if ( $this->fp ) {
flock($this->fp, LOCK_UN);
fclose($this->fp);
@unlink($this->pidFile);
}
}
public function __destruct() {
$this->release();
}
}
// 使用
$lock = new PidLock('syncUserData');
if ( ! $lock->acquire() ) {
error_log("[JobLock] syncUserData already running, exit.");
exit(1);
}
// 業務邏輯...
$lock->release(); // 或依賴析構函數自動釋放
| 方案 | 可靠性 | 複雜度 | 跨機器 | 適用場景 |
|---|---|---|---|---|
| ps aux 掃描 | 中(有竞態) | 低 | 否 | 單機、輕量、無文件寫入權限 |
| PID 文件鎖 (flock) | 高(內核級原子操作) | 低 | 否 | 單機標準生產環境首選 |
| 數據庫行鎖 (SELECT FOR UPDATE) | 高 | 中 | 是 | 分布式、已有 DB 連接池 |
| Redis SETNX 鎖 | 高 | 中 | 是 | 分布式、高併發、需自動過期 |
| 系統工具 flock(1) | 高 | 極低 | 否 | Crontab 直接包裝:flock -n /tmp/job.lock -c 'php job.php' |
# /etc/crontab * * * * * www-data flock -n /tmp/sync_user_data.lock -c 'php /var/www/html/index.php /records/job/syncUserData >> /var/log/sync_user_data.log 2>&1'
-n:非阻塞,鎖被佔用時直接退出(不等待)-c:執行命令字符串根據項目架構選擇合適方案:單機首選 flock(1) 或 PID 文件鎖;分布式環境使用 Redis/數據庫鎖;ps aux 僅作為無文件寫入權限環境的備選。