什么是任务调度
在传统方式下,你需要为每个定时任务手写 cron 条目。
这样做的问题是调度定义脱离源码,无法进行版本管理,每次调整都要 SSH 登录服务器。
Laravel 的调度器允许你在应用内部以流畅的语法定义调度。
只需要在服务器上加一行 cron 条目,其他调度定义都随代码一起进入版本管理。
标准写法是把调度定义写在 routes/console.php 里。
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;
Schedule::call(function () {
DB::table('recent_users')->delete();
})->daily();
使用 schedule:list Artisan 命令可以查看已定义任务的列表以及下一次执行时间。php artisan schedule:list
调度器的执行流程
定义调度
调度定义写在 routes/console.php。
也可以在 bootstrap/app.php 的 withSchedule 方法中定义。
// bootstrap/app.php
use Illuminate\Console\Scheduling\Schedule;
->withSchedule(function (Schedule $schedule) {
$schedule->call(new DeleteRecentUsers)->daily();
})
可调度的任务类型
调度 Artisan 命令
使用 command 方法调度 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::command('delete:recent-users', function () {
DB::table('recent_users')->delete();
})->purpose('Delete recent users')->daily();
调度队列任务
使用 job 方法调度队列任务。
无需闭包即可让任务入队,非常方便。
use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;
Schedule::job(new Heartbeat)->everyFiveMinutes();
也可以指定队列名与连接。
// 派发到 "heartbeats" 队列,使用 "sqs" 连接
Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();
调度 shell 命令
exec 方法用来执行操作系统命令。
use Illuminate\Support\Facades\Schedule;
Schedule::exec('node /home/forge/script.js')->daily();
调度闭包
call 方法可以调度任意 PHP 闭包。
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;
Schedule::call(function () {
DB::table('recent_users')->delete();
})->daily();
也可以传入带 __invoke 方法的 Invocable 对象。
Schedule::call(new DeleteRecentUsers)->daily();
设置执行频率
常用频率方法
以下是常用的频率方法。
| 方法 | 说明 |
|---|
->everySecond() | 每秒执行 |
->everyMinute() | 每分钟执行 |
->everyFiveMinutes() | 每 5 分钟执行 |
->everyFifteenMinutes() | 每 15 分钟执行 |
->everyThirtyMinutes() | 每 30 分钟执行 |
->hourly() | 每小时执行 |
->hourlyAt(17) | 每小时的第 17 分钟执行 |
->daily() | 每天 0 点执行 |
->dailyAt('13:00') | 每天 13 点执行 |
->twiceDaily(1, 13) | 每天 1 点和 13 点执行 |
->weekly() | 每周日 0 点执行 |
->weeklyOn(1, '8:00') | 每周一 8 点执行 |
->monthly() | 每月 1 日 0 点执行 |
->monthlyOn(4, '15:00') | 每月 4 日 15 点执行 |
->quarterly() | 每季度首日 0 点执行 |
->yearly() | 每年 1 月 1 日 0 点执行 |
->timezone('Asia/Shanghai') | 指定时区 |
直接使用 cron 表达式
也可以通过 cron 方法直接使用 cron 表达式。
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->cron('0 9 * * *'); // 每天 9 点执行
频率与星期组合
可以把频率方法与星期约束组合出更精细的调度。
use Illuminate\Support\Facades\Schedule;
// 每周一 13 点执行
Schedule::call(function () {
// ...
})->weekly()->mondays()->at('13:00');
// 工作日 8 点到 17 点每小时执行
Schedule::command('foo')
->weekdays()
->hourly()
->timezone('Asia/Shanghai')
->between('8:00', '17:00');
设置时区
timezone 方法可以为单个任务指定时区。
use Illuminate\Support\Facades\Schedule;
Schedule::command('report:generate')
->timezone('Asia/Shanghai')
->at('9:00');
想为所有任务设置公共时区,可以配置 config/app.php 的 schedule_timezone。
// config/app.php
'schedule_timezone' => 'Asia/Shanghai',
如果时区包含夏令时,切换时任务可能被执行两次或完全不执行。
尽量使用 UTC。
条件约束
when / skip
when 只在闭包返回 true 时执行任务。
skip 相反,返回 true 时跳过。
use Illuminate\Support\Facades\Schedule;
// 条件为 true 时执行
Schedule::command('emails:send')->daily()->when(function () {
return true;
});
// 条件为 true 时跳过
Schedule::command('emails:send')->daily()->skip(function () {
return true;
});
environments
environments 方法限制任务只在特定环境执行。
Schedule::command('emails:send')
->daily()
->environments(['staging', 'production']);
时间段约束
between / unlessBetween 可以限制执行的时间段。
// 只在 7 点到 22 点之间执行
Schedule::command('emails:send')
->hourly()
->between('7:00', '22:00');
// 23 点到 4 点之间不执行
Schedule::command('emails:send')
->hourly()
->unlessBetween('23:00', '4:00');
星期约束
| 方法 | 说明 |
|---|
->weekdays() | 仅工作日 |
->weekends() | 仅周末 |
->mondays() ~ ->sundays() | 特定星期 |
->days([0, 3]) | 同时指定周日(0)和周三(3)等多天 |
use Illuminate\Support\Facades;
use Illuminate\Console\Scheduling\Schedule;
Facades\Schedule::command('emails:send')
->hourly()
->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);
防止重叠
默认情况下,即便上一次任务尚未结束,下一次执行也会照常开始。
使用 withoutOverlapping 会等上一次执行完再开始下一次。
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->withoutOverlapping();
可以指定锁的过期时间(分钟),默认 24 小时。
// 10 分钟后锁自动过期
Schedule::command('emails:send')->withoutOverlapping(10);
withoutOverlapping 使用应用缓存来管理锁。
如果任务因异常卡死,可以用 schedule:clear-cache 清除锁。php artisan schedule:clear-cache
多服务器执行控制
多台服务器同时运行调度器时,onOneServer 可以让任务只在其中一台上执行。
use Illuminate\Support\Facades\Schedule;
Schedule::command('report:generate')
->fridays()
->at('17:00')
->onOneServer();
使用此功能需要将应用默认缓存驱动设为 database、memcached、dynamodb 或 redis,且所有服务器连接到同一个缓存服务。
onOneServer() 的分布式执行流程
任务分组
如果多个任务共用同样的设置,可以用 group 方法一次配置。
use Illuminate\Support\Facades\Schedule;
Schedule::daily()
->onOneServer()
->timezone('Asia/Shanghai')
->group(function () {
Schedule::command('emails:send --force');
Schedule::command('emails:prune');
});
后台执行
同一时刻的多个任务默认按定义顺序串行执行。
长任务会拖慢后续任务。
runInBackground 会让任务在后台并行执行。
use Illuminate\Support\Facades\Schedule;
Schedule::command('analytics:report')
->daily()
->runInBackground();
runInBackground 只能用于 command 和 exec 方法。
维护模式
应用进入维护模式时,调度任务默认不会执行。
若要强制运行,可使用 evenInMaintenanceMode。
Schedule::command('emails:send')->evenInMaintenanceMode();
维护模式下的处理流程
暂停调度器
无需改代码即可暂停调度器。
# 暂停
php artisan schedule:pause
# 恢复
php artisan schedule:continue
即使暂停,也可以让部分任务继续运行,通过 evenWhenPaused。
适用于健康检查、监控等在维护期仍需运行的任务。
Schedule::command('emails:send')->evenWhenPaused();
处理输出
输出到文件
sendOutputTo 可以把任务输出写入文件。
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->daily()
->sendOutputTo(storage_path('logs/emails-send.log'));
appendOutputTo 则会追加到已有文件末尾。
Schedule::command('emails:send')
->daily()
->appendOutputTo(storage_path('logs/emails-send.log'));
输出到邮件
emailOutputTo 可以把任务输出以邮件发送。
需要事先完成 Laravel 的邮件配置。
Schedule::command('report:generate')
->daily()
->sendOutputTo($filePath)
->emailOutputTo('[email protected]');
只在失败时发送邮件,使用 emailOutputOnFailure。
Schedule::command('report:generate')
->daily()
->emailOutputOnFailure('[email protected]');
任务钩子
before / after 用于在任务前后插入逻辑。
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->daily()
->before(function () {
// 执行前
})
->after(function () {
// 执行后
});
onSuccess / onFailure 用于成功/失败时的钩子。
use Illuminate\Support\Stringable;
Schedule::command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// 成功时处理
})
->onFailure(function (Stringable $output) {
// 失败时处理
});
服务器部署
添加 cron 条目
只需在服务器 crontab 中加入下面一行,Laravel 调度器就会每分钟运行。* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
使用 crontab -e 命令编辑。 确认调度器运行情况
查看已定义任务及下一次执行时间。php artisan schedule:list
本地开发运行
本地开发时可以不使用 cron,而是通过 schedule:work 命令让调度器常驻。
php artisan schedule:work
该命令在前台运行,每分钟触发一次调度,直到你 Ctrl+C 停止。
亚分钟级调度(间隔小于 1 分钟)
常规 cron 的最小单位是分钟,但 Laravel 支持秒级调度。
use Illuminate\Support\Facades\Schedule;
Schedule::call(function () {
DB::table('recent_users')->delete();
})->everySecond();
如果定义了亚分钟任务,schedule:run 会持续运行到本分钟结束,处理所有亚分钟任务。
建议把亚分钟任务委派给队列任务或后台命令。
任务本身耗时较长时,会延迟后续亚分钟任务的执行。
若要在部署时中断正在运行的 schedule:run,可以在部署脚本中加入:
php artisan schedule:interrupt
常用命令速查
# 显示任务列表
php artisan schedule:list
# 手动执行调度器(cron 会调用的命令)
php artisan schedule:run
# 本地开发用常驻运行
php artisan schedule:work
# 暂停调度器
php artisan schedule:pause
# 恢复调度器
php artisan schedule:continue
# 清理重叠锁
php artisan schedule:clear-cache
# 中断亚分钟任务的执行(部署时使用)
php artisan schedule:interrupt