标题:【Laravel系列7.6】任务调度
任务调度
任务调度是什么一个概念呢?其实就是可以自动定时去运行大家需要后端运行的脚本,比如说我们最早讲过的 Laravel 中的命令行脚本。本身这些脚本如果需要定时执行的话,我们都需要使用 crontab 来操作。crontab 就是一个任务调度的工具,但它是操作系统上层面的,或者说是操作系统上的一个工具。如果有多个脚本需要执行,那么我们就要写多个定时脚本的配置。而 Laravel 中其实也已经提供了这样的操作,我们可以使用一个命令行来控制所有的命令行脚本。
使用任务调度
在 Laravel 中进行任务调度控制非常简单,只需要在 app/Console/Kernel.php 的 schedule() 方法中进行定义就可以了。
protected function schedule(Schedule $schedule)
{
$schedule->command('ZyBlog:Test1 2 --b=3')->appendOutputTo("./zyblog_test1.log");
$schedule->command(test1::class, ['3', '--b=4'])->appendOutputTo("./zyblog_test1.log");
$schedule->exec("ls -al")->sendOutputTo('./exec_command1.log');
$schedule->call(function(){
echo "callable schedule command";
})->sendOutputTo('./call_command.log');
$schedule->call(new ZyBlog())->sendOutputTo('./call_command.log');
}
在这里我们使用了四种方式来调用不同的操作脚本。appendOutputTo() 是向一个日志中追加内容,sendOutputTo() 则是将返回的数据内容发送到指定的日志文件。它们两个的作用就像是 >> 和 > 重定向。大家可以在运行测试之后分别查看每个日志文件中的内容就可以看出效果了。appendOutputTo() 在底层调用的依然是 sendOutputTo() 这个方法,只是设定了 sendOutputTo() 的第二个参数为 true ,这个参数的意思就是是否追加。
command() 从方法名称就可以看出,这是调用 Laravel 下面的命令行脚本的意思,在这里我们就是调用之前很早的时候我们写过的那个命令行脚本。同时我们使用了两种方式来调用,一种是直接全部字符串,另一种是类名加参数数组的形式。
exec() 可以直接执行系统中的命令,当前运行 PHP 的用户需要有相应的命令权限才可以。在这里我们就是测试的列出当前目录中的所有信息。
call() 函数也是有两种调用方式,一是直接放入一个回调函数,二是传入一个类。回调函数比较好理解,直接就会运行这个回调函数里面的内容。而传入一个类的话,就需要这个类实现 __invoke() 这个魔术方法。还记得它是干什么的吗?用户让一个类对象可以像方法一样调用。
在这里需要注意的是,call() 函数方法不会有输出,也就是说,它们不会输出到日志中,call_command.log 日志文件不会创建。
接下来,我们就可以使用一个命令行来测试上面的所有命令的调用。
# php artisan schedule:run
# Running scheduled command: '/usr/local/Cellar/php/7.3.9_1/bin/php' 'artisan' ZyBlog:Test1 2 --b=3 >> './zyblog_test1.log' 2>&1
# Running scheduled command: '/usr/local/Cellar/php/7.3.9_1/bin/php' 'artisan' ZyBlog:Test1 3 --b=4 >> './zyblog_test1.log' 2>&1
# Running scheduled command: ls -al > './exec_command1.log' 2>&1
# Running scheduled command: Callback
# callable schedule commandRunning scheduled command: Callback
# App\ContainerTest\ZyBlogRunning scheduled command: Callback
看到命令运行的情况和生成的命令调用语句了吧。怎么样,是不是通过执行一个命令就完成了对上面 5 个命令的调用。这就是任务调度的作用。如果你想让这些命令都自动执行的话,只需要将 php artisan schedule:run 挂到 crontab 上就可以了。
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
设定时间
在 crontab 中,最经典的就是可以设置自动运行的时间。而在上面的代码中,我们都是直接运行的,也就是只要运行 schedule:run ,那么里面的这些任务都会执行。其实,Laravel 工具也是可以在这里定义调度时间的,和 crontab 中的设定方式非常像,甚至更简单。
$schedule->call(function(){
echo "Three Minutes:", date('Y-m-d H:i:s');
})->everyThreeMinutes();
上面这个 everyThreeMinutes() 代表的就是每三分钟执行一次。我们也可以直接使用 crontab 的方式来定义。
$schedule->call(function(){
echo "Cron Three Minutes:", date('Y-m-d H:i:s');
})->cron("*/3 * * * *");
这个 cron() 方法中,可以使用与 crontab 相同的方式来定义运行的时间。大家在运行测试的时候我们可以多运行几次,也可以间隔三分钟再运行,或者直接将 schedule:run 挂到 crontab 上进行测试。最后,我们再来看一个复杂一点的定时配置。
$schedule->call(function(){
echo "周二 每三分钟 9点到10点 Three Minutes:", date('Y-m-d H:i:s');
})->tuesdays()->everyThreeMinutes()->between('9:00', '10:00');
上面这段配置的意思就是每周二从9点到10点,间隔三分钟运行一次。通过这段代码也可以看出,所有的时间设定函数都是可以链式调用的。而且相关的配置函数也非常多,大家可以自己查阅官方文档进行配置测试。
避免重复运行任务
在日常的业务开发中,我们可以会遇到一种情况,那就是每分钟或者间隔比较短地去执行一个任务时,上一次的这个任务还没有跑完。这种情况如果不处理的话,过不了多久时间,就会跑起来很多任务,占用很多内存,最后导致系统崩掉。这可不是开玩笑,是真实的经历。在很早期的时候,我们需要在运行脚本前先去判断系统的进程中是否有已经跑起来的任务,需要使用 system() 之类的函数去执行 ps -ef | grep xxx
之类的操作获得返回的进程ID,而现在使用 Laravel 的话,则方便很多,因为 Laravel 的任务调度功能已经考虑到了这个问题。
$schedule->command(test2::class)->runInBackground()->withoutOverlapping();
// A scheduled event name is required to prevent overlapping. Use the 'name' method before 'withoutOverlapping'
$schedule->call(function(){
echo "不重复运行,", date('Y-m-d H:i:s');
sleep(30);
})->name('NoRepect')->withoutOverlapping();
// test2
public function handle()
{
echo "Command sleep 20's, ", date("Y-m-d H:i:s");
sleep(20);
return 0;
}
在 test2 这个测试脚本中,我们只是给它 sleep() 了 20 秒。然后使用一个命令行界面去运行 php artisan schedule:run 时会卡到这里。接着你再使用一个命令行来运行,那么这时就会直接运行完整个任务调度里面的程序,不会再次进入到 test2 来运行。这个说的可能比较晕乎,大家自己试一下就知道了,只需要在命令后面加上 withoutOverlapping() 就可以了。
下面另一段是使用回调函数的任务脚本,这种方式的脚本在避免任务重复的时候,必须要有一个任务名称,也就是 ->name('NoRepect') 这里要配置一个名称,否则会报上面注释中的错误。
辅助操作函数
除了上面我们学习到的那些基本用法之外,任务调度还提供了针对单个任务的一些其它辅助函数,主要是几个勾子函数,我们一个一个来看看。
任务前后勾子
任务前后勾子函数其实就是在任务执行的前后先运行函数中的内容。
$schedule->call(function(){
echo "function";
})->before(function(){
echo "before";
})->after(function(){
echo "after";
});
这段代码输出的内容就是 beforefunctionafter 这样的内容。
任务成功失败勾子
任务成功失败的勾子函数其实也是类似的,是在任务返回成功失败码的时候执行不同的函数内容。
$schedule->command(test1::class, ['3', '--b=4'])->onSuccess(function(){
echo "success";
})->onFailure(function(){
echo "failure";
});
这个成功失败码是怎么定义的呢?其实就是 Linux 中的程序结束码,如果是 0 的话,就认为程序是执行成功的,任何非 0 的就是失败的。
任务请求勾子
最后是请求勾子函数,它的作用是在任务执行前后,去 ping 一个链接。你可以为这个链接带上 GET 参数,实际上就是实现了一次 GET 请求。
$schedule->call(function(){
echo "ping";
})->pingBefore('http://laravel8/request')->thenPing('http://laravel8/request');
调用分析
对于任务调度来说,它的整个执行过程还是比较复杂的,不过根本上其实走的是我们的 事件 系统,也就是在上一课所学习过的内容。首先,我们执行的 artisan 本身就是一个类似于 public/index.php 这样的入口文件,它会调用 vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php 创建一个核心对象。
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();
});
}
protected function defineConsoleSchedule()
{
$this->app->singleton(Schedule::class, function ($app) {
return tap(new Schedule($this->scheduleTimezone()), function ($schedule) {
$this->schedule($schedule->useCache($this->scheduleCache()));
});
});
}
在这个对象的构造函数中,你会看到它调用了 defineConsoleSchedule() 这个方法,而这个方法里面的回调函数中会看到 $this->schedule() 方法的调用,它就是我们在 app/Console/Kernel.php 中定义调度的方法。app/Console/Kernel.php 本身就是继承自 vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php 的,所以这里是一个 模板方法模式 的典型应用。
接下来我们就一步步向下追踪,中间的过程很多,我们就挑一些重要的看看。schedule() 方法定义的调度并不是马上执行的,它会全部转换成闭包对象留着后面通过事件运行,在 vendor/laravel/framework/src/Illuminate/Console/Scheduling/Schedule.php 中,我们可以看到如果是 command() 方法准备的调度,那么在这里的 exec() 方法中会生成命令行信息。这个 Schedule 对象又是啥?schedule() 方法传递过来的参数 $schedule 还记得吧,上面 defineConsoleSchedule() 中 new 出来的对象。
public function exec($command, array $parameters = [])
{
if (count($parameters)) {
$command .= ' '.$this->compileParameters($parameters);
// '/usr/local/Cellar/php/7.3.9_1/bin/php' 'artisan' ZyBlog:Test1 2 --b=3
}
$this->events[] = $event = new Event($this->eventMutex, $command, $this->timezone);
return $event; // Illuminate\Console\Scheduling\Event
}
注意最后返回的这个事件对象是 Illuminate\Console\Scheduling\Event.php 的这个事件对象。不是我们上节课讲过的普通事件对象。如果是 回调 形式的任务调度,那么会走另外一条线,返回的是 CallbackEvent 对象。这个大家可以自己跟踪调试看一下。
上述工作结束后,就来到了 vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php 的 handle() 方法。它会去执行 schedule:run 这个命令,这个命令定义的类是在 learn-laravel/vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php 中,继续进入这个类的 handle() 方法中。
public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHandler $handler)
{
// ……
// ……
if ($event->onOneServer) {
$this->runSingleServerEvent($event);
} else {
$this->runEvent($event);
}
// ……
// ……
}
没错,核心的就是这两个 run 相关的方法了,继续追踪,就会发现它们实现上就是在进行事件分发,也就是调用事件了。剩下的想必也不用我多说了,定义好的调度事件就会一个一个的执行了。
总结
任务调度的功能看似简单,但实际内容的调用过程其实非常复杂,还有很多东西我们并没有讲到,但是大体的思路已经出来了。大家可以继续进行深入的研究。如果你只是日常应用的话,到这里其实就已经够了,出现什么问题也可以自己尝试在源码中排查了。
参考文档:
https://learnku.com/docs/laravel/8.5/scheduling/10396