Laravel-Schedule 计划任务「原理了解」
Laravel-Schedule 原理了解
文章概述
Laravel-Schedule 流程分为两个步骤:
- 第一步,根据配置的 Command 命令、Cron 表达式进行注册事件;
- 第二步,操作系统配置每分钟触发
Laravel-Schedule,由Laravel-Schedule自主完成事件是否符合执行时间过滤,重复性检查,并可选Background或者Foreground进行执行任务。
事件注册
首先,在命令行应用程序入口文件 artisan 引入 bootstrap/app.php
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
然后,向容器中注册Laravel-Kernel,并使用make构建实例
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
App\Console\Kernel 继承于 Illuminate\Foundation\Console\Kernel
所以在实例过程中会调用Illuminate\Foundation\Console\Kernel构造方法:
public function __construct(Application $app, Dispatcher $events)
{
if (! defined('ARTISAN_BINARY')) {
define('ARTISAN_BINARY', 'artisan');
}
$this->app = $app;
$this->events = $events;
$this->app->booted(function () {
$this->defineConsoleSchedule();
});
}
这里又完成了一次事件注册,在应用启动booted完成后回调 $this->defineConsoleSchedule()
protected function defineConsoleSchedule()
{
$this->app->singleton(Schedule::class, function ($app) {
return new Schedule;
});
$schedule = $this->app->make(Schedule::class);
$this->schedule($schedule);
}
重点在于defineConsoleSchedule这个方法,容器中注册并实例化Schedule对象,并使用址传递对Schedule实例进行操作,这里的操作就是计划任务的事件注册。
Illuminate\Foundation\Console\Kernel 中的schedule方法
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
//
}
当然,我们不需要在这里修改任何代码。上面,我们说过Laravel-Kernel对象的实例类是App\Console\Kernel,他继承了Illuminate\Foundation\Console\Kernel类。
所以我们在官方文档中也可以清楚看到,计划任务的配置是在App\Console\Kernel中的schedule方法中定义的,例如:
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('inspire')->hourly();
}
我们来看看官方文档的解读:
Closure 定义调度
使用Closure定义调度。例如,每天使用DB构造器方式来清空数据库一个表
$schedule->call(function () {
DB::table('recent_users')->delete();
})->daily();
Artisan 命令调度
除了计划 Closure 调用,你还能调度 Artisan 命令 和操作系统命令。举个例子,你可以给 command 方法传递命令名称或者类名称来调度一个 Artisan 命令:
$schedule->command('emaiLaravel-Schedule:send --force')->daily();
队列任务调度
job方法可以用来调度 队列任务。这个方法提供了一种快捷方式来调度任务,无需使用call方法手动创建闭包来调度任务:
$schedule->job(new Heartbeat)->everyFiveMinutes();
Shell 命令调度
exec 方法可用于向操作系统发出命令:
$schedule->exec('node /home/forge/script.js')->daily();
Laravel-Schedule 提供很多提高我们开发效率的执行频率方法
| 方法 | 描述 |
|---|---|
| ->cron(' '); | 在自定义的 Cron 时间表上执行该任务 |
| ->everyMinute(); | 每分钟执行一次任务 |
| ->everyFiveMinutes(); | 每五分钟执行一次任务 |
| ->everyTenMinutes(); | 每十分钟执行一次任务 |
| ->everyFifteenMinutes(); | 每十五分钟执行一次任务 |
| ->everyThirtyMinutes(); | 每半小时执行一次任务 |
| ->hourly(); | 每小时执行一次任务 |
| ->hourlyAt(17); | 每小时的第 17 分钟执行一次任务 |
| ->daily(); | 每天午夜执行一次任务 |
| ->dailyAt('13:00'); | 每天的 13:00 执行一次任务 |
| ->twiceDaily(1, 13); | 每天的 1:00 和 13:00 分别执行一次任务 |
| ->weekly(); | 每周执行一次任务 |
| ->monthly(); | 每月执行一次任务 |
| ->monthlyOn(4, '15:00'); | 在每个月的第四天的 15:00 执行一次任务 |
| ->quarterly(); | 每季度执行一次任务 |
| ->yearly(); | 每年执行一次任务 |
| ->timezone('America/New_York'); | 设置时区 |
了解Laravel-Schedule给我们提供的多种任务定义和执行频率设置的方式后,我们回来思考其实现的原理是怎么样的?
schedule方法里的每一句任务的定义,就是构造一个事件对象,并将这个事件对象放到集合数组里
Illuminate\Console\Scheduling\Schedule.php command方法的核心实现代码如下:
$this->events[] = $event = new Event($this->mutex, $command);
mutex 这个变量用来控制事件当前时间执行的不可重复性
schedule方法里的每一句调度频率设置,就是表达式的构建
这个表达式 expression 就是与我们常用 crontab 表达式是同样的类型,everyTenMinutes() 每十分钟执行一次,其实对应的表达式就是 */10 * * * * *,具体 Laravel-Schedule 实现代码如下,应该不难看懂。
public $expression = '* * * * * *';
public function everyTenMinutes()
{
return $this->spliceIntoPosition(1, '*/10');
}
protected function spliceIntoPosition($position, $value)
{
$segments = explode(' ', $this->expression);
$segments[$position - 1] = $value;
return $this->cron(implode(' ', $segments));
}
这个很重要,因为事件的过滤中,需要匹配执行时间是否等于当前时间。
运行事件
启动调度器,使用调度器时,只需将以下Cron项目添加到服务器:
* * * * * php /path-to-your-project/artisan schedule:run >> /dev/null 2>&1
上面这个Cron会每分钟调用一次Laravel-Schedule命令调度器。执行schedule:run命令时, Laravel-Schedule会根据你的调度运行预定任务。
让我们带着疑问继续理解Laravel-Schedule运行事件原理。
schedule:run 是什么?
我们看 Illuminate\Console\Scheduling\ScheduleRunCommand 代码是怎么写的?和普通自定义Artisan命令一样,继承 Command 基类。然后具体任务内容在handle方法里实现。
class ScheduleRunCommand extends Command
{
//...
public function handle()
{
foreach ($this->schedule->dueEvents($this->laravel) as $event) {
// ...
$event->run($this->laravel);
}
// ...
}
}
dueEvents 完成过滤动作 collect($this->events)->filter->isDue($app) 使用 isDue 方法进行过滤。
public function isDue($app)
{
if (! $this->runsInMaintenanceMode() && $app->isDownForMaintenance()) {
return faLaravel-Schedulee;
}
return $this->expressionPasses() &&
$this->runsInEnvironment($app->environment());
}
protected function expressionPasses()
{
$date = Carbon::now();
if ($this->timezone) {
$date->setTimezone($this->timezone);
}
return CronExpression::factory($this->expression)->isDue($date->toDateTimeString());
}
其实原理很简单,方法 expressionPasses 通过 Carbon 第三方扩展包获取当前时间,并与Event实例的 Expression 进行匹对
return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime
如果返回True,那就表示Event需要执行
$event->run($this->laravel);
public function run(Container $container)
{
if ($this->withoutOverlapping &&
! $this->mutex->create($this)) {
return;
}
$this->runInBackground
? $this->runCommandInBackground($container)
: $this->runCommandInForeground($container);
}
withoutOverlapping 和 mutex 就是在这里控制任务重复执行
(new Process(
$this->buildCommand(), base_path(), null, null, null
))->run();
最后,由执行器执行命令任务...done
几点疑问?
1.假设每个五分钟执行,比如08:52定义命令调度 Command 到 Schedule ,会在08:57时刻执行?
不会,只会在08:55时刻执行,也就是满足时钟的固定周期。
2.任务调度的两种执行方式 runCommandInBackground 与 runCommandInForeground 有什么区别?
runCommandInBackground 代码如下:
protected function runCommandInBackground(Container $container)
{
$this->callBeforeCallbacks($container);
(new Process(
$this->buildCommand(), base_path(), null, null, null
))->run();
}
runCommandInForeground 代码如下:
protected function runCommandInForeground(Container $container)
{
$this->callBeforeCallbacks($container);
(new Process(
$this->buildCommand(), base_path(), null, null, null
))->run();
$this->callAfterCallbacks($container);
}
差别在于 $this->callAfterCallbacks($container) ,是否等待当前任务执行完成,如果选择 runCommandInBackground 方式运行,任务命令直接传递给操作系统进行执行,然后直接返回,等待操作系统执行完成任务后,会执行另一条命令 schedule:finish 通过事件ID进行异步响应对应的任务事件。
3.Closure 定义调度,和命令其他方式定义调度是不相同的,详细可以查看CallBackEvent->run() 同步方式执行
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
推荐文章: