>  기사  >  PHP 프레임워크  >  Laravel의 예약된 작업 스케줄링 메커니즘에 대한 심층적인 이해

Laravel의 예약된 작업 스케줄링 메커니즘에 대한 심층적인 이해

WBOY
WBOY앞으로
2022-02-23 17:20:175396검색

이 글은 laravel의 예약된 작업 스케줄링 메커니즘에 대한 관련 지식을 제공하며, 기본 구현 로직, 백그라운드 작업 및 중복 방지와 관련된 문제를 주로 소개합니다.

Laravel의 예약된 작업 스케줄링 메커니즘에 대한 심층적인 이해

【관련 추천: laravel 동영상 튜토리얼

1. 기본 구현 로직

복잡한 웹 시스템 환경에서는 실행해야 할 예약된 스크립트나 작업이 많아야 합니다.

예를 들어, 크롤러 시스템은 일부 웹사이트 데이터를 정기적으로 크롤링해야 하고, 대출 자동 상환 시스템은 매달 정기적으로 사용자 계정에서 인출 및 정산을 해야 하며,

멤버십 시스템은 정기적으로 남은 멤버십 일수를 감지해야 합니다. 사용자에게 적시에 갱신 사실을 알리기 위해 Linux 시스템에 내장된 crontab은 일반적으로 예약된 작업을 실행하는 데 널리 사용됩니다. 작업 명령 형식은 다음과 같습니다.

crontab 명령 설명

명령줄 crontab -e crontab 편집에 들어가서 실행하려는 명령을 편집하고 저장하고 종료하면 적용됩니다.

그러나 이 기사에서는 crontab의 내용을 너무 많이 논의하지는 않지만 PHP Laravel 프레임워크가 crontab 기반의 보다 강력한 작업 스케줄링(Task Scheduling) 모듈을 캡슐화하는 방법에 대한 심층 분석을 제공할 것입니다.

예약된 작업의 경우 물론 각 작업에 대해 crontab 명령을 구성할 수 있습니다. 그러나 이렇게 하면 예약된 작업 수가 증가함에 따라 crontab 명령도 선형적으로 증가합니다.

결국 crontab은 시스템 수준의 구성이므로 업무상 기계를 절약하기 위해 동일한 서버에 여러 개의 작은 프로젝트를 배치하는 경우가 많습니다. c

rontab 명령이 너무 많으면 쉬울 것입니다. 혼란을 관리하고 기능도 충분히 유연하지 않고 강력하지 않습니다(의지대로 중지 및 시작할 수 없고 작업 간 종속성을 처리할 수 없음 등).

이 문제에 대한 Laravel의 해결책은 비즈니스의 모든 예약된 작업이 이 crontab에서 처리되고 판단되어 코드 수준에서 작업을 관리하는 것입니다.

* * * * * php artisan schedule:run >> /dev/null 2>&1

즉, php artisan Schedule:run은 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任务
    }
}

에 등록되어 있습니다. 위의 예에서 세 가지 예약된 작업이 시스템에 등록되어 있음을 명확하게 볼 수 있습니다. 그리고 제공합니다. EveryMinute, EveryFifteenMinutes, 매일, 시간별 및 기타 의미론적 방법을 제공하여 작업 주기를 구성합니다.

본질적으로 이러한 의미 체계 메서드는 crontab 표현의 또 다른 이름일 뿐이며 결국 crontab의 표현식으로 변환됩니다(예: * * * * *는 1분에 한 번씩 실행됨을 의미함).

이런 식으로 1분마다 실행되는 php artisan Schedule:run 명령은 Kernel::schedule에 등록된 모든 명령어를 스캔하고 해당 명령어에 대해 구성된 실행 주기가 만료되었는지 확인합니다.

만료되면 푸시됩니다. 실행될 큐입니다. 마지막으로 모든 명령을 순서대로 실행합니다.

// ScheduleRunCommand::handle函数
public function handle()
{
    foreach ($this->schedule->dueEvents() as $event) {
        if (! $event->filtersPass()) {
            continue;
        }
        $event->run();
    }
}

일정 작업 흐름도

여기서 주목해야 할 두 가지 사항이 있습니다. 먼저, 지시가 기한이 되었는지, 실행해야 하는지 판단하는 방법입니다. 둘째, 명령어 실행 순서의 문제이다.

먼저 crontab 표현식에서 지정하는 실행 시간은 상대 시간이 아닌 절대 시간을 의미합니다. 따라서 현재 시간과 crontab 표현식을 기반으로

명령이 기한이 지났고 실행되어야 하는지 여부를 결정할 수 있습니다. 상대 시간을 구현하려면 마지막 실행 시간을 저장해야 하며, 그런 다음 다음 실행 시간을 계산할 수 있습니다. 절대 시간과 상대 시간의 차이는 다음 그림으로 요약할 수 있습니다. (crontab의 실행 시간은 그림 왼쪽 목록에 표시됩니다.)

Laravel은 crontab 표현식의 정적 분석 및 판단을 위해 cron 표현식 라이브러리(github.com/mtdowling/cron-expression)를 사용합니다. 그 원리도 비교적 직관적입니다. 즉, 정적 문자 분석 및 비교입니다.

crontab은 상대 시간이 아닌 절대 시간입니다

두 번째 문제는 실행 순서입니다. 이전 그림을 보면 Kernel::schedule 메소드에 여러 작업을 등록하면

일반적으로 알 수 있습니다. 순차적으로 실행됩니다. 즉, 태스크 2는 태스크 1이 완료될 때까지 실행을 시작하지 않습니다.

이 경우 Task 1의 시간이 매우 많이 걸리면 Task 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来唯一标识一个待执行任务,

通过比较系统中注册的所有任务的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);
}

3. 防止重复

有些定时任务指令需要执行很长时间,而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);
}

4. 如何实现30秒任务?

我们知道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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 csdn.net에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제