跳轉至內容

任務排程

簡介

過去,你可能需要為伺服器上計劃的每個任務編寫一個 cron 配置條目。然而,這很快就會變得很麻煩,因為你的任務排程不再受版本控制,而且你必須透過 SSH 連線到伺服器才能檢視現有的 cron 條目或新增新條目。

Laravel 的命令排程程式為管理伺服器上的定時任務提供了一種全新的方法。排程程式允許你在 Laravel 應用程式本身中流暢且富有表現力地定義命令排程。使用排程程式時,伺服器上只需要一個 cron 條目。你的任務排程通常定義在應用程式的 routes/console.php 檔案中。

定義排程

你可以在應用程式的 routes/console.php 檔案中定義所有定時任務。首先,讓我們看一個例子。在這個例子中,我們將排程一個閉包,使其每天午夜執行。在閉包內部,我們將執行資料庫查詢來清空一張表。

1<?php
2 
3use Illuminate\Support\Facades\DB;
4use Illuminate\Support\Facades\Schedule;
5 
6Schedule::call(function () {
7 DB::table('recent_users')->delete();
8})->daily();

除了使用閉包進行排程外,你還可以排程 可呼叫物件 (invokable objects)。可呼叫物件是包含 __invoke 方法的簡單 PHP 類。

1Schedule::call(new DeleteRecentUsers)->daily();

如果你更喜歡將 routes/console.php 檔案僅用於命令定義,你可以使用應用程式 bootstrap/app.php 檔案中的 withSchedule 方法來定義定時任務。此方法接收一個包含排程程式例項的閉包。

1use Illuminate\Console\Scheduling\Schedule;
2 
3->withSchedule(function (Schedule $schedule) {
4 $schedule->call(new DeleteRecentUsers)->daily();
5})

如果你想檢視定時任務的概覽以及它們下一次計劃執行的時間,可以使用 schedule:list Artisan 命令。

1php artisan schedule:list

排程 Artisan 命令

除了排程閉包,你還可以排程 Artisan 命令 和系統命令。例如,你可以使用 command 方法透過命令名稱或類名來排程 Artisan 命令。

當使用命令類名排程 Artisan 命令時,你可以傳遞一個額外的命令列引數陣列,這些引數將在呼叫命令時提供給它。

1use App\Console\Commands\SendEmailsCommand;
2use Illuminate\Support\Facades\Schedule;
3 
4Schedule::command('emails:send Taylor --force')->daily();
5 
6Schedule::command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();

排程 Artisan 閉包命令

如果你想排程由閉包定義的 Artisan 命令,你可以在命令定義之後鏈式呼叫與排程相關的方法。

1Artisan::command('delete:recent-users', function () {
2 DB::table('recent_users')->delete();
3})->purpose('Delete recent users')->daily();

如果你需要將引數傳遞給閉包命令,可以將它們提供給 schedule 方法。

1Artisan::command('emails:send {user} {--force}', function ($user) {
2 // ...
3})->purpose('Send emails to the specified user')->schedule(['Taylor', '--force'])->daily();

排程佇列任務

job 方法可用於排程 佇列任務。該方法提供了一種便捷的方式來排程佇列任務,而無需使用 call 方法來定義用於入隊的閉包。

1use App\Jobs\Heartbeat;
2use Illuminate\Support\Facades\Schedule;
3 
4Schedule::job(new Heartbeat)->everyFiveMinutes();

可以為 job 方法提供可選的第二和第三引數,以指定應使用哪個佇列名稱和佇列連線來處理該任務。

1use App\Jobs\Heartbeat;
2use Illuminate\Support\Facades\Schedule;
3 
4// Dispatch the job to the "heartbeats" queue on the "sqs" connection...
5Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();

排程 Shell 命令

exec 方法可用於向作業系統傳送命令。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::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(); 每月 1 日 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'); 設定任務的時區。

這些方法可以與附加約束組合使用,建立更精細的排程,使其僅在每週的特定日期執行。例如,你可以將命令排程為每週一執行。

1use Illuminate\Support\Facades\Schedule;
2 
3// Run once per week on Monday at 1 PM...
4Schedule::call(function () {
5 // ...
6})->weekly()->mondays()->at('13:00');
7 
8// Run hourly from 8 AM to 5 PM on weekdays...
9Schedule::command('foo')
10 ->weekdays()
11 ->hourly()
12 ->timezone('America/Chicago')
13 ->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 方法可用於將任務執行限制在一週內的特定日期。例如,你可以將命令排程為每週日和週三的小時任務。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('emails:send')
4 ->hourly()
5 ->days([0, 3]);

或者,在定義任務執行的日期時,你可以使用 Illuminate\Console\Scheduling\Schedule 類中提供的常量。

1use Illuminate\Support\Facades;
2use Illuminate\Console\Scheduling\Schedule;
3 
4Facades\Schedule::command('emails:send')
5 ->hourly()
6 ->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);

時間範圍約束

between 方法可用於根據一天中的時間限制任務執行。

1Schedule::command('emails:send')
2 ->hourly()
3 ->between('7:00', '22:00');

同樣,unlessBetween 方法可用於在一段時間內排除任務的執行。

1Schedule::command('emails:send')
2 ->hourly()
3 ->unlessBetween('23:00', '4:00');

真值測試約束

when 方法可用於基於給定真值測試的結果限制任務執行。換句話說,如果給定的閉包返回 true,只要沒有其他限制條件阻止任務執行,任務就會執行。

1Schedule::command('emails:send')->daily()->when(function () {
2 return true;
3});

skip 方法可以看作是 when 的反面。如果 skip 方法返回 true,則定時任務將不會執行。

1Schedule::command('emails:send')->daily()->skip(function () {
2 return true;
3});

當使用鏈式 when 方法時,排程命令僅在所有 when 條件都返回 true 時執行。

環境約束

environments 方法可用於僅在給定環境(由 APP_ENV 環境變數 定義)中執行任務。

1Schedule::command('emails:send')
2 ->daily()
3 ->environments(['staging', 'production']);

時區

使用 timezone 方法,你可以指定定時任務的時間應在給定的時區內解析。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('report:generate')
4 ->timezone('America/New_York')
5 ->at('2:00')

如果你反覆為所有定時任務分配相同的時區,可以透過在應用程式的 app 配置檔案中定義 schedule_timezone 選項,來指定應分配給所有排程的時區。

1'timezone' => 'UTC',
2 
3'schedule_timezone' => 'America/Chicago',

請記住,一些時區使用夏令時。當夏令時發生變化時,你的定時任務可能會執行兩次,甚至根本不執行。因此,我們建議儘可能避免進行時區排程。

防止任務重疊

預設情況下,即使任務的上一個例項仍在執行,定時任務也會執行。為了防止這種情況,你可以使用 withoutOverlapping 方法。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('emails:send')->withoutOverlapping();

在這個例子中,emails:send Artisan 命令 如果當前未執行,則每分鐘執行一次。如果你的任務執行時間波動很大,導致無法準確預測特定任務需要多長時間,withoutOverlapping 方法尤其有用。

如果需要,你可以指定“無重疊”鎖過期前必須經過多少分鐘。預設情況下,該鎖將在 24 小時後過期。

1Schedule::command('emails:send')->withoutOverlapping(10);

在底層,withoutOverlapping 方法利用應用程式的 快取 來獲取鎖。如有必要,你可以使用 schedule:clear-cache Artisan 命令清除這些快取鎖。這通常僅在任務因意外的伺服器問題而卡住時才需要。

在單臺伺服器上執行任務

要使用此功能,你的應用程式必須使用 databasememcacheddynamodbredis 快取驅動程式作為應用程式的預設快取驅動程式。此外,所有伺服器都必須與同一個中央快取伺服器進行通訊。

如果你的應用程式排程程式執行在多臺伺服器上,你可以將定時任務限制為僅在單臺伺服器上執行。例如,假設你有一個每週五晚上生成新報告的定時任務。如果任務排程程式執行在三臺工作伺服器上,定時任務將在所有三臺伺服器上執行並生成三次報告。這不好!

要表明任務應僅在一臺伺服器上執行,請在定義定時任務時使用 onOneServer 方法。第一臺獲取任務的伺服器將對該任務進行原子鎖定,以防止其他伺服器同時執行相同的任務。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('report:generate')
4 ->fridays()
5 ->at('17:00')
6 ->onOneServer();

你可以使用 useCache 方法自定義排程程式使用的快取儲存,以獲取單伺服器任務所需的原子鎖。

1Schedule::useCache('database');

命名單伺服器任務

有時你可能需要排程同一個任務以不同的引數進行分發,同時仍要求 Laravel 在單臺伺服器上執行該任務的每個變體。為此,你可以透過 name 方法為每個排程定義分配一個唯一名稱。

1Schedule::job(new CheckUptime('https://laravel.com.tw'))
2 ->name('check_uptime:laravel.com')
3 ->everyFiveMinutes()
4 ->onOneServer();
5 
6Schedule::job(new CheckUptime('https://vapor.laravel.com'))
7 ->name('check_uptime:vapor.laravel.com')
8 ->everyFiveMinutes()
9 ->onOneServer();

同樣,如果預定閉包旨在在單臺伺服器上執行,則必須為其分配名稱。

1Schedule::call(fn () => User::resetApiRequestCount())
2 ->name('reset-api-request-count')
3 ->daily()
4 ->onOneServer();

後臺任務

預設情況下,同時排程的多個任務將根據它們在 schedule 方法中定義的順序按順序執行。如果你有耗時較長的任務,這可能會導致後續任務比預期的開始時間晚得多。如果你希望在後臺執行任務,以便它們可以同時執行,則可以使用 runInBackground 方法。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('analytics:report')
4 ->daily()
5 ->runInBackground();

runInBackground 方法僅在透過 commandexec 方法排程任務時才能使用。

維護模式

當應用程式處於 維護模式 時,應用程式的定時任務將不會執行,因為我們不希望任務干擾你在伺服器上可能執行的任何未完成的維護工作。然而,如果你想強制任務即使在維護模式下也執行,你可以在定義任務時呼叫 evenInMaintenanceMode 方法。

1Schedule::command('emails:send')->evenInMaintenanceMode();

暫停排程任務

你可以使用 schedule:pause Artisan 命令暫時暫停定時任務處理,而無需更改已部署的程式碼。

1php artisan schedule:pause

當排程程式暫停時,定時任務不會執行。你可以使用 schedule:continue 命令恢復定時任務處理。

1php artisan schedule:continue

如果某個任務在排程程式暫停時仍需執行,你可以使用 evenWhenPaused 方法對其進行標記。

1Schedule::command('emails:send')->evenWhenPaused();

排程分組

當定義多個具有相似配置的定時任務時,你可以使用 Laravel 的任務分組功能,以避免為每個任務重複設定相同的配置。分組任務可以簡化程式碼並確保相關任務的一致性。

要建立一組定時任務,請呼叫所需的任務配置方法,然後呼叫 group 方法。group 方法接受一個閉包,該閉包負責定義共享指定配置的任務。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::daily()
4 ->onOneServer()
5 ->timezone('America/New_York')
6 ->group(function () {
7 Schedule::command('emails:send --force');
8 Schedule::command('emails:prune');
9 });

執行排程程式

現在我們已經瞭解瞭如何定義定時任務,讓我們討論如何實際在伺服器上執行它們。schedule:run Artisan 命令將評估你所有的定時任務,並根據伺服器的當前時間確定它們是否需要執行。

因此,使用 Laravel 排程程式時,我們只需要在伺服器上新增一個每分鐘執行 schedule:run 命令的 cron 配置條目。如果你不知道如何向伺服器新增 cron 條目,可以考慮使用像 Laravel Cloud 這樣的託管平臺,它可以為你管理定時任務的執行。

1* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

秒級排程任務

在大多數作業系統上,cron 任務最多隻能每分鐘執行一次。然而,Laravel 的排程程式允許你將任務排程為更頻繁的時間間隔,甚至最快每秒一次。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::call(function () {
4 DB::table('recent_users')->delete();
5})->everySecond();

當在應用程式中定義了秒級任務時,schedule:run 命令將一直執行到當前分鐘結束,而不是立即退出。這使得該命令能夠在這一分鐘內隨時呼叫所有所需的秒級任務。

由於耗時過長的秒級任務可能會延遲後續任務的執行,因此建議所有秒級任務都分發佇列任務或後臺命令來處理實際的任務邏輯。

1use App\Jobs\DeleteRecentUsers;
2 
3Schedule::job(new DeleteRecentUsers)->everyTenSeconds();
4 
5Schedule::command('users:delete')->everyTenSeconds()->runInBackground();

中斷秒級任務

由於定義了秒級任務時 schedule:run 命令會在一整分鐘內執行,有時在部署應用程式時可能需要中斷該命令。否則,已經執行的 schedule:run 命令例項將繼續使用你應用程式之前部署的程式碼,直到當前分鐘結束。

要中斷正在進行的 schedule:run 呼叫,你可以將 schedule:interrupt 命令新增到應用程式的部署指令碼中。此命令應在應用程式部署完成後呼叫。

1php artisan schedule:interrupt

在本地執行排程程式

通常,你不會在本地開發機器上新增排程程式 cron 條目。相反,你可以使用 schedule:work Artisan 命令。此命令將在前臺執行,並每分鐘呼叫一次排程程式,直到你終止該命令。當定義了秒級任務時,排程程式將繼續在每分鐘內執行以處理這些任務。

1php artisan schedule:work

任務輸出

Laravel 排程程式提供了幾種便捷的方法來處理定時任務生成的輸出。首先,使用 sendOutputTo 方法,你可以將輸出傳送到檔案以供日後檢查。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('emails:send')
4 ->daily()
5 ->sendOutputTo($filePath);

如果你想將輸出追加到指定檔案,可以使用 appendOutputTo 方法。

1Schedule::command('emails:send')
2 ->daily()
3 ->appendOutputTo($filePath);

使用 emailOutputTo 方法,你可以將輸出透過電子郵件傳送到你選擇的地址。在透過電子郵件傳送任務輸出之前,你應該配置 Laravel 的 電子郵件服務

1Schedule::command('report:generate')
2 ->daily()
3 ->sendOutputTo($filePath)
4 ->emailOutputTo('[email protected]');

如果你只想在定時 Artisan 或系統命令以非零退出程式碼終止時傳送輸出郵件,請使用 emailOutputOnFailure 方法。

1Schedule::command('report:generate')
2 ->daily()
3 ->emailOutputOnFailure('[email protected]');

emailOutputToemailOutputOnFailuresendOutputToappendOutputTo 方法僅適用於 commandexec 方法。

任務鉤子

使用 beforeafter 方法,你可以指定在定時任務執行前後執行的程式碼。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('emails:send')
4 ->daily()
5 ->before(function () {
6 // The task is about to execute...
7 })
8 ->after(function () {
9 // The task has executed...
10 });

onSuccessonFailure 方法允許你指定定時任務成功或失敗時執行的程式碼。失敗表示定時 Artisan 或系統命令以非零退出程式碼終止。

1Schedule::command('emails:send')
2 ->daily()
3 ->onSuccess(function () {
4 // The task succeeded...
5 })
6 ->onFailure(function () {
7 // The task failed...
8 });

如果你的命令有可用輸出,你可以在 afteronSuccessonFailure 鉤子中透過將 Illuminate\Support\Stringable 例項型別提示為鉤子閉包定義的 $output 引數來訪問它。

1use Illuminate\Support\Stringable;
2 
3Schedule::command('emails:send')
4 ->daily()
5 ->onSuccess(function (Stringable $output) {
6 // The task succeeded...
7 })
8 ->onFailure(function (Stringable $output) {
9 // The task failed...
10 });

Ping URL

使用 pingBeforethenPing 方法,排程程式可以在任務執行前或執行後自動 ping 給定的 URL。此方法對於通知外部服務(如 Envoyer)你的定時任務已經開始或已完成執行非常有用。

1Schedule::command('emails:send')
2 ->daily()
3 ->pingBefore($url)
4 ->thenPing($url);

pingOnSuccesspingOnFailure 方法可用於僅在任務成功或失敗時 ping 給定的 URL。失敗表示定時 Artisan 或系統命令以非零退出程式碼終止。

1Schedule::command('emails:send')
2 ->daily()
3 ->pingOnSuccess($successUrl)
4 ->pingOnFailure($failureUrl);

pingBeforeIfthenPingIfpingOnSuccessIfpingOnFailureIf 方法可用於僅在給定條件為 true 時 ping 給定的 URL。

1Schedule::command('emails:send')
2 ->daily()
3 ->pingBeforeIf($condition, $url)
4 ->thenPingIf($condition, $url);
5 
6Schedule::command('emails:send')
7 ->daily()
8 ->pingOnSuccessIf($condition, $successUrl)
9 ->pingOnFailureIf($condition, $failureUrl);

活動

Laravel 在排程過程中會分發各種 事件。你可以為以下任何事件 定義監聽器

事件名稱
Illuminate\Console\Events\ScheduledTaskStarting
Illuminate\Console\Events\ScheduledTaskFinished
Illuminate\Console\Events\ScheduledBackgroundTaskFinished
Illuminate\Console\Events\ScheduledTaskSkipped
Illuminate\Console\Events\ScheduledTaskFailed