基于 Swoole 定时器实现毫秒级任务调度
Swoole 定时器简介
Swoole 提供了异步高精度定时器功能,该功能类似 JavaScript 的 setInterval
/setTimeout
,粒度为毫秒级,底层基于 epoll_wait
(异步)和 setitimer
(同步)实现,数据结构使用最小堆。定时器的添加和删除,全部为内存操作,无 IO 消耗,因此性能是非常高的。
需要注意的是,Swoole 实现的定时器与 PHP 自带的 pcntl_alarm 不同,pcntl_alarm
是基于时钟信号和 tick
函数实现的,最大仅支持到秒,此外不支持同时设定多个定时器程序,性能也很差。
Swoole 提供了两种类型的定时器,一种是每隔一定时间执行的定时器,一种是指定时间后一次性执行的定时器,下面我们分别看看这两种定时器的实现和使用。
Swoole 定时器的使用
间隔时钟定时器
在 Swoole 中,我们可以通过 Timer::tick
实现间隔时钟定时器,该定时器会每隔指定时间触发回调函数的执行:
\Swoole\Timer::tick(1000, function () {
echo "Swoole 很棒\n";
});
Swoole 定时器需要在 PHP CLI 模式下才能运行,上述代码的意思是每隔 1000ms 执行一次回调函数打印字符串:
指定时钟定时器
此外,还可以通过 Timer::after
定义一个指定时间后执行的定时器,与间隔时钟定时器不同,这种定时器是一次性的,执行完成后就会销毁:
\Swoole\Timer::after(3000, function () {
echo "Laravel 也很棒\n";
});
上述定义器的含义是 3000ms 后执行指定的回调函数,并且执行之后就退出程序:
清除定时器
对于一次性执行的指定时钟定时器,不用关心清除问题,而对于间隔时钟定时器,如果不定义清楚逻辑的话,会永远执行下去,直到程序退出,我们可以通过 Timer::clear
删除定时器来达到清除的目的,在具体实现的时候,将定时器 ID 传入该方法即可,上述两种定时器定义方法都会返回对应的定时器 ID, 因此,清除定义器可以作用于上述两种定时器:
/*$timerId = \Swoole\Timer::tick(1000, function () {
echo "Swoole 很棒\n";
});*/
$timerId = \Swoole\Timer::after(1000, function () {
echo "Laravel 也很棒\n";
});
\Swoole\Timer::clear($timerId);
这种情况下,两个定时器都不会调用,对于间隔时钟定时器,还可以这么清除:
$count = 0;
\Swoole\Timer::tick(1000, function ($timerId, $count) {
global $count;
echo "Swoole 很棒\n";
$count++;
if ($count == 3) {
\Swoole\Timer::clear($timerId);
}
}, $count);
这种情况下,定时器会在执行三次后退出:
基于 Swoole 定时器实现毫秒级任务调度
我们可以基于 Swoole 定时器实现毫秒级调度任务来替代 Linux 自带的 Cron Job(最小粒度是分钟),以 Laravel 框架为例,我们可以基于 LaravelS 扩展包来定义一个继承自 CronJob
基类的调度任务类:
<?php
namespace App\Jobs\Timer;
use Hhxsv5\LaravelS\Swoole\Timer\CronJob;
use Illuminate\Support\Facades\Log;
class TestCronJob extends CronJob
{
protected $i = 0;
// 该方法可类比为 Swoole 定时器中的回调方法
public function run()
{
Log::info(__METHOD__, ['start', $this->i, microtime(true)]);
$this->i++;
Log::info(__METHOD__, ['end', $this->i, microtime(true)]);
if ($this->i == 3) { // 总共运行3次
Log::info(__METHOD__, ['stop', $this->i, microtime(true)]);
$this->stop(); // 清除定时器
}
}
// 每隔 1000ms 执行一次任务
public function interval()
{
return 1000; // 定时器间隔,单位为 ms
}
// 是否在设置之后立即触发 run 方法执行
public function isImmediate()
{
return false;
}
}
然后到 config/laravels.php
配置文件中修改 timer
配置项如下:
'timer' => [
'enable' => true,
'jobs' => [
// Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab
// \Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
// Two ways to configure parameters:
// [\App\Jobs\XxxCronJob::class, [1000, true]], // Pass in parameters when registering
\App\Jobs\Timer\TestCronJob::class, // Override the corresponding method to return the configuration
],
'max_wait_time' => 5, // Max waiting time of reloading
],
接下来,我们启动或重启 Swoole 服务器:
php bin/laravels start
在 storage/logs
下的最新日志里就可以看到上述定时任务的输出了:
[2019-05-29 22:37:20] local.INFO: App\Jobs\Timer\TestCronJob::run ["start",0,1559140640.694025]
[2019-05-29 22:37:20] local.INFO: App\Jobs\Timer\TestCronJob::run ["end",1,1559140640.752043]
[2019-05-29 22:37:21] local.INFO: App\Jobs\Timer\TestCronJob::run ["start",1,1559140641.752905]
[2019-05-29 22:37:21] local.INFO: App\Jobs\Timer\TestCronJob::run ["end",2,1559140641.754724]
[2019-05-29 22:37:22] local.INFO: App\Jobs\Timer\TestCronJob::run ["start",2,1559140642.694884]
[2019-05-29 22:37:22] local.INFO: App\Jobs\Timer\TestCronJob::run ["end",3,1559140642.696726]
[2019-05-29 22:37:22] local.INFO: App\Jobs\Timer\TestCronJob::run ["stop",3,1559140642.698137]
我们重点关注三个「start」的日志记录,对应的时间戳正好相差 1 秒,符合我们的预期。注意定时器在执行的过程中可能存在一定误差,所以这个毫秒数是数量级级别的,并不是完全相等,而且时间单位越小,误差可能越大,不过这已经比 Linux 自带的任务调度只能精确到分钟要好很多了。
18 Comments
请问我的定时器只执行一次是为什么呢
你定义的是一次性的还是间隔性的定时器
问题
是基于 Swoole 定时器实现毫秒级任务,TestCronJob 执行
php bin/laravels start
,在logs查看日志只是走了一次的开启协程是可以的
如果开启
'enable_coroutine' => true, // 启用协程
这个配置是可以的配置Supervisord也是可以的
如果,开启laradock的php的php-worker容器,配置supervisord.d也是可以的。
但是,这里会启动多次,supervisord-stdout.log会有类似这样的错误
谢谢!
重复启动了啊
php bin/laravels start
只能运行一次,你可以把它类比为启动php-fpm
服务我记得作者不建议开启协程,压测的时候也发现开启协程后,会导致阻塞。
学院君你好,我也遇到了楼下一样的问题,定时器只执行了一次就不执行了 您说的 “你定义的是一次性的还是间隔性的定时器” 这个问题,我不清楚这个在laravels在哪里定义 定时器代码: class TestCronJob extends CronJob { protected $i = 0;
} 配置: 'timer' => [ 'enable' => true, 'jobs' => [ \App\Jobs\Timer\TestCronJob::class, ], 'max_wait_time' => 5, ] 效果: [2019-08-07 18:19:53] local.INFO: App\Jobs\Timer\TestCronJob::run ["start",0,1565173193.588481] [2019-08-07 18:19:53] local.INFO: App\Jobs\Timer\TestCronJob::run ["end",1,1565173193.608589]
解决了,和xdebug冲突且没有日志输出,把xdebug扩展注释重启
为什么我的定时器跑了3遍了没停下来?stop()没作用?
Timer::tick 可以在程序启动的时候就开始运行吗?应该如何做?
stop()没作用+1