PHP 預設為請求-響應模式,無原生定時器。需實現:用戶關閉瀏覽器後繼續執行、突破 30 秒限制、固定間隔循環執行任務(如定時生成靜態、發送郵件、數據同步)。
<?php
// 1. 忽略用戶斷開連線(關閉瀏覽器仍繼續執行)
ignore_user_abort(true);
// 2. 移除執行時間限制(預設 30 秒)
set_time_limit(0);
// 3. 設定間隔(秒)
$interval = 24 * 60 * 60; // 每天執行一次
// 4. 無限循環
do {
// 等待指定間隔
sleep($interval);
// 執行任務
$sql = "UPDATE blog SET updated_at = NOW() WHERE status = 1";
// $db->execute($sql);
// 其他操作:生成報表、發送郵件、清理快取...
} while (true);
| 函數 | 作用 | 注意事項 |
|---|---|---|
| ignore_user_abort(true) | 客戶端斷開不終止腳本 | 需在輸出前調用;CLI 模式預設 true |
| set_time_limit(0) | 移除 max_execution_time 限制 | 安全模式下無效;需 php.ini 允許 |
| sleep($seconds) | 暫停執行指定秒數 | 接收信號會提前返回;usleep 微秒級 |
| connection_aborted() | 檢測客戶端是否斷開 | 配合 ignore_user_abort 判斷 |
<?php
class CronTask
{
private $lockFile;
private $logFile;
private $interval;
private $running = true;
public function __construct($interval, $name = 'cron')
{
$this->interval = $interval;
$this->lockFile = sys_get_temp_dir() . "/{$name}.lock";
$this->logFile = __DIR__ . "/logs/{$name}.log";
// 信號處理:優雅退出
declare(ticks = 1);
pcntl_signal(SIGTERM, array($this, 'signalHandler'));
pcntl_signal(SIGINT, array($this, 'signalHandler'));
}
public function run()
{
// 單實例鎖
if (!$this->acquireLock()) {
$this->log("另一實例正在運行,退出");
return;
}
ignore_user_abort(true);
set_time_limit(0);
$this->log("任務啟動,間隔: {$this->interval} 秒");
while ($this->running) {
$start = microtime(true);
try {
$this->executeTask();
} catch (Exception $e) {
$this->log("任務異常: " . $e->getMessage());
}
$elapsed = microtime(true) - $start;
$sleepTime = max(1, $this->interval - $elapsed);
$this->log("本次耗時: {$elapsed}s, 休眠: {$sleepTime}s");
// 分段睡眠,定期檢查退出信號
$this->interruptibleSleep($sleepTime);
}
$this->releaseLock();
$this->log("任務正常結束");
}
protected function executeTask()
{
// 子類實現具體業務邏輯
}
private function acquireLock()
{
$fp = fopen($this->lockFile, 'w');
if (!flock($fp, LOCK_EX | LOCK_NB)) {
return false;
}
fwrite($fp, getmypid());
return true;
}
private function releaseLock()
{
@unlink($this->lockFile);
}
private function interruptibleSleep($seconds)
{
$end = time() + $seconds;
while (time() < $end && $this->running) {
sleep(min(10, $end - time())); // 每 10 秒檢查一次信號
}
}
public function signalHandler($signo)
{
$this->log("收到信號 $signo,準備退出...");
$this->running = false;
}
private function log($msg)
{
$line = date('Y-m-d H:i:s') . " [$msg]\n";
file_put_contents($this->logFile, $line, FILE_APPEND);
error_log($line); // 同時寫入 PHP 錯誤日誌
}
}
// 使用:繼承實現具體任務
class BlogUpdateTask extends CronTask
{
protected function executeTask()
{
// 實際業務邏輯
$db = new PDO('mysql:host=localhost;dbname=blog', 'user', 'pass');
$db->exec("UPDATE posts SET view_count = view_count + 1 WHERE featured = 1");
}
}
// 執行
$task = new BlogUpdateTask(3600, 'blog_update'); // 每小時
$task->run();
| 方式 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| Web 觸發 + ignore_user_abort | 無需 CLI 權限、共享 Web 環境 | 佔用 FPM 進程、不穩定、難以管理 | 簡單任務、無 CLI 環境 |
| Crontab + CLI 腳本 | 系統級調度、穩定、標準化 | 最小粒度 1 分鐘、無內建防重複 | 大多數定時任務 |
| Systemd Timer | 秒級精度、依賴管理、日誌整合 | 需 root 權限配置 | 生產環境推薦 |
| Supervisor + 長運行進程 | 進程管理、自動重啟、日誌輪轉 | 需編寫常駐腳本 | 高頻/長任務 |
| 消息隊列 Worker | 解耦、可擴展、失敗重試 | 架構複雜度高 | 異步任務、高併發 |
| Swoole Timer / PHP 8.1+ Fiber | 進程內毫秒級、高效能 | 需擴展/新版本 | 高頻即時任務 |
# /etc/crontab 或 crontab -e
# 每天凌晨 2 點執行
0 2 * * * www-data /usr/bin/php /var/www/html/cron/daily.php >> /var/log/cron/daily.log 2>&1
# 每小時執行(需防重複鎖)
0 * * * * www-data /usr/bin/php /var/www/html/cron/hourly.php
# 每 5 分鐘檢查隊列
*/5 * * * * www-data /usr/bin/php /var/www/html/cron/queue_worker.php
# 開機自啟長運行進程
@reboot www-data /usr/bin/php /var/www/html/cron/daemon.php
#!/usr/bin/php
<?php
$lockFile = '/tmp/my_cron.lock';
$fp = fopen($lockFile, 'w');
if (!flock($fp, LOCK_EX | LOCK_NB)) {
exit("已有實例運行\n");
}
// 業務邏輯...
flock($fp, LOCK_UN);
生產環境強烈建議使用 **Crontab + CLI 腳本 + 鎖機制 + 日誌監控**,或 **Systemd Timer**,避免 Web 觸發方式的不可靠性。