PHP 設計為「請求-回應」模式,腳本執行完即結束。要實現定時任務,必須引入外部觸發機制或讓腳本長期運行。主流方案對比:
| 方案 | 原理 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| 系統 Crontab + CLI | 作業系統排程器定時呼叫 php /path/script.php | 精準、可靠、標準化、支援並行、記憶體隔離 | 需伺服器 root/Shell 存取權 | VPS/專用主機、生產環境首選 |
| Crontab + wget/curl | 排程器發送 HTTP 請求觸發 Web 端點 | 無需 CLI 權限、共享主機可用 | 受 Web 超時/記憶體限制、需防止重複執行 | 共享主機、無 CLI 權限環境 |
| Web 觸發(用戶請求驅動) | 每次用戶訪問檢查任務隊列,到期則執行(Discuz、UCenter Home 模式) | 零配置、無需系統排程 | 流量低時任務延遲、增加請求延遲、併發風險 | 低流量站點、簡單維護任務 |
| ignore_user_abort() + 無限循環 | 腳本啟動後忽略用戶斷線,sleep() 間隔執行 | 純 PHP 實現、無外部依賴 | 長期運行記憶體洩漏、單進程阻塞、部署管理困難 | 臨時測試、無任何權限環境 |
| 守護進程 | nohup php daemon.php >/dev/null 2>&1 & 或 Systemd/Supervisor 管理 | 常駐記憶體、高效能、支援信號控制 | 需 CLI、進程管理知識、記憶體洩漏風險 | 高併發、即時性要求高的任務隊列 |
| 消息隊列 + Worker | Redis/RabbitMQ/Beanstalkd 分發任務,多 Worker 消費 | 解耦、可擴展、支援優先級、重試、監控 | 架構複雜、引入額外基礎設施 | 中大型系統、異步處理、排程任務 |
# 編輯 crontab:crontab -e
# 每分鐘執行一次
* * * * * /usr/bin/php /var/www/html/cron/minute.php >> /var/log/cron_minute.log 2>&1
# 每天凌晨 3:30 執行備份
30 3 * * * /usr/bin/php /var/www/html/cron/backup.php >> /var/log/cron_backup.log 2>&1
腳本範本:
<?php
// cron/minute.php
define('CRON_RUN', true);
require_once __DIR__ . '/../bootstrap.php'; // 載入框架/設定
// 防止重複執行(單實例鎖)
$lockFile = '/tmp/cron_minute.lock';
$fp = fopen($lockFile, 'w');
if (!flock($fp, LOCK_EX | LOCK_NB)) {
exit("另一實例正在運行\n");
}
// 任務邏輯
try {
// 1. 清理過期快取
Cache::cleanExpired();
// 2. 發送待發郵件
EmailQueue::process(50);
// 3. 統計每日 PV/UV
Stats::dailyRollup();
} catch (Throwable $e) {
error_log("[CRON] " . $e->getMessage());
} finally {
flock($fp, LOCK_UN);
fclose($fp);
}
?>
資料表 cron 儲存任務(nextrun、interval、filename)。每次請求掛載 cron.php:
<?php
// common.php 或框架入口處引入
if (defined('IN_CRON')) return; // 避免重複
$now = time();
$tasks = DB::fetch_all("SELECT * FROM cron WHERE nextrun <= $now AND available = 1");
foreach ($tasks as $task) {
// 更新下次執行時間,防止併發重複執行
DB::query("UPDATE cron SET nextrun = nextrun + interval WHERE cronid = {$task['cronid']}");
// 執行任務檔案
@include_once DISCUZ_ROOT . "./source/cron/{$task['filename']}";
}
?>
優化:僅在非 AJAX、非靜態資源請求時觸發;使用 fastcgi_finish_request() 先回應用戶,再執行任務。
<?php
ignore_user_abort(true); // 客戶端斷開也繼續執行
set_time_limit(0); // 無限執行時間
$interval = 30; // 每 30 秒執行一次
$lockFile = '/tmp/my_daemon.lock';
$fp = fopen($lockFile, 'w');
if (!flock($fp, LOCK_EX | LOCK_NB)) die("已有實例運行\n");
register_shutdown_function(function() use ($fp, $lockFile) {
flock($fp, LOCK_UN);
fclose($fp);
@unlink($lockFile);
});
while (true) {
// 任務邏輯
$fpLog = fopen('task_log.txt', 'a');
fwrite($fpLog, date('Y-m-d H:i:s') . " 執行任務\n");
fclose($fpLog);
sleep($interval);
// 可選:檢查停止信號檔案
if (file_exists('/tmp/stop_daemon')) break;
}
?>
嚴重缺陷:PHP 非為長期運行設計,擴展記憶體洩漏、無法優雅重啟、單點故障。僅適合臨時測試。
Supervisor 配置 /etc/supervisor/conf.d/worker.conf:
[program:php-worker]
command=/usr/bin/php /var/www/html/worker.php
process_name=%(program_name)s_%(process_num)02d
numprocs=4
autostart=true
autorestart=true
user=www-data
stdout_logfile=/var/log/worker.log
redirect_stderr=true
Worker 腳本(消費 Redis 隊列):
<?php
require 'vendor/autoload.php';
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
while (true) {
// 阻塞式取任務,超時 5 秒
$job = $redis->blpop('job_queue', 5);
if (!$job) continue;
$data = json_decode($job[1], true);
try {
// 處理任務...
echo "Processed: " . $data['id'] . "\n";
} catch (Throwable $e) {
// 重試邏輯、死信隊列...
error_log($e->getMessage());
}
}
?>
flock)防止重複啟動。unset()、定期 gc_collect_cycles() 控制記憶體。Throwable、記錄日誌、不讓單個任務失敗導致進程退出。pcntl_signal 捕獲 SIGTERM)。ignore_user_abort() + sleep() 死循環作為生產方案。