高性能千万级定时任务管理服务 forsun Laravel 插件使用详解

Forsun高性能高精度定时服务,轻松管理千万级定时任务。

定时服务项目地址:https://github.com/snower/forsun

laravel插件项目地址: https://github.com/snower/forsun-laravel

  • 轻松支持千万级定时任务调度。
  • 定时任务触发推送到Queue,轻松支持跨机器和共性能分布式。
  • 支持任务到期触发command、Job、Shell、Http和Event。
  • 支持驱动原生Laravel Schedule运行。
  • 支持创建延时任务和定时到期任务,和原生Laravel Schedule保持相同接口,轻松使用。

背景

在实际项目中,存在大量需要定时或是延时触发的任务,比如电商中,延时需要检查订单是否支付成功,是否配送成功,定时给用户推送提醒等等,常规做法是用 crontab 每分钟扫码数据看是否到达时间,繁琐且扩展性伸缩性较差。

使用 forsun 服务,可以简单的针对每一个订单创建一个定时任务,配合异步队列,可以轻松实现扩展性伸缩性,Apache Thrift 的编程接口也可以很容易的和 celery、laravel 配合。

其他场景下,比如失败延时重试,使用 forsun 定时服务也可以很简单就可以实现。

安装

composer require "snower/forsun-laravel"

配置

  • 在 config/app.php 注册 ServiceProvider 和 Facade
'providers' => [
    // ...
    Snower\LaravelForsun\ServiceProvider::class,
],
'aliases' => [
    // ...
    'Forsun' => Snower\LaravelForsun\Facade::class,
],
  • 创建配置文件
php artisan vendor:publish --provider="Snower\LaravelForsun\ServiceProvider"
  • 修改应用根目录下的 config/forsun.php 中对应的参数即可。

使用

定义调度

  • Artisan 命令调度。

//不指定name是自动生成
Forsun::plan()->command('emails:send --force')->daily();

//指定name
Forsun::plan('email')->command(EmailsCommand::class, ['--force'])->daily();
  • 队列任务调度
Forsun::plan()->job(new Heartbeat)->everyFiveMinutes();
  • Shell 命令调度
Forsun::plan()->exec('node /home/forge/script.js')->daily();
  • Event事件调度
Forsun::plan()->fire('testevent', [])->everyMinute();
  • Http事件调度
Forsun::plan()->http('http://www.baidu.com')->everyMinute();

注意:

  • 每个任务只能设置一次调度频率。
  • 不支持任务输出、任务钩子及维护模式。
  • Forsun::plan是不指定任务名时自动生成,每个任务名必须唯一,相同任务名重复定义将会自动覆盖。

移除调度

$plan = Forsun::plan()->command('emails:send --force')->daily();
$plan->remove();

$plan = Forsun::plan()->command('emails:send --force')->daily();
$plan_name = $plan->getName();
Forsun::remove($plan_name);

调度频率设置

方法 描述
->hourly(); 每小时运行
->hourlyAt(17); 每小时的第 17 分钟执行一次任务
->daily(); 每天午夜执行一次任务
->dailyAt('13:00'); 每天的 13:00 执行一次任务
->monthly(); 每月执行一次任务
->monthlyOn(4, '15:00'); 在每个月的第四天的 15:00 执行一次任务
->everyMinute(); 每分钟执行一次任务
->everyFiveMinutes(); 每五分钟执行一次任务
->everyTenMinutes(); 每十分钟执行一次任务
->everyFifteenMinutes(); 每十五分钟执行一次任务
->everyThirtyMinutes(); 每半小时执行一次任务
->at(strtoetime("2018-03-05 12:32:12")); 在指定时间2018-03-05 12:32:12运行一次
->interval(10); 从当前时间开始计算每10秒运行一次
->later(5); 从当前时间开始计算稍后5秒运行一次
->delay(30); 从当前时间开始计算稍后30秒运行一次

需要复杂定时控制建议生成多个定时任务或是在处理器中再次发起定时任务计划更简便同时也性能更高。

调度器应该尽可能使用Event或是Job通过Queue Work可以更高性能运行。

驱动原生Laravel Schedule运行

#注册
php artisan forsun:schedule:register

#取消注册
php artisan forsun:schedule:unregister
本帖已被设为精华帖!
本帖由系统于 6年前 自动加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 39
leo

对于解决的问题有些不太明白。

比如电商中,延时需要检查订单是否支付成功,是否配送成功,定时给用户推送提醒等等,常规做法是用 crontab 每分钟扫码数据看是否到达时间,繁琐且扩展性伸缩性较差。

这个需求直接通过一个 job + delay 参数就可以解决了啊。

至于其他功能感觉和 Laravel 自带的定时任务没有太大区别。

6年前 评论

@leo
如果你仔细研究那么就应该知道,laravel自带的cron只能预先定义而不能在controller和job中动态添加,而且依赖系统crontab也不能很好的跨机器做分布式
第二laravel自带的cron能添加的任务是有限的,顶多几百已经很多了,但在实际项目中有很多都需要很多定时任务的情况,比如像饿了么的超过十五分钟不付款取消顶多,商家超过五分钟取消订单退款等等,这样的都是动辄百万以上的定时任务
第三就是延时任务的问题,虽然queue自带了delay参数,但是很多queue实现的delay并不是很理想,性能和可维护性都很低,一个高性能可管理的延时任务还是很需要的

6年前 评论
lps2128 3年前

@leo job + delay 参数确实可以产生延时任务,区别只是性能和扩展性可管理性

6年前 评论
leo

@snower 没想到什么样的需求需要在 controller 中动态添加定时任务。

如果队列选择 redis 的话 job + delay 其实并没有性能问题。

至于分布式,Laravel 在5.4的时候就已经支持 $schedule->job(new FooBarJob())->everyMinutes(); 这样的定义方式,将定时任务变成 job 来执行。

6年前 评论

@leo 有啊,比如说用户下单的时候同时创建一个支付提醒任务,五分钟还未完成付款就给用户推送一条消息提醒,虽然也可以用轮询查询数据库的方式,但是如果订单量增高的话,这样很容易挂的

这样的场景不要太多了吧,电商的订单超时、支付超时、发货超时、收货超时,快递业务的配送超时等等,虽然都可以用常规crontab轮询数据库的方式,但是随着业务的增长和负责话,轮询的查询量会指数级增长,而且轮询的方式本身是没有办法直接扩容到多台机器的

说到redis队列,redis本身并不是为了队列实现的,只是用了list数据结构近似当作队列,延时也不是一个feature,其每秒能处理的量并没有你想的那么高
另外一个本身就是队列的beanstalk的性能都很低,更不用说database的方式了

6年前 评论
leo

@snower 这个我刚刚也说了呀,job + delay 就可以解决了,下单之后触发一个 delay 5 分钟的 job 不就可以了?这就和轮询没什么关系了。

至于说队列本身的选择,除非您的 forsun 能够有一个强有力的团队或者公司在背后做研发和维护的支撑,否则我相信大多数人更相信成熟度高(稳定性、运维经验等)的 redis 或者 beanstalk。

6年前 评论

@leo 这个强不强大的团队也没什么关系吧,如果你有实际负责使用或者认真研究一下beanstalk这样的代码,就会发现其本省就有很多问题,这也是在实际的系统中运用出来的,并不是有这个功能就是在相对要求更高之下还能很好的运作,laravel的队列设计本省来说还是很弱的,并不适合处理大量任务的情况,我们也是踩了很多坑才得出来的,job + delay实在很弱也要实际在项目中使用才能发现,我分享的也是每秒几千job这样中型项目负责度的一个解决方案,更高需求的时候当然也要更好的方案才行,当然这也是仁者见仁智者见智了

6年前 评论
mouyong

@leo 作者的意思应该是针对情景处理。我的理解大概是。订单若 5 分支未支付就推送提醒:第一个用户下单了,还未支付;大概过了3 分钟,第二个用户下了订单,也还未支付,以此类推,每个用户都没有支付,那么需要针对订单级别创建队列,每个订单都从下单那一刻起有一个 5 分钟延时任务。作者说在控制器中无法动态创建这样的延时队列任务,还提到了 Laravel 自带的 cron 能添加的任务有限。

@snower leo 的意思我理解应该是,如果一个新的东西,背后没有强有力的支撑,中小型的都不敢优先选用。可能 Laravel 自带的 任务系统 配合延时参数就可以解决上面的问题。Job 是可以在控制器中触发的。我也暂时还没有想到在什么样的情况下,需要在控制器中创建定时任务。如果从你的知识储备中有存在,可以分享一下吗?

6年前 评论

@蜗牛 就是你刚才说的这个场景啊,常规做法都是在订单写入数据库后,后台轮询不断查询数据库然后处理的方式,但这样有很多缺点,第一无法自动支持多台机器并行处理,第二订单量比较大的情况下会造成数据库压力非常大,第三在一个相对比较复杂的系统中,一个订单可能有很多个不同定时任务的要求,这样处理量就会非常恐怖了,第四就是主备容灾、自动扩容、监控和一定程度的自动伸缩性的要求了

我们现在每天基本都是数十万的订单量,而且经常会出现订单突然翻倍的情况,一个订单又有接近五六个不同长时定时任务要求,如果轮询扫描数据哭的话,基本每次要扫描数百万订单,如果不是独立的定时任务管理系统,通过直接入定时任务系统,配合修改过的队列系统,省去了大量重复读写数据库的压力才能高效处理。

6年前 评论
颜⑧

前段时间也做了一个,但是没有秒级的功能,看调度上每10秒运行一次,感觉比之前的设计好很多。
看上面的评价 ,问下 大家队列都是 redis 或者 beanstalk ?kafla 有尝试过吗

6年前 评论

@颜⑧ kafla laravel本省没有支持,应该没人用吧,而且感觉laravel队列设计似乎用来支持kafla这样比较大的队伍做worker似乎有点吃力啊,大多数情况下每天亿级别这种消息量,beanstalk还是可以的

6年前 评论
巴啦啦

‘可以简单的针对每一个订单创建一个定时任务,配合异步队列,可以轻松实现扩展性伸缩性’,订单若 5 分支未支付就推送提醒:第一个用户下单了,还未支付;大概过了3 分钟,第二个用户下了订单,也还未支付,以此类推,每个用户都没有支付,那么需要针对订单级别创建队列,每个订单都从下单那一刻起有一个 5 分钟延时任务。能给个demo吗,针对这种情景,你上面没有这种针对订单的延时任务代码。。。感谢

6年前 评论
巴啦啦

比如说,现在A用户下单了,我要在这笔订单的五分钟后执行一次任务,在控制器里该怎么写:bowtie:

6年前 评论

@仰望 其实也是很简单的啊,用户下单的时候订单完成写入数据库拿到订单id之后,根据订单id创建一个延时任务或者根据订单生成时间自动计算生成一个固定时间任务都行

任务到期之后可以触发一个event或者执行一个job都行,触发event的之后任然会执行监听了该Event对应的job了

Forsun::plan("{$prefix}:{$order_id}")->job(new OrderTimeoutHandler())->at('2018-03-14 10:21:22');

// or

Forsun::plan("{$prefix}:{$order_id}")->event("order.timeout", ['oder_id' => $order_id])->delay(20);

固定时间任务区别延时任务的好处是因为针对相同name创建的任务重复创建会覆盖,所以但异常或是容错系统自动纠正的时候可以很方便的根据下单时间重新计算过期时间再次创建任务或者直接处理过期

6年前 评论

标记一下; :+1: 不过感觉自己这辈子都用不到这个插件 :joy:

6年前 评论

标记一下; :thumbsup: 不过感觉自己这辈子都用不到这个插件 :joy:

6年前 评论
hiwangqi

支持下

5年前 评论

现在的公司基本上用不到这个插件了。。 :joy:

5年前 评论

感谢分享,收藏一下,后面要用到 :+1:

5年前 评论
wenber

翻了下楼,有个问题.关于任务移除的问题.
若每笔订单生成后并注册定时提醒付款任务(event or job),在用户进行付款操作后,此时是移除该任务是传入事件名称或者队列任务类名吗?

5年前 评论

@ziyanziyu 不是的,每个定时任务创建的时候需要给一个唯一Key来做ID,移除用这个ID来操作,用相同ID来再创建新的任务会对相同ID执行更新操作,这样可以对接队列的时候很方便的确保不会重复创建很多个相同任务

Forsun::plan("{$prefix}:{$order_id}")

上边这个操作就是制定这个定时任务的ID

5年前 评论

@ziyanziyu 常规来说都是一个job或者event定义对订单的业务处理流程吧,那么对每个订单的处理都是一样的啊,也就是很多个定时任务都只对应一个job或者event处理器,对吧

5年前 评论

@snower 这个点的确是个痛点。我这边有个场景订单新生成后需要做几道多维度的统计和处理,用 job 处理会出现重复的情况,无法避免。用你这个方法貌似能解决。

5年前 评论

非常重要的Bug

@snower

snower/forsun-laravel/src/Builder.php 文件中第90行开始, 以redis作为驱动,你把最最最重要的一个属性password给忽略了。

$connection = Arr::get(Arr::get($queue_config["connections"], 'redis', []), 'connection', Arr::get($database_config, 'default', 'default'));
$database_config = Arr::get($database_config['redis'], $connection, []);
$this->params = [
        'host' => Arr::get($database_config, 'host', '127.0.0.1'),
        'port' => strval(Arr::get($database_config, 'port', 6379)),
        'selected_db' => strval(Arr::get($database_config, 'database', 0)),
    ];
$this->queue_name = Arr::get($queue_config["connections"]["redis"], 'queue', 'default');

当redis设置了密码(requirepass)就导致在运行过程中致命的错误

action redis 1529472738 {u'host': u'127.0.0.1', u'selected_db': u'0', u'command': u'RPUSH \'queues:default\' \'{\\"job\\":\\"Illuminate\\\\\\\\Events\\\\\\\\CallQueuedHandler@call\\",\\"data\\":{\\"class\\":\\"Snower\\\\\\\\LaravelForsun\\\\\\\\Jobs\\\\\\\\CommandRunHandler\\",\\"method\\":\\"handle\\",\\"data\\":\\"a:1:{i:0;s:70:\\\\\\"\\\'\\\\/usr\\\\/local\\\\/Cellar\\\\/php@7.1\\\\/7.1.17\\\\/bin\\\\/php\\\' artisan forsun:test --force\\\\\\";}\\"}}\'', u'port': u'6380'} 

execute error: NOAUTH Authentication required.
5年前 评论

@leo
我也觉得这个没什么卵用, 延迟任务放到队列已经把性能解决, 况且动态添加我觉得没必要, laravel本身自带的事件监听+队列足够满足任何需要触发的推送提醒通知这些,

定时器这个东西无非就是检查多少分钟没支付的订单,自动取消而已, 自己写个cli脚本足够了

5年前 评论

@snower 常规做法都是在订单写入数据库后,后台轮询不断查询数据库然后处理的方式; 这种做法适合小型平台, 大平台有自己的设计, 而不是扫描数据表实现的.

5年前 评论

@BIBIBABIBO 你想想你业务多了 得写多少脚本。。。 本身就是为了方便管理,业务多了,大家的逻辑都是一样的,也容易管理和理解

5年前 评论

你好 有这个插件得demo 么 ,我调用得时候有很多奇怪得 报错

5年前 评论

@leo job + delay的最大问题是无法取消调整,扔进队列你就没办法变更了,每天百万以上定时任务简直就是灾难
Laravel 自带的定时任务则是强依赖系统crontab,跨机器自动化有很大问题,而且最小分钟级别的定时力度在大量任务也让机器存在突发负载,遇到异常情况可能雪崩,而且针对订单超时这样情况只能使用轮询方式,单机轮询量有限

5年前 评论

各种实现请参考 Linux 底层队列,或者定时任务的实现,其实道理都是一样的,所有定时任务从表面上看都是触发一个事件或者其他,其实没有那么玄乎,计算机只能靠轮询来处理,性能无外乎阻塞或非阻塞,机器永远是机器,只能不断的去检测,最底层也是轮询的,包括系统的事件驱动和消息驱动模式,各种高性能,高并发的服务架构也都是采用异步、非阻塞,或者结合的模式来设计的,我大致翻了翻作者的源码,发现差不多也是这样子,RPC 确实是实现分布式的一个不错的方式,另外其他热备,容灾,扩容之类的,其实说到底就是能否降低耦合,完善异常处理机制,耦合度越低,分布式越好做,扩展性也更好,很多高大上的设计、机制已经模式,底层都跑不了的,其实大家文字沟通的问题,我觉得所说的每种方案都有这些影子在里边,可能性能有高下之分,但是还没有到,换一种解决不了的地步,纯属跟人见解,勿喷

4年前 评论

还是给作者点赞,撸码不易,分享更不易 :+1:

4年前 评论

@Leesinyii 严格来说任何软件地城都是一样的,cpu都是一样的,操作系统提供的功能也就这么的,无论如何我们不可能超越现实和物理限制,所以软件架构设计的核心与重要性在于如何组合这些有限而又简单的底层功能,表达自己的创意而又扬长避短,在性能、维护性、便利性、稳定性间尽可能做最佳取舍,以分立内聚的单立功能构成复杂成长性系统,以适应复杂的外部环境,从这一点来说,其实现实世界、人生也是以相似原理来构建的,或者说软件架构设计本身即参照现实世界环境来构建,所以合适的东西用在合适的地方吧,没什么特别的

4年前 评论

你好,我安装完报错,是怎么回事Forsun::plan Non-static method Snower\LaravelForsun\Forsun::plan() should not be called statically 可是我已近按照你的已经引入了,一直不行,请指教 ;我用的是laraval7+php7.2

3年前 评论

file 这个端口是我网站部署的端口还是这个插件的端口

file【表情】

1年前 评论
snower (楼主) 11个月前
snower (楼主) 11个月前

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