この記事では、laravel スケジュールされたタスクのスケジューリング メカニズムに関する関連知識を提供します。主に、基本的な実装ロジック、バックグラウンド操作、および重複を防ぐための関連問題について紹介します。皆様のお役に立てれば幸いです。
[関連する推奨事項: laravel ビデオ チュートリアル]
複雑な Web Inシステムのバックグラウンドでは、実行されるスケジュールされたスクリプトやタスクが多数存在する必要があります。
たとえば、クローラー システムは一部の Web サイト データを定期的にクロールする必要があり、自動ローン返済システムは毎月定期的にユーザー アカウントの引き落としと決済を行う必要があり、
メンバーシップ システムは定期的にユーザーの会員残日数を把握し、更新等を速やかに通知します。 Linux システムの組み込み crontab は、スケジュールされたタスクを実行するために一般に広く使用されています。タスク コマンドの形式は次のとおりです。
crontab コマンドの説明
コマンド ライン crontab -e で crontab 編集に入り、実行するコマンドを編集します。保存して終了すると有効になります。
ただし、この記事では crontab の内容についてはあまり説明しませんが、PHP Laravel フレームワークが crontab に基づいたより強力なタスク スケジューリング (タスク スケジューリング) モジュールをどのようにカプセル化するかについて詳細な分析を提供します。
スケジュールされたタスクの場合、もちろん、タスクごとに crontab 命令を構成できます。ただし、これを行うと、スケジュールされたタスクの数が増加するにつれて、crontab 命令も直線的に増加します。
結局のところ、crontab はシステム レベルの構成です。ビジネスでマシンを節約するために、私たちはよく複数の小さなプロジェクトを同じサーバーに配置します。多くの c
rontab 命令があります。これがなければ、混乱を管理するのは簡単ですが、機能は十分に柔軟で強力ではありません(自由に停止および開始できない、タスク間の依存関係を処理できないなど)。
Laravel の解決策は、crontab を 1 つだけ宣言することです。ビジネス内のスケジュールされたタスクはすべてこの crontab で処理および判断され、コード レベルでタスクを管理します:
* * * * * php artisan schedule:run >> /dev/null 2>&1
それが php 職人ですSchedule:run は 1 分に 1 回実行されます (crontab の最高頻度) ビジネスにおける特定のタスク構成については、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任务 } }
に登録されています。例 3 つのスケジュールされたタスクがシステムに登録されており、タスク サイクルを構成するために、everyMinute、everyFifteenMinutes、daily、hourly などのセマンティック メソッドが提供されます。
本質的に、これらのセマンティック メソッドは crontab 表現の別名にすぎず、最終的には crontab の式に変換されます (* * * * * は 1 分ごとに実行を意味します)。
このように、1分ごとに実行されるphpArtisanのschedule:runコマンドは、Kernel::scheduleに登録されているすべてのコマンドをスキャンし、コマンドに設定された実行サイクルの期限が切れたことを判断します。有効期限が切れた場合は、保留中の実行キューにプッシュします。最後に、すべての命令を順番に実行します。
// ScheduleRunCommand::handle函数 public function handle() { foreach ($this->schedule->dueEvents() as $event) { if (! $event->filtersPass()) { continue; } $event->run(); } }
スケジュールタスクのフローチャート
ここで注意すべき点は 2 つあり、1 つは、指示が期日を超えて実行されるべきかどうかを判断する方法です。第二に、命令の実行順序の問題です。
まず、crontab 式で指定される実行時間は、相対時間ではなく絶対時間を指します。したがって、
は、現在の時刻と crontab 式に基づいてのみ、命令の期限が切れているか、実行する必要があるかを判断できます。相対時間を実装する場合は、最後の実行時刻
を保存する必要があります。そうすれば、次の実行時刻を計算できます。絶対時間と相対時間の違いは次の図のように要約できます (crontab の実行時間は図の左側のリストに表示されます)。
Laravel では、crontab 式の静的解析と判定に cron-expression ライブラリ (github.com/mtdowling/cron-expression) を使用していますが、その原理も静的な文字解析と比較という比較的直感的なものです。
crontab は相対時間ではなく絶対時間です
2 番目の問題は実行順序です。前の図からわかるように、複数の時間を実行すると、タスクは Kernel::schedule メソッドに登録されます
通常、タスクは順番に実行されます。つまり、タスク 1 が完了するまでタスク 2 は実行を開始しません。
この場合、タスク 1 に非常に時間がかかると、タスク 2 の予定どおりの実行に影響するため、開発中に特別な注意が必要になります。
ただし、Kernel::schedule にタスクを登録する際に runInBackground を追加することで、タスクのバックグラウンド実行を実現できます。
2. バックグラウンド実行
この問題を解決するために、Laravel はバックグラウンドでタスクを実行するメソッド runInBackground を提供します。のように:###
// 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视频教程】
以上がLaravelのスケジュールされたタスクのスケジューリングメカニズムについての深い理解の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。