原文:Task Scheduling。作者归属:Laravel 文档贡献者;页面版权归 Laravel。本文依据 Laravel 13.x 官方文档完整章节译写,适用前提为 Laravel 13、PHP 8.3+。示例仅做静态审核,未运行。
如果为每个任务各写一条 Cron,调度规则就容易散落在服务器上:查看或修改任务需要登录机器,规则也未必处于应用的版本控制中。Laravel 的调度器把任务定义放回应用,一般集中在 routes/console.php,服务器只保留一条每分钟调用调度器的 Cron。
调度器负责判断何时触发任务。闭包、Artisan 命令、队列任务和系统命令仍然各有自己的执行语义;特别是队列任务被派发之后,还需要相应的队列消费者实际处理。

在应用中定义计划
最直接的入口是 Schedule::call,它接收闭包,也可以接收实现 __invoke 的可调用对象。原文的入门闭包在每天午夜执行 DB::table('recent_users')->delete(),这会删除整张表的所有行;原文还有同样的每秒删除示例。它们说明的是调度 API,不能直接复制成生产清理策略。
编者改写:下例保留“每日清理”的调度方式,要求删除范围同时满足“业务已批准”和“已到过期时间”。cleanup_approved 与 expires_at 是为说明条件而假设的字段,必须由实际业务模型、保留政策和数据库结构确认后替换;这不是无需审查即可执行的迁移或清理脚本。
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;
Schedule::call(function () {
DB::table('recent_users')
->where('cleanup_approved', true)
->where('expires_at', '<', now())
->delete();
})->daily();
使用可调用对象时,调度表达式可以保持很短;删除条件与业务验证应封装在对象内部:
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();
})
查看已定义任务及其下一次计划运行时间,可以使用:
php artisan schedule:list
调度 Artisan、闭包命令、队列和系统命令
command 支持命令名称,也支持命令类名。使用类名时,第二个参数数组可传入额外的命令行参数:
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 命令可以直接在命令定义后链式添加调度方法。原文的 delete:recent-users 也是无条件整表删除;以下按前面相同的编者假设加入业务条件:
Artisan::command('delete:recent-users', function () {
DB::table('recent_users')
->where('cleanup_approved', true)
->where('expires_at', '<', now())
->delete();
})->purpose('Delete approved, expired recent users')->daily();
闭包命令有参数时,交给 schedule:
Artisan::command('emails:send {user} {--force}', function ($user) {
// 此处实现邮件发送业务。
})->purpose('Send emails to the specified user')
->schedule(['Taylor', '--force'])
->daily();
队列任务用 job;它避免了为了派发任务而再写一层闭包。第二、第三个参数分别是队列名称与连接名称:
use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;
Schedule::job(new Heartbeat)->everyFiveMinutes();
Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();
系统命令则用 exec:
Schedule::exec('node /home/forge/script.js')->daily();
编者说明:应用命令、类、队列连接和脚本路径都必须真实存在。不要把请求输入直接拼入 exec 或命令字符串;示例中的 --force 也是具体业务命令的选项,不应视为所有命令都安全的通用开关。
频率与日期约束
频率方法可以和星期、时间区间、环境等条件组合。下表保留原文列出的频率入口:
| 方法 | 含义 |
|---|---|
cron('* * * * *') |
自定义 Cron 表达式。 |
everySecond()、everyTwoSeconds()、everyFiveSeconds() |
分别每 1、2、5 秒。 |
everyTenSeconds()、everyFifteenSeconds()、everyTwentySeconds()、everyThirtySeconds() |
分别每 10、15、20、30 秒。 |
everyMinute()、everyTwoMinutes()、everyThreeMinutes()、everyFourMinutes()、everyFiveMinutes() |
分别每 1、2、3、4、5 分钟。 |
everyTenMinutes()、everyFifteenMinutes()、everyThirtyMinutes() |
分别每 10、15、30 分钟。 |
hourly()、hourlyAt(17) |
每小时;或每小时的第 17 分钟。 |
everyOddHour($minutes = 0) |
每个奇数小时,可指定分钟。 |
everyTwoHours($minutes = 0)、everyThreeHours($minutes = 0)、everyFourHours($minutes = 0)、everySixHours($minutes = 0) |
每 2、3、4、6 小时,可指定分钟。 |
daily()、dailyAt('13:00') |
每日午夜;或每日 13:00。 |
twiceDaily(1, 13)、twiceDailyAt(1, 13, 15) |
每日 01:00 和 13:00;或 01:15 和 13:15。 |
daysOfMonth([1, 10, 20]) |
每月指定日期。 |
weekly()、weeklyOn(1, '8:00') |
每周日 00:00;或每周一 08:00。 |
monthly()、monthlyOn(4, '15:00') |
每月1日 00:00;或每月4日 15:00。 |
twiceMonthly(1, 16, '13:00')、lastDayOfMonth('15:00') |
每月1日和16日 13:00;或月末 15:00。 |
quarterly()、quarterlyOn(4, '14:00') |
每季度首日 00:00;或每季度的第4日 14:00。 |
yearly()、yearlyOn(6, 1, '17:00') |
每年首日 00:00;或每年6月1日 17:00。 |
timezone('America/New_York') |
指定这项计划所使用的时区。 |
例如,每周一 13:00 运行闭包,或在芝加哥时区的工作日 08:00–17:00 每小时运行一个命令:
Schedule::call(function () {
// 每周一次的业务。
})->weekly()->mondays()->at('13:00');
Schedule::command('foo')
->weekdays()
->hourly()
->timezone('America/Chicago')
->between('8:00', '17:00');
星期条件包括 weekdays()、weekends(),以及从 sundays() 到 saturdays() 的七个方法。days 可以用数字指定星期,0 代表周日;也可以使用调度类的常量:
Schedule::command('emails:send')->hourly()->days([0, 3]);
// 等价的常量写法:
use Illuminate\Support\Facades;
use Illuminate\Console\Scheduling\Schedule;
Facades\Schedule::command('emails:send')
->hourly()
->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);
between 只允许任务在给定时间段内执行,unlessBetween 排除给定时间段,后者也可以跨午夜:
Schedule::command('emails:send')->hourly()->between('7:00', '22:00');
Schedule::command('emails:send')->hourly()->unlessBetween('23:00', '4:00');
when 接收返回布尔值的闭包:只有条件为真,且其他约束也允许时才运行。链式添加多个 when 时,必须全部为真。skip 则相反,返回真就跳过:
Schedule::command('emails:send')->daily()->when(function () {
return true;
});
Schedule::command('emails:send')->daily()->skip(function () {
return true;
});
按环境过滤使用 environments,它根据 APP_ENV 决定当前环境:
Schedule::command('emails:send')
->daily()
->environments(['staging', 'production']);
时区与夏令时
用 timezone 指定某项计划的时间解释方式:
Schedule::command('report:generate')
->timezone('America/New_York')
->at('2:00');
如果所有计划反复使用同一个时区,可以在应用的 app 配置文件中设置:
'timezone' => 'UTC',
'schedule_timezone' => 'America/Chicago',
夏令时切换可能导致某项任务执行两次,或者完全不执行。原文因此建议尽可能避免基于此类时区安排任务。编者说明:涉及账期、结算、通知的业务还需要明确重复与漏触发的处理方式,调度时区本身不是业务幂等机制。
防止前一次尚未结束就再次进入
默认情况下,到时间就会再触发,即使前一实例仍在运行。withoutOverlapping 用应用缓存中的锁避免重叠:
Schedule::command('emails:send')->withoutOverlapping();
Schedule::command('emails:send')->withoutOverlapping(10);
未另设频率时,示例每分钟检查一次;已有实例运行中就不再进入。锁默认 24 小时过期,也可像第二行那样设置为 10 分钟。锁超时时间必须结合最长执行时间选择:设得太短可能让前一实例未结束时再次进入,设得太长则会延长异常残留锁的影响。
意外服务器故障可能留下锁。必要时可用 php artisan schedule:clear-cache 清理,但应先确认没有仍在执行的实例;这是一项改变调度状态的操作。
多服务器只触发一次
如果三台服务器都运行调度器,同一个周五报表可能生成三次。onOneServer 让率先获得原子锁的服务器运行该次任务:
Schedule::command('report:generate')
->fridays()
->at('17:00')
->onOneServer();
Schedule::useCache('database');
使用此功能,默认缓存驱动须为 database、memcached、dynamodb 或 redis,所有服务器必须连接同一个集中式缓存。useCache 可以选择调度器获得单服务器原子锁时所用的缓存存储。机器各用一份本地独立缓存,不能形成这类共同锁。
同一个任务用不同参数多次调度时,每一组定义都应有独立名称,确保每种参数组合分别只在一台服务器上运行:
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();
最后一个例子说明:计划闭包若要单服务器运行,也必须命名。编者说明:onOneServer 解决多机同一时点竞争,withoutOverlapping 解决任务运行时长跨过下一次触发的问题;两者不是互相替代的同一开关,更不等于队列消费或业务写入永远只发生一次。
后台执行、维护模式、暂停和分组
同一时刻到期的任务默认按定义顺序执行,长任务可能拖慢后面的任务。可以让命令在后台运行:
Schedule::command('analytics:report')->daily()->runInBackground();
runInBackground 只适用于 command 和 exec。应用处于维护模式时,计划任务默认不执行;确实需要继续的任务可显式指定:
Schedule::command('emails:send')->evenInMaintenanceMode();
Laravel 13 文档还提供不修改部署代码即可暂停任务处理的命令:
php artisan schedule:pause
php artisan schedule:continue
暂停期间默认不运行计划任务;schedule:continue 恢复处理。必须在暂停期间仍执行的任务,可标记 evenWhenPaused()。这与维护模式是两个需要分别理解的状态:
Schedule::command('emails:send')->evenWhenPaused();
多个任务有相同的频率、时区或单服务器要求时,先指定公共条件,再用 group 定义组内任务:
Schedule::daily()
->onOneServer()
->timezone('America/New_York')
->group(function () {
Schedule::command('emails:send --force');
Schedule::command('emails:prune');
});
运行调度器:服务器只需一条 Cron
schedule:run 根据服务器当前时间评估所有已定义任务。在服务器配置每分钟运行一次即可:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
路径、PHP 可执行文件与执行身份需要按部署环境确认,并具备应用、缓存和日志所需权限。原例把标准输出和标准错误都重定向到 /dev/null;这会隐藏该入口的输出,实际部署应结合后文的任务输出与失败监控设计。Laravel Cloud 可以代管计划任务执行,但它是可选托管平台。
秒级任务与部署中断
大多数操作系统的 Cron 最短间隔是每分钟,Laravel 却可以在这一分钟内继续调度,最短到每秒一次。定义了小于一分钟的任务后,schedule:run 不会立即退出,而是持续到当前分钟结束。
原文的 everySecond() 闭包含整表删除;前述风险同样适用。耗时超出预期的秒级任务会推迟后续任务,所以原文建议把实际工作交给队列或后台命令:
use App\Jobs\DeleteRecentUsers;
Schedule::job(new DeleteRecentUsers)->everyTenSeconds();
Schedule::command('users:delete')->everyTenSeconds()->runInBackground();
这两行假定业务已经正确实现了删除范围,不代表命令本身具有安全的默认条件。队列消费者容量不足时,缩短派发间隔还可能造成积压。
秒级调度进程存活到分钟结束,也意味着部署完成后,已启动的那一轮可能继续使用旧代码。原文要求在部署完成之后调用:
php artisan schedule:interrupt
此命令用于中断正在进行的 schedule:run 调用,让后续调度使用新版本。它不应被误解成所有后台进程或已派发队列任务都已终止的证明。
本地开发通常不必修改系统 Cron。可以前台运行以下命令,它每分钟调用一次调度器,直到手动结束;存在秒级任务时,也会在每一分钟内持续处理:
php artisan schedule:work
保存输出和发送失败通知
sendOutputTo 把输出写入文件,appendOutputTo 追加写入。emailOutputTo 将输出发送到指定邮箱,使用之前必须配置 Laravel 邮件服务。emailOutputOnFailure 只在命令以非零退出码结束时发邮件:
Schedule::command('emails:send')->daily()->sendOutputTo($filePath);
Schedule::command('emails:send')->daily()->appendOutputTo($filePath);
Schedule::command('report:generate')
->daily()
->sendOutputTo($filePath)
->emailOutputTo('ops@example.com');
Schedule::command('report:generate')
->daily()
->emailOutputOnFailure('ops@example.com');
这四种输出方法只适用于 command 和 exec。本文将原网页受邮箱保护处理的地址替换为明确的示例地址 ops@example.com。命令输出可能包含个人数据、令牌或其他敏感信息,配置文件路径、保留周期与收件人时应审查实际输出;这里没有发送任何通知。
任务前后钩子、结果钩子与外部 Ping
before 和 after 分别在任务执行前后调用闭包;onSuccess 与 onFailure 按成功或失败调用,失败同样以非零退出码为依据:
Schedule::command('emails:send')
->daily()
->before(function () {
// 任务即将执行。
})
->after(function () {
// 任务已执行。
});
Schedule::command('emails:send')
->daily()
->onSuccess(function () {
// 命令成功。
})
->onFailure(function () {
// 命令失败。
});
命令有可用输出时,在 after、onSuccess 或 onFailure 回调中为参数标注 Illuminate\Support\Stringable,就可以取得输出:
use Illuminate\Support\Stringable;
Schedule::command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// 检查成功输出;入日志前按业务需要脱敏。
})
->onFailure(function (Stringable $output) {
// 检查失败输出。
});
pingBefore、thenPing 可在任务开始前或结束后访问外部 URL,例如通知 Envoyer 这类外部服务;按成功或失败分别通知,则使用 pingOnSuccess 和 pingOnFailure:
Schedule::command('emails:send')
->daily()
->pingBefore($url)
->thenPing($url);
Schedule::command('emails:send')
->daily()
->pingOnSuccess($successUrl)
->pingOnFailure($failureUrl);
带 If 的四个方法增加一个布尔条件,只在条件为真时发起对应请求:
Schedule::command('emails:send')
->daily()
->pingBeforeIf($condition, $url)
->thenPingIf($condition, $url);
Schedule::command('emails:send')
->daily()
->pingOnSuccessIf($condition, $successUrl)
->pingOnFailureIf($condition, $failureUrl);
编者说明:这些 URL 会产生真实网络请求,可能带有监控密钥,不应直接接收不可信输入或写入公开日志。失败钩子说明调度命令的退出状态;队列业务是否最终成功,还应由队列和业务层各自确认。
通过事件接入统一观察
调度过程还会分发事件,可以通过监听器统一记录启动、完成、跳过与失败。原文列出的事件都在 Illuminate\Console\Events 命名空间:
ScheduledTaskStartingScheduledTaskFinishedScheduledBackgroundTaskFinishedScheduledTaskSkippedScheduledTaskFailed
本文完整保留原文的调度入口、频率表、条件、锁、多服务器、暂停、分组、秒级部署、输出、钩子和事件各章;编者改动是为整表删除加入明确假设的业务条件,并增加运行边界说明。本文没有运行 Artisan、Cron、邮件、网络 Ping 或删除任务,静态审查也不构成对实际业务代码的安全保证。
来源:Laravel 13.x 官方文档,Laravel 文档贡献者;© Laravel。原创示意图归本文编者。












暂无评论内容