Home >PHP Framework >Laravel >In-depth understanding of Laravel's scheduled task scheduling mechanism
This article brings you relevant knowledge about the laravel scheduled task scheduling mechanism. It mainly introduces the basic implementation logic, background operation and related issues to prevent duplication. I hope it will be helpful to everyone.
[Related recommendations: laravel video tutorial]
A complex web In the background of the system, there must be many scheduled scripts or tasks to be run.
For example, the crawler system needs to crawl some website data regularly, the automatic loan repayment system needs to regularly debit and settle user accounts every month,
The membership system needs to regularly detect the number of remaining membership days of the user so that Prompt notification of renewals, etc. The built-in crontab in Linux systems is generally widely used to run scheduled tasks. The task command format is as follows:
crontab command explanation
Command line crontab -e to enter crontab editing, edit the command you want to execute, save and exit It will take effect.
However, this article will not discuss the content of crontab too much, but will provide an in-depth analysis of how the PHP Laravel framework encapsulates a more powerful task scheduling (Task Scheduling) module based on crontab.
For scheduled tasks, of course we can configure a crontab instruction for each task. However, if you do this, as the number of scheduled tasks increases, the crontab instructions will also increase linearly.
After all, crontab is a system-level configuration. In order to save machines in business, we often put multiple small projects on the same server. There are many c
rontab instructions. Without it, it is easy to manage chaos, and the functions are not flexible and powerful enough (cannot stop and start at will, handle dependencies between tasks, etc.).
Laravel’s solution is to declare only one crontab, and all scheduled tasks in the business are processed and judged in this crontab to achieve management tasks at the code level:
* * * * * php artisan schedule:run >> /dev/null 2>&1
That is php artisan schedule:run runs once every minute (the highest frequency of crontab). As for the specific task configuration in the business, it is registered in Kernel::schedule()
class Kernel extends ConsoleKernel { Protected function schedule(Schedule $schedule) { $schedule->command('account:check')->everyMinute(); // 每分钟执行一次php artisan account:check 指令 $schedule->exec('node /home/username/index.js')->everyFifteenMinutes(); //每15分钟执行一次node /home/username/index.js 命令 $schedule->job(new MyJob())->cron('1 2 3 10 *'); // 每年的10月3日凌晨2点1分向任务队列分发一个MyJob任务 } }
We can see it clearly in the above example Three scheduled tasks are registered in the system, and semantic methods such as everyMinute, everyFifteenMinutes, daily, and hourly are provided to configure the task cycle.
Essentially, these semantic methods are just another name for the crontab representation, and will eventually be converted into the expression in crontab (such as * * * * * means execution once every minute).
In this way, the php artisan schedule:run command executed once every minute will scan all commands registered in Kernel::schedule and determine that the execution cycle configured for the command has expired.
If it expires, push it into the pending execution queue. Finally execute all instructions in sequence.
// ScheduleRunCommand::handle函数 public function handle() { foreach ($this->schedule->dueEvents() as $event) { if (! $event->filtersPass()) { continue; } $event->run(); } }
schedule task flow chart
There are two points to note here. First, how to judge whether the instruction has been due and should be executed. Second, the issue of the order of execution of instructions.
First of all, the execution time specified by the crontab expression refers to absolute time, not relative time. Therefore, only based on the current time and crontab expression,
can determine whether the instruction has been due and should be executed. If you want to implement relative time, you must store the time of the last execution,
and then you can calculate when the next execution should be. The difference between absolute time and relative time can be summarized by the following picture (the execution time of crontab is shown in the list on the left of the picture).
Laravel uses the cron-expression library (github.com/mtdowling/cron-expression) for static analysis and judgment of crontab expressions. The principle is also relatively intuitive, which is static character analysis and comparison.
crontab is an absolute time, not a relative time
The second problem is the execution order. We can see from the previous figure that if you Multiple tasks are registered in the Kernel::schedule method.
Normally, they are executed sequentially. That is to say, Task 2 will not start execution until Task 1 is completed.
In this case, if Task 1 is very time-consuming, it will affect the on-time execution of Task 2, which requires special attention during development.
However, adding runInBackground when registering a task in Kernel::schedule can realize the background execution of the task. We will discuss this in detail below.
The previously mentioned feature of the scheduled task queue's sequential execution. If the execution time of the previous task is too long, it will hinder the on-time execution of the subsequent tasks.
To solve this problem, Laravel provides the method runInBackground to execute the task in the background. like:
// Kernel.php protected function schedule(Schedule $schedule) { $schedule->command('test:hello') // 执行command命令:php artisan test:hello ->cron('10 11 1 * *') // 每月1日的11:10:00执行该命令 ->timezone('Asia/Shanghai') // 设置时区 ->before(function(){/*do something*/}) // 前置hook,命令执行前执行此回调 ->after(function(){/*do something*/}) // 后置钩子,命令执行完之后执行此回调 ->runInBackground(); // 后台运行本命令 // 每分钟执行command命令:php artisan test:world $schedule->command('test:world')->everyMinute(); }
后台运行的原理,其实也非常简单。我们知道在linux系统下,命令行的指令最后加个“&”符号,可以使任务在后台执行。
runInBackground方法内部原理其实就是让最后跑的指令后面加了“&”符号。不过在任务改为后台执行之后,
又有了一个新的问题,即如何触发任务的后置钩子函数。因为后置钩子函数是需要在任务跑完之后立即执行,
所以必须要有办法监测到后台运行的任务结束的一瞬间。我们从源代码中一探究竟(Illuminate/Console/Scheduling/CommandBuilder.php)
// 构建运行在后台的command指令 protected function buildBackgroundCommand(Event $event) { $output = ProcessUtils::escapeArgument($event->output); $redirect = $event->shouldAppendOutput ? ' >> ' : ' > '; $finished = Application::formatCommandString('schedule:finish').' "'.$event->mutexName().'"'; return $this->ensureCorrectUser($event, '('.$event->command.$redirect.$output.' 2>&1 '.(windows_os() ? '&' : ';').' '.$finished.') > ' .ProcessUtils::escapeArgument($event->getDefaultOutput()).' 2>&1 &' ); }
$finished字符串的内容是一个隐藏的php artisan指令,即php artisan schedule:finish
该命令被附在了本来要执行的command命令后面,用来检测并执行后置钩子函数。
php artisan schedule:finish
通过比较系统中注册的所有任务的mutex_name,来确定需要执行哪个任务的后置函数。代码如下:
// Illuminate/Console/Scheduling/ScheduleFinishCommand.php // php artisan schedule:finish指令的源代码 public function handle() { collect($this->schedule->events())->filter(function ($value) { return $value->mutexName() == $this->argument('id'); })->each->callAfterCallbacks($this->laravel); }
有些定时任务指令需要执行很长时间,而laravel schedule任务最频繁可以做到1分钟跑一次。
这也就意味着,如果任务本身跑了1分钟以上都没有结束,那么等到下一个1分钟到来的时候,又一个相同的任务跑起来了。
这很可能是我们不想看到的结果。因此,有必要想一种机制,来避免任务在同一时刻的重复执行(prevent overlapping)。
这种场景非常类似多进程或者多线程的程序抢夺资源的情形,常见的预防方式就是给资源加锁。
具体到laravel定时任务,那就是给任务加锁,只有拿到任务锁之后,才能够执行任务的具体内容。
Laravel中提供了withoutOverlapping方法来让定时任务避免重复。具体锁的实现上,需要实现Illuminate\Console\Scheduling\Mutex.php接口中所定义的三个接口:
interface Mutex { // 实现创建锁接口 public function create(Event $event); // 实现判断锁是否存在的接口 public function exists(Event $event); // 实现解除锁的接口 public function forget(Event $event); }
该接口当然可以自己实现,Laravel也给了一套默认实现,即利用缓存作为存储锁的载体(可参考Illuminate\Console\Scheduling\CacheMutex.php文件)。
在每次跑任务之间,程序都会做出判断,是否需要防止重复,如果重复了,则不再跑任务代码:
// Illuminate\Console\Scheduling\Event.php public function run() { // 判断是否需要防止重复,若需要防重复,并且创建锁不成功,则说明已经有任务在跑了,这时直接退出,不再执行具体任务 if ($this->withoutOverlapping && ! $this->mutex->create($this)) { return; } $this->runInBackground?$this->runCommandInBackground($container):$this->runCommandInForeground($container); }
我们知道crontab任务最精细的粒度只能到分钟级别。那么如果我想实现30s执行一次的任务,
需要如何实现?关于这个问题,stackoverflow上面也有一些讨论,有建议说在业务层面实现,自己写个sleep来实现,示例代码如下:
public function handle() { runYourCode(); // 跑业务代码 sleep(30); // 睡30秒 runYourCode(); // 再跑一次业务代码 }
如果runYourCode执行实现不太长的话,上面这个任务每隔1min执行一次,其实相当于runYourCode函数每30秒执行一次。
如果runYourCode函数本身执行时间比较长,那这里的sleep 30秒会不那么精确。
当然,也可以不使用Laravel的定时任务系统,改用专门的定时任务调度开源工具来实现每隔30秒执行一次的功能,
在此推荐一个定时任务调度工具nomad(https://github.com/hashicorp/nomad)。
如果你确实要用Laravel自带的定时任务系统,并且又想实现更精确一些的每隔30秒执行一次任务的功能,那么可以结合laravel 的queue job来实现。如下:
public function handle() { $job1 = (new MyJob())->onQueue(“queue-name”); $job2 = (new MyJob())->onQueue(“queue-name”)->delay(30); dispatch($job1); dispatch($job2): } class MyJob implement Illuminate\Contracts\Queue\ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function handle() { runYourCode(); } }
通过Laravel 队列功能的delay方法,可以将任务延时30s执行,因此如果每隔1min,我们都往队列中dispatch两个任务,其中一个延时30秒。
另外,把自己要执行的代码runYourCode写在任务中,即可实现30秒执行一次的功能。不过这里需要注意的是,这种实现中scheduling的防止重合功能不再有效,
需要自己在业务代码runYourCode中实现加锁防止重复的功能。
以上,就是使用Laravel Scheduling定时任务调度的原理分析和注意事项。作为最流行的PHP框架,Laravel大而全,
组件基本包含了web开发的各方面需求。其中很多组件的实现思想,还是很值得深入源码一探究竟的。
【相关推荐:laravel视频教程】
The above is the detailed content of In-depth understanding of Laravel's scheduled task scheduling mechanism. For more information, please follow other related articles on the PHP Chinese website!