任務排程
簡介
過去,你可能需要在伺服器上為每個需要排程的任務撰寫一個 cron 設定項目。然而,這很快就會變得麻煩,因為你的任務排程不再在原始碼控制中,而且你必須 SSH 進入伺服器才能檢視現有的 cron 項目或新增額外的項目。
Laravel 的指令排程器提供了一種全新的方式來管理伺服器上的排程任務。排程器讓你可以在 Laravel 應用程式本身中流暢且富有表達性地定義指令排程。使用排程器時,伺服器上只需要一個 cron 項目。你的任務排程通常定義在應用程式的 routes/console.php 檔案中。
定義排程
你可以在應用程式的 routes/console.php 檔案中定義所有的排程任務。首先,讓我們看一個範例。在這個範例中,我們將排程一個閉包在每天午夜被呼叫。在閉包中,我們會執行一個資料庫查詢來清除資料表:
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;
Schedule::call(function () {
DB::table('recent_users')->delete();
})->daily();
除了使用閉包排程之外,你還可以排程可呼叫物件。可呼叫物件是包含 __invoke 方法的簡單 PHP 類別:
Schedule::call(new DeleteRecentUsers)->daily();
如果你希望將 routes/console.php 檔案保留給指令定義,你可以在應用程式的 bootstrap/app.php 檔案中使用 withSchedule 方法來定義排程任務。此方法接受一個閉包,該閉包接收排程器的實例:
use Illuminate\Console\Scheduling\Schedule;
->withSchedule(function (Schedule $schedule) {
$schedule->call(new DeleteRecentUsers)->daily();
})
如果你想檢視排程任務的概覽以及它們下次排程執行的時間,你可以使用 schedule:list Artisan 指令:
php artisan schedule:list
排程 Artisan 指令
除了排程閉包之外,你還可以排程 Artisan 指令 和系統指令。例如,你可以使用 command 方法,使用指令的名稱或類別來排程 Artisan 指令。
當使用指令的類別名稱排程 Artisan 指令時,你可以傳遞一個額外的命令列參數陣列,這些參數應該在呼叫指令時提供:
use App\Console\Commands\SendEmailsCommand;
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send Taylor --force')->daily();
Schedule::command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();
排程 Artisan 閉包指令
如果你想排程由閉包定義的 Artisan 指令,你可以在指令定義之後鏈接排程相關的方法:
Artisan::command('delete:recent-users', function () {
DB::table('recent_users')->delete();
})->purpose('Delete recent users')->daily();
如果你需要將參數傳遞給閉包指令,你可以將它們提供給 schedule 方法:
Artisan::command('emails:send {user} {--force}', function ($user) {
// ...
})->purpose('Send emails to the specified user')->schedule(['Taylor', '--force'])->daily();
排程佇列任務
job 方法可用於排程佇列任務。此方法提供了一種方便的方式來排程佇列任務,而無需使用 call 方法來定義用於將任務加入佇列的閉包:
use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;
Schedule::job(new Heartbeat)->everyFiveMinutes();
可選的第二個和第三個參數可以提供給 job 方法,指定應該用於將任務加入佇列的佇列名稱和佇列連線:
use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;
// 將任務調度到 "sqs" 連線上的 "heartbeats" 佇列...
Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();
排程 Shell 指令
exec 方法可用於向作業系統發出指令:
use Illuminate\Support\Facades\Schedule;
Schedule::exec('node /home/forge/script.js')->daily();
排程頻率選項
我們已經看到了幾個如何設定任務以指定間隔執行的範例。然而,還有更多你可以指派給任務的任務排程頻率:
| 方法 | 描述 |
|---|---|
->cron('* * * * *'); | 使用自訂 cron 排程執行任務。 |
->everySecond(); | 每秒執行任務。 |
->everyTwoSeconds(); | 每兩秒執行任務。 |
->everyFiveSeconds(); | 每五秒執行任務。 |
->everyTenSeconds(); | 每十秒執行任務。 |
->everyFifteenSeconds(); | 每十五秒執行任務。 |
->everyTwentySeconds(); | 每二十秒執行任務。 |
->everyThirtySeconds(); | 每三十秒執行任務。 |
->everyMinute(); | 每分鐘執行任務。 |
->everyTwoMinutes(); | 每兩分鐘執行任務。 |
->everyThreeMinutes(); | 每三分鐘執行任務。 |
->everyFourMinutes(); | 每四分鐘執行任務。 |
->everyFiveMinutes(); | 每五分鐘執行任務。 |
->everyTenMinutes(); | 每十分鐘執行任務。 |
->everyFifteenMinutes(); | 每十五分鐘執行任務。 |
->everyThirtyMinutes(); | 每三十分鐘執行任務。 |
->hourly(); | 每小時執行任務。 |
->hourlyAt(17); | 每小時的第 17 分鐘執行任務。 |
->everyOddHour($minutes = 0); | 每奇數小時執行任務。 |
->everyTwoHours($minutes = 0); | 每兩小時執行任務。 |
->everyThreeHours($minutes = 0); | 每三小時執行任務。 |
->everyFourHours($minutes = 0); | 每四小時執行任務。 |
->everySixHours($minutes = 0); | 每六小時執行任務。 |
->daily(); | 每天午夜執行任務。 |
->dailyAt('13:00'); | 每天 13:00 執行任務。 |
->twiceDaily(1, 13); | 每天 1:00 和 13:00 執行任務。 |
->twiceDailyAt(1, 13, 15); | 每天 1:15 和 13:15 執行任務。 |
->daysOfMonth([1, 10, 20]); | 在每月的特定日期執行任務。 |
->weekly(); | 每週日 00:00 執行任務。 |
->weeklyOn(1, '8:00'); | 每週一 8:00 執行任務。 |
->monthly(); | 每月第一天 00:00 執行任務。 |
->monthlyOn(4, '15:00'); | 每月 4 日 15:00 執行任務。 |
->twiceMonthly(1, 16, '13:00'); | 每月 1 日和 16 日 13:00 執行任務。 |
->lastDayOfMonth('15:00'); | 每月最後一天 15:00 執行任務。 |
->quarterly(); | 每季第一天 00:00 執行任務。 |
->quarterlyOn(4, '14:00'); | 每季 4 日 14:00 執行任務。 |
->yearly(); | 每年第一天 00:00 執行任務。 |
->yearlyOn(6, 1, '17:00'); | 每年 6 月 1 日 17:00 執行任務。 |
->timezone('America/New_York'); | 為任務設定時區。 |
這些方法可以與額外的約束條件結合,建立只在特定星期幾執行的更精細排程。例如,你可以排程一個指令每週一執行:
use Illuminate\Support\Facades\Schedule;
// 每週一下午 1 點執行一次...
Schedule::call(function () {
// ...
})->weekly()->mondays()->at('13:00');
// 平日早上 8 點到下午 5 點之間每小時執行...
Schedule::command('foo')
->weekdays()
->hourly()
->timezone('America/Chicago')
->between('8:00', '17:00');
以下是額外排程約束的列表:
| 方法 | 描述 |
|---|---|
->weekdays(); | 將任務限制在平日。 |
->weekends(); | 將任務限制在週末。 |
->sundays(); | 將任務限制在週日。 |
->mondays(); | 將任務限制在週一。 |
->tuesdays(); | 將任務限制在週二。 |
->wednesdays(); | 將任務限制在週三。 |
->thursdays(); | 將任務限制在週四。 |
->fridays(); | 將任務限制在週五。 |
->saturdays(); | 將任務限制在週六。 |
->days(array\|mixed); | 將任務限制在特定的日子。 |
->between($startTime, $endTime); | 將任務限制在開始和結束時間之間執行。 |
->unlessBetween($startTime, $endTime); | 將任務限制在開始和結束時間之間不執行。 |
->when(Closure); | 根據真假測試限制任務。 |
->environments($env); | 將任務限制在特定環境。 |
日期約束
days 方法可用於將任務的執行限制在特定的星期幾。例如,你可以排程一個指令每週日和週三每小時執行:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->hourly()
->days([0, 3]);
或者,你可以使用 Illuminate\Console\Scheduling\Schedule 類別上可用的常數來定義任務應該執行的日期:
use Illuminate\Support\Facades;
use Illuminate\Console\Scheduling\Schedule;
Facades\Schedule::command('emails:send')
->hourly()
->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);
時間區間約束
between 方法可用於根據一天中的時間來限制任務的執行:
Schedule::command('emails:send')
->hourly()
->between('7:00', '22:00');
類似地,unlessBetween 方法可用於在一段時間內排除任務的執行:
Schedule::command('emails:send')
->hourly()
->unlessBetween('23:00', '4:00');
真假測試約束
when 方法可用於根據給定真假測試的結果來限制任務的執行。換句話說,如果給定的閉包回傳 true,只要沒有其他約束條件阻止任務執行,任務就會執行:
Schedule::command('emails:send')->daily()->when(function () {
return true;
});
skip 方法可以視為 when 的反向。如果 skip 方法回傳 true,排程任務將不會執行:
Schedule::command('emails:send')->daily()->skip(function () {
return true;
});
當使用鏈接的 when 方法時,只有當所有 when 條件都回傳 true 時,排程指令才會執行。
環境約束
environments 方法可用於只在給定的環境中執行任務(由 APP_ENV 環境變數 定義):
Schedule::command('emails:send')
->daily()
->environments(['staging', 'production']);
時區
使用 timezone 方法,你可以指定排程任務的時間應該在給定的時區中解釋:
use Illuminate\Support\Facades\Schedule;
Schedule::command('report:generate')
->timezone('America/New_York')
->at('2:00')
如果你重複地將相同的時區指派給所有排程任務,你可以透過在應用程式的 app 設定檔中定義 schedule_timezone 選項來指定應該指派給所有排程的時區:
'timezone' => 'UTC',
'schedule_timezone' => 'America/Chicago',
[!WARNING] 請記住,某些時區會使用日光節約時間。當日光節約時間變化發生時,你的排程任務可能會執行兩次,或者甚至完全不執行。因此,我們建議盡可能避免時區排程。
防止任務重疊
預設情況下,即使任務的前一個實例仍在執行,排程任務也會執行。要防止這種情況,你可以使用 withoutOverlapping 方法:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->withoutOverlapping();
在這個範例中,如果 emails:send Artisan 指令 尚未執行,它將每分鐘執行一次。如果你有執行時間差異很大的任務,withoutOverlapping 方法特別有用,讓你無法準確預測給定任務需要多長時間。
如果需要,你可以指定「無重疊」鎖定過期之前必須經過的分鐘數。預設情況下,鎖定會在 24 小時後過期:
Schedule::command('emails:send')->withoutOverlapping(10);
在底層,withoutOverlapping 方法利用你應用程式的快取來取得鎖定。如果需要,你可以使用 schedule:clear-cache Artisan 指令清除這些快取鎖定。這通常只在任務因意外的伺服器問題而卡住時才需要。
在單一伺服器上執行任務
[!WARNING] 要使用這個功能,你的應用程式必須使用
database、memcached、dynamodb或redis快取驅動程式作為應用程式的預設快取驅動程式。此外,所有伺服器都必須與同一個中央快取伺服器通訊。
如果你的應用程式的排程器在多個伺服器上執行,你可以將排程任務限制為只在單一伺服器上執行。例如,假設你有一個排程任務每週五晚上產生新報告。如果任務排程器在三個工作伺服器上執行,排程任務會在所有三個伺服器上執行並產生報告三次。不好!
要指示任務應該只在一個伺服器上執行,請在定義排程任務時使用 onOneServer 方法。第一個取得任務的伺服器會對任務加上原子鎖定,以防止其他伺服器同時執行相同的任務:
use Illuminate\Support\Facades\Schedule;
Schedule::command('report:generate')
->fridays()
->at('17:00')
->onOneServer();
你可以使用 useCache 方法來自訂排程器用於取得單一伺服器任務所需的原子鎖定的快取儲存庫:
Schedule::useCache('database');
命名單一伺服器任務
有時你可能需要排程相同的任務以不同的參數調度,同時仍指示 Laravel 在單一伺服器上執行每個任務的排列組合。要完成此操作,你可以透過 name 方法為每個排程定義指派一個唯一的名稱:
Schedule::job(new CheckUptime('https://laravel.com'))
->name('check_uptime:laravel.com')
->everyFiveMinutes()
->onOneServer();
Schedule::job(new CheckUptime('https://vapor.laravel.com'))
->name('check_uptime:vapor.laravel.com')
->everyFiveMinutes()
->onOneServer();
類似地,如果排程閉包旨在在一個伺服器上執行,則必須為其指派一個名稱:
Schedule::call(fn () => User::resetApiRequestCount())
->name('reset-api-request-count')
->daily()
->onOneServer();
背景任務
預設情況下,同時排程的多個任務會根據它們在 schedule 方法中定義的順序依序執行。如果你有長時間執行的任務,這可能會導致後續任務比預期晚很多才開始。如果你想在背景中執行任務,以便它們可以同時執行,你可以使用 runInBackground 方法:
use Illuminate\Support\Facades\Schedule;
Schedule::command('analytics:report')
->daily()
->runInBackground();
[!WARNING] >
runInBackground方法只能在使用command和exec方法排程任務時使用。
維護模式
當應用程式處於維護模式時,應用程式的排程任務不會執行,因為我們不希望任務干擾你可能正在伺服器上執行的任何未完成維護。但是,如果你想強制任務即使在維護模式下也要執行,你可以在定義任務時呼叫 evenInMaintenanceMode 方法:
Schedule::command('emails:send')->evenInMaintenanceMode();
排程群組
當定義多個具有類似設定的排程任務時,你可以使用 Laravel 的任務群組功能來避免為每個任務重複相同的設定。群組任務可以簡化你的程式碼並確保相關任務之間的一致性。
要建立一組排程任務,請呼叫所需的任務設定方法,然後呼叫 group 方法。group 方法接受一個閉包,該閉包負責定義共用指定設定的任務:
use Illuminate\Support\Facades\Schedule;
Schedule::daily()
->onOneServer()
->timezone('America/New_York')
->group(function () {
Schedule::command('emails:send --force');
Schedule::command('emails:prune');
});
執行排程器
現在我們已經學會了如何定義排程任務,讓我們討論如何在伺服器上實際執行它們。schedule:run Artisan 指令會評估所有的排程任務,並根據伺服器的當前時間決定是否需要執行它們。
因此,當使用 Laravel 的排程器時,我們只需要在伺服器上新增一個 cron 設定項目,每分鐘執行 schedule:run 指令。如果你不知道如何在伺服器上新增 cron 項目,請考慮使用像 Laravel Cloud 這樣的託管平台,它可以為你管理排程任務執行:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
次分鐘排程任務
在大多數作業系統上,cron 任務被限制為最多每分鐘執行一次。然而,Laravel 的排程器讓你可以排程以更頻繁的間隔執行任務,甚至每秒執行一次:
use Illuminate\Support\Facades\Schedule;
Schedule::call(function () {
DB::table('recent_users')->delete();
})->everySecond();
當在應用程式中定義次分鐘任務時,schedule:run 指令會持續執行直到當前分鐘結束,而不是立即退出。這讓指令可以在整個分鐘內呼叫所有需要的次分鐘任務。
由於執行時間超過預期的次分鐘任務可能會延遲後續次分鐘任務的執行,建議所有次分鐘任務都調度佇列任務或背景指令來處理實際的任務處理:
use App\Jobs\DeleteRecentUsers;
Schedule::job(new DeleteRecentUsers)->everyTenSeconds();
Schedule::command('users:delete')->everyTenSeconds()->runInBackground();
中斷次分鐘任務
由於當定義次分鐘任務時 schedule:run 指令會執行整個呼叫的分鐘,你有時可能需要在部署應用程式時中斷該指令。否則,已經在執行的 schedule:run 指令實例會繼續使用應用程式先前部署的程式碼,直到當前分鐘結束。
要中斷進行中的 schedule:run 呼叫,你可以將 schedule:interrupt 指令加入到應用程式的部署腳本中。此指令應該在應用程式完成部署後呼叫:
php artisan schedule:interrupt
本機執行排程器
通常,你不會在本機開發機器上新增排程器 cron 項目。相反,你可以使用 schedule:work Artisan 指令。此指令會在前台執行,每分鐘呼叫排程器,直到你終止該指令。當定義次分鐘任務時,排程器會在每分鐘內持續執行以處理這些任務:
php artisan schedule:work
任務輸出
Laravel 排程器提供了幾個方便的方法來處理排程任務產生的輸出。首先,使用 sendOutputTo 方法,你可以將輸出發送到檔案以供稍後檢查:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->daily()
->sendOutputTo($filePath);
如果你想將輸出附加到給定的檔案,你可以使用 appendOutputTo 方法:
Schedule::command('emails:send')
->daily()
->appendOutputTo($filePath);
使用 emailOutputTo 方法,你可以將輸出透過電子郵件發送到你選擇的電子郵件地址。在透過電子郵件發送任務輸出之前,你應該設定 Laravel 的電子郵件服務:
Schedule::command('report:generate')
->daily()
->sendOutputTo($filePath)
->emailOutputTo('taylor@example.com');
如果你只想在排程的 Artisan 或系統指令以非零退出碼終止時才透過電子郵件發送輸出,請使用 emailOutputOnFailure 方法:
Schedule::command('report:generate')
->daily()
->emailOutputOnFailure('taylor@example.com');
[!WARNING] >
emailOutputTo、emailOutputOnFailure、sendOutputTo和appendOutputTo方法僅限於command和exec方法。
任務鉤子
使用 before 和 after 方法,你可以指定在排程任務執行之前和之後要執行的程式碼:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->daily()
->before(function () {
// 任務即將執行...
})
->after(function () {
// 任務已執行...
});
onSuccess 和 onFailure 方法讓你可以指定在排程任務成功或失敗時要執行的程式碼。失敗表示排程的 Artisan 或系統指令以非零退出碼終止:
Schedule::command('emails:send')
->daily()
->onSuccess(function () {
// 任務成功...
})
->onFailure(function () {
// 任務失敗...
});
如果指令有輸出可用,你可以透過在鉤子閉包定義中將 Illuminate\Support\Stringable 實例型別提示為 $output 參數來存取它:
use Illuminate\Support\Stringable;
Schedule::command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// 任務成功...
})
->onFailure(function (Stringable $output) {
// 任務失敗...
});
Ping URL
使用 pingBefore 和 thenPing 方法,排程器可以在任務執行之前或之後自動 ping 給定的 URL。此方法對於通知外部服務(例如 Envoyer)排程任務正在開始或已完成執行非常有用:
Schedule::command('emails:send')
->daily()
->pingBefore($url)
->thenPing($url);
pingOnSuccess 和 pingOnFailure 方法可用於只在任務成功或失敗時 ping 給定的 URL。失敗表示排程的 Artisan 或系統指令以非零退出碼終止:
Schedule::command('emails:send')
->daily()
->pingOnSuccess($successUrl)
->pingOnFailure($failureUrl);
pingBeforeIf、thenPingIf、pingOnSuccessIf 和 pingOnFailureIf 方法可用於只在給定條件為 true 時 ping 給定的 URL:
Schedule::command('emails:send')
->daily()
->pingBeforeIf($condition, $url)
->thenPingIf($condition, $url);
Schedule::command('emails:send')
->daily()
->pingOnSuccessIf($condition, $successUrl)
->pingOnFailureIf($condition, $failureUrl);
事件
Laravel 在排程過程中會調度各種事件。你可以為以下任何事件定義監聽器:
| 事件名稱 |
|---|
Illuminate\Console\Events\ScheduledTaskStarting |
Illuminate\Console\Events\ScheduledTaskFinished |
Illuminate\Console\Events\ScheduledBackgroundTaskFinished |
Illuminate\Console\Events\ScheduledTaskSkipped |
Illuminate\Console\Events\ScheduledTaskFailed |