深入理解Laravel定时任务调度机制
本篇文章给大家带来了关于laravel定时任务调度机制的相关知识,其中主要介绍了基本实现逻辑、后台运行以及防止重复的相关问题,希望对大家有帮助。
【相关推荐:laravel视频教程】
1. 基本实现逻辑
一个复杂的web系统后台当中,一定会有很多定时脚本或者任务要跑。
例如爬虫系统需要定期去爬取一些网站数据,自动还贷系统需要每个月定时对用户账户扣款结算,
会员系统需要定期检测用户剩余会员天数以便及时通知续费等等。Linux系统中内置的crontab一般被广泛地用于跑定时任务。其任务指令格式如下:
crontab指令解释
命令行crontab -e进入crontab编辑,把自己要执行的指令编辑好之后保存退出即可生效。
不过本文并不会过多讨论crontab的内容,而是要深入分析一下PHP Laravel框架是如何基于crontab封装出功能更加强大的任务调度(Task Scheduling)模块。
对于定时任务,我们当然可以每个任务配置一个crontab指令。只不过这样做的话随着定时任务的增加,crontab指令也线性增长。
毕竟crontab是一项系统级的配置,在业务中我们为了节约机器,往往对于量不大的多个项目会放在同一台服务器上,c
rontab指令多了就容易管理混乱,并且功能也不够灵活强大(无法随心所欲的停启、处理任务间依赖关系等)。
对此Laravel的解决方案是只声明一条crontab,业务中的所有定时任务全都在这一条crontab中做处理和判断,实现在代码层面管理任务:
* * * * * php artisan schedule:run >> /dev/null 2>&1
即php artisan schedule:run每分钟跑一次(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, daily, hourly等语义化的方法来配置任务周期。
本质上,这些语义化的方法只是crontab表示方式的一个别称罢了,最终都会转化为crontab中的表达方式(如 * * * * * 表示每分钟执行一次)。
如此一来,每分钟执行一次的php artisan schedule:run指令,会扫描Kernel::schedule中注册的所有指令并判断该指令配置的执行周期时候已经到期,
如果到期则推入待执行队列。最后依次执行所有的指令。
// ScheduleRunCommand::handle函数 public function handle() { foreach ($this->schedule->dueEvents() as $event) { if (! $event->filtersPass()) { continue; } $event->run(); } }
schedule task流程图
这里需要注意两个点,第一、如何判断指令是否已经Due了该执行了。第二、指令的执行顺序问题。
首先,crontab表达式所指定的执行时间,是指绝对时间,而不是相对时间。所以仅仅根据当前时间和crontab表达式,
即可判断出指令是否已经Due了该执行了。如果想要实现相对时间,那么必须存储上一次执行的时间,
然后才能进行推算下次执行应该是什么时候。绝对时间和相对时间的区别可以用下面一幅图概括(crontab的执行时间如图中左侧列表所示)。
Laravel中对于crontab表达式的静态分析和判断使用的是cron-expression库(github.com/mtdowling/cron-expression),原理也比较直观,就是静态的字符分析比对。
crontab是绝对时间,而非相对时间
第二个问题是执行顺序,前面的图中我们可以看出,如果你在Kernel::schedule方法中注册了多个任务,
正常情况下它们是顺序依次执行的。也就是说必须要等到Task 1执行完成之后,Task 2才会开始执行。
在这种情况下,如果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,来确定需要执行哪个任务的后置函数。代码如下:
// 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中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

PHP和Flutter是移动端开发的流行技术。Flutter胜在跨平台能力、性能和用户界面,适合需要高性能、跨平台和自定义UI的应用程序。PHP则适用于性能较低、不跨平台的服务器端应用程序。

使用ORM可简化PHP中的数据库操作,它将对象映射到关系数据库中。Laravel中的EloquentORM允许使用面向对象的语法与数据库交互,可通过定义模型类、使用Eloquent方法或在实战中构建博客系统等方式来使用ORM。

Laravel - Artisan 命令 - Laravel 5.7 提供了处理和测试新命令的新方法。它包括测试 artisan 命令的新功能,下面提到了演示?

PHP单元测试工具分析:PHPUnit:适用于大型项目,提供全面功能,易于安装,但可能冗长且速度较慢。PHPUnitWrapper:适合小型项目,易于使用,针对Lumen/Laravel优化,但功能受限,不提供代码覆盖率分析,社区支持有限。

Laravel9和CodeIgniter4的最新版本提供了更新的特性和改进。Laravel9采用MVC架构,提供数据库迁移、身份验证和模板引擎等功能。CodeIgniter4采用HMVC架构,提供路由、ORM和缓存。在性能方面,Laravel9的基于服务提供者设计模式和CodeIgniter4的轻量级框架使其具有出色的性能。在实际应用中,Laravel9适用于需要灵活性和强大功能的复杂项目,而CodeIgniter4适用于快速开发和小型应用程序。

比较Laravel和CodeIgniter的数据处理能力:ORM:Laravel使用EloquentORM,提供类对象关系映射,而CodeIgniter使用ActiveRecord,将数据库模型表示为PHP类的子类。查询构建器:Laravel具有灵活的链式查询API,而CodeIgniter的查询构建器更简单,基于数组。数据验证:Laravel提供了一个Validator类,支持自定义验证规则,而CodeIgniter的验证功能内置较少,需要手动编码自定义规则。实战案例:用户注册示例展示了Lar

PHP单元和集成测试指南单元测试:关注单个代码单元或函数,使用PHPUnit创建测试用例类进行验证。集成测试:关注多个代码单元协同工作的情况,使用PHPUnit的setUp()和tearDown()方法设置和清理测试环境。实战案例:使用PHPUnit在Laravel应用中进行单元和集成测试,包括创建数据库、启动服务器以及编写测试代码。

在选择大型项目框架时,Laravel和CodeIgniter各有优势。Laravel针对企业级应用程序而设计,提供模块化设计、依赖项注入和强大的功能集。CodeIgniter是一款轻量级框架,更适合小型到中型项目,强调速度和易用性。对于具有复杂需求和大量用户的大型项目,Laravel的强大功能和可扩展性更合适。而对于简单项目或资源有限的情况下,CodeIgniter的轻量级和快速开发能力则更为理想。
