Laravel 技巧之 定时任务

定时任务 Scheduled Tasks 是 Laravel 提供的组件之一,稍微上点规模的项目应该都会用到,比如开发微信应用时通过定时任务去刷新access token,比如每天定时发推送提醒用户要记得签到。对于定时任务的基本用法,官网文档已经描述得很详细了,这里不再多说。

本文主要是介绍定时任务在实际应用中的两个小技巧:

1. 多个任务并行执行

先简单介绍一下 Laravel 定时任务组件的基本原理:

当cli初始化完毕之后,系统会调用 App\Console\Kernel::schedule 方法,也就是我们定义定时任务列表的地方,这个方法里每调用一次 $schedule->command() 就会生成一个 Illuminate\Console\Scheduling\Event 对象并保存在 $schedule->events 数组里。当执行 php artisan scheduled:run 时,系统会遍历 $schedule->events,把当前时间需要执行的任务放在一个集合中,最后依次 串行执行 这些任务。

这样做在大多数情况下是没有问题的,但有一些特殊的情况,比如在每个月的第一天要给100W个用户发送邮件,同一批次的定时任务必须等到这些邮件全部发送完毕之后才会被执行,假如这些任务里有对执行时间十分敏感的任务,比每5分钟一次的数据快照,就会导致那个时间点数据的缺失。

这种情况下如果定时任务能够并行执行,就不会有这样的问题。Laravel 实际上提供了解决方案,但很奇怪文档里面并没有提到,就是 runInBackground 方法,在定义定时任务时 $schedule->command('foo:bar')->everyMinutes()->runInBackground(); 就可以了。

2. 负载均衡

随着业务逻辑的增多,定时任务也会越来越多,定时任务服务器的负载也会越来越高,甚至导致任务执行缓慢,然而我们却只能在一台服务器上设置定时任务,如果在多台服务器上同时配置了定时任务,还会导致定时任务的重复执行。这个时候我们希望能够像队列那样,将定时任务分散到多台服务器上。

截止 v5.4.15,Laravel 还没有提供内置方案来解决这个问题,但只需要简单的改造就可以实现我们需要的效果。首先我们把将每个定时任务里 handle 方法提取出来创建一个新的Job并继承 ShouldQueue,然后在定时任务的 handle 里直接 dispatch 对应的Job即可,这样原本的业务逻辑就会被队列处理掉,当系统有多台服务器在处理队列时,也就实现了我们需要的负载均衡。

但是这样毕竟还是麻烦,每个定时任务都要创建一个Command和一个Job,太费劲,于是我提交了一个 Proposal ,目前已经实现并且merge入5.4分支,相信下个版本大家就能用上了。用法也很简单,只需要创建一个继承 ShouldQueue的Job,然后在App\Console\Kernel::schedule 方法里定义

$schedule->job(new FooBarJob())->everyMinutes();

就可以了

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由 Summer 于 7年前 加精
leo
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 22
Summer

:+1: 有一个非常棒的功能

7年前 评论
Summer

7年前 评论

不错,非常实用

7年前 评论

厉害了我的哥

7年前 评论
Destiny

很厉害。。。原来是你。。。

7年前 评论

刚看了下Laravel 5.1.45并没有 runInBackground 方法,刚准备上线一个很重的定时任务。。。

7年前 评论
leo

@binafor 看了下提交日志,应该是5.2才引入的

7年前 评论

我刚在5.1 下测试了下,貌似5.1 本来就是并行运行的,知道我这样的测试有没有问题?

//command 1
public function handle()
    {
        Log::info(1);
        sleep(40);
        $this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
        Log::info(2);
    }

//command 2
public function handle()
    {
        Log::info('test');
        sleep(30);
        Log::info('end');
    }

//app/Console/Kernel.php
protected function schedule(Schedule $schedule)
    {
        $schedule->command('test')->everyMinute();
        $schedule->command('inspire')->everyMinute();
    }

file
file

从日志来看,启动是同时的,从进程来看,刚到整00s时,我检测到 同时启动了两个进程

7年前 评论

@leo 知道我这样的测试有没有问题? -> 不知道我这样的测试有没有问题?
新输入法打个字漏洞百出。。见谅

7年前 评论
LDL1023

laravel 中怎样实现秒级的定时任务呢?
比如我有个任务要 5 秒跑一次。

6年前 评论
leo

@LDL1023 Laravel 原生是不支持秒级的,这个需要自己找方法实现了

6年前 评论
LDL1023

@leo 好的

6年前 评论
wanghan

@leo 楼主今天新建了一个5.4.30的项目用来测试定时任务,可是我发现$schedule->call('foo')->before('bar'),其中before不会被执行,百度上也找不到答案,楼主遇见过吗?

5年前 评论

秒级定时任务这样子做可有不妥?

通过everyMinute后使用sleep

   for ($i=0; $i<20; $i++) {
        sleep(3);
    }
4年前 评论

@leo 请问 leo 老师,定时任务以队列方式运行,不也是在本机运行的吗? laravel 的队列系统可以自动在多个负载均衡服务器中分配任务吗?

2年前 评论
leo

@weir 可以,但需要每一台服务器都启动 queue worker

2年前 评论

file 用runInBackground每分钟运行的时候会出现偶尔30s的延迟。这是为什么呢

2年前 评论
woshijust (作者) 2年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!